def __init__(self, name): """ Create a factory and world. ``name`` is the string used to look up factory-specific settings from the configuration. :param str name: internal name of this factory """ self.name = name self.config_name = "world %s" % name self.world = World(self.name) self.world.factory = self self.protocols = dict() self.connectedIPs = defaultdict(int) self.limitConnections = configuration.getintdefault(self.config_name, "limitConnections", 0) self.limitPerIP = configuration.getintdefault(self.config_name, "limitPerIP", 0) self.furnace_manager = FurnaceManager(self) self.vane = WeatherVane(self)
class BravoFactory(Factory): """ A ``Factory`` that creates ``BravoProtocol`` objects when connected to. """ implements(IPushProducer) protocol = BravoProtocol timestamp = None time = 0 day = 0 eid = 1 handshake_hook = None login_hook = None interfaces = [] def __init__(self, name): """ Create a factory and world. ``name`` is the string used to look up factory-specific settings from the configuration. :param str name: internal name of this factory """ self.name = name self.config_name = "world %s" % name self.world = World(self.name) self.world.factory = self self.protocols = dict() self.connectedIPs = defaultdict(int) self.limitConnections = configuration.getintdefault(self.config_name, "limitConnections", 0) self.limitPerIP = configuration.getintdefault(self.config_name, "limitPerIP", 0) self.furnace_manager = FurnaceManager(self) self.vane = WeatherVane(self) def startFactory(self): log.msg("Initializing factory for world '%s'..." % self.name) authenticator = configuration.get(self.config_name, "authenticator") selected = retrieve_named_plugins(IAuthenticator, [authenticator])[0] log.msg("Using authenticator %s" % selected.name) self.handshake_hook = selected.handshake self.login_hook = selected.login # Get our plugins set up. self.register_plugins() log.msg("Starting world...") self.world.start() if configuration.has_option(self.config_name, "perm_cache"): cache_level = configuration.getint(self.config_name, "perm_cache") self.world.enable_cache(cache_level) log.msg("Starting timekeeping...") self.timestamp = reactor.seconds() self.time = self.world.time self.update_season() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(2) log.msg("Starting entity updates...") self.entity_loop = LoopingCall(self.update_entities) self.entity_loop.start(.2) log.msg("Starting furnaces...") self.furnace_manager.start() # Start automatons. for automaton in self.automatons: automaton.start() self.chat_consumers = set() log.msg("Factory successfully initialized for world '%s'!" % self.name) def stopFactory(self): """ Called before factory stops listening on ports. Used to perform shutdown tasks. """ log.msg("Shutting down world...") # Stop automatons. Technically, they may not actually halt until their # next iteration, but that is close enough for us, probably. # Automatons are contracted to not access the world after stop() is # called. for automaton in self.automatons: automaton.stop() self.time_loop.stop() self.entity_loop.stop() self.furnace_manager.stop() # Write back current world time. This must be done before stopping the # world. self.world.time = self.time # And now stop the world. self.world.stop() log.msg("World data saved!") def buildProtocol(self, addr): """ Create a protocol. This overriden method provides early player entity registration, as a solution to the username/entity race that occurs on login. """ banned = self.world.serializer.load_plugin_data("banned_ips") # Do IP bans first. for ip in banned.split(): if addr.host == ip: # Use KickedProtocol with extreme prejudice. log.msg("Kicking banned IP %s" % addr.host) p = KickedProtocol("Sorry, but your IP address is banned.") p.factory = self return p # We are ignoring values less that 1, but making sure not to go over # the connection limit. if (self.limitConnections and len(self.protocols) >= self.limitConnections): log.msg("Reached maximum players, turning %s away." % addr.host) p = KickedProtocol("The player limit has already been reached." " Please try again later.") p.factory = self return p # This probably a needs cleanup. We need to see if that IP has # connected already, check if +1 connections is ok, and ignore # values less than 1. if (self.limitPerIP and addr.host in self.connectedIPs.keys() and self.connectedIPs[addr.host] >= self.limitPerIP): log.msg("At maximum connections for %s already, dropping." % addr.host) p = KickedProtocol("There are too many players connected from this IP.") p.factory = self return p else: self.connectedIPs[addr.host] += 1 # If the player wasn't kicked, let's continue! log.msg("Starting connection for %s" % addr) p = self.protocol(self.name) p.host = addr.host p.factory = self self.register_entity(p) # Copy our hooks to the protocol. p.register_hooks() return p def set_username(self, protocol, username): """ Attempt to set a new username for a protocol. :returns: whether the username was changed """ # If the username's already taken, refuse it. if username in self.protocols: return False if protocol.username in self.protocols: # This protocol's known under another name, so remove it. del self.protocols[protocol.username] # Set the username. self.protocols[username] = protocol protocol.username = username return True def register_plugins(self): """ Setup plugin hooks. """ log.msg("Registering client plugin hooks...") plugin_types = { "automatons": IAutomaton, "generators": ITerrainGenerator, "seasons": ISeason, "open_hooks": IWindowOpenHook, "click_hooks": IWindowClickHook, "close_hooks": IWindowCloseHook, "pre_build_hooks": IPreBuildHook, "post_build_hooks": IPostBuildHook, "dig_hooks": IDigHook, "sign_hooks": ISignHook, "use_hooks": IUseHook, } pp = {"factory": self, "furnaces": self.furnace_manager} for t, interface in plugin_types.iteritems(): l = configuration.getlistdefault(self.config_name, t, []) if issubclass(interface, ISortedPlugin): plugins = retrieve_sorted_plugins(interface, l, parameters=pp) else: plugins = retrieve_named_plugins(interface, l, parameters=pp) log.msg("Using %s: %s" % (t.replace("_", " "), ", ".join(plugin.name for plugin in plugins))) setattr(self, t, plugins) # Assign generators to the world pipeline. self.world.pipeline = self.generators # Use hooks have special funkiness. uh = self.use_hooks self.use_hooks = defaultdict(list) for plugin in uh: for target in plugin.targets: self.use_hooks[target].append(plugin) def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity. Handles entity registration as well as instantiation. """ location = Location() location.x = x location.y = y location.z = z entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.add(entity)) d.addCallback(lambda none: log.msg("Created entity %s" % entity)) return entity def register_entity(self, entity): """ Registers an entity with this factory. Registration is perhaps too fancy of a name; this method merely makes sure that the entity has a unique and usable entity ID. """ if not entity.eid: self.eid += 1 entity.eid = self.eid log.msg("Registered entity %s" % entity) def destroy_entity(self, entity): """ Destroy an entity. The factory doesn't have to know about entities, but it is a good place to put this logic. """ bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.discard(entity)) d.addCallback(lambda none: log.msg("Destroyed entity %s" % entity)) def update_entities(self): """ Update all entities covered by this factory. """ # XXX this method could cause chunks to be generated :c points = set() for player in self.protocols.itervalues(): x = player.location.x z = player.location.z bigx, chaff, bigz, chaff = split_coords(x, z) new = set((i + bigx, j + bigz) for i, j in circle) points.update(new) for x, y in points: d = self.world.request_chunk(x, y) d.addCallback(lambda chunk: chunk.update_entities(self)) def update_time(self): """ Update the in-game timer. The timer goes from 0 to 24000, both of which are high noon. The clock increments by 20 every second. Days are 20 minutes long. The day clock is incremented every in-game day, which is every 20 minutes. The day clock goes from 0 to 360, which works out to a reset once every 5 days. This is a Babylonian in-game year. """ t = reactor.seconds() self.time += 20 * (t - self.timestamp) self.timestamp = t days, self.time = divmod(self.time, 24000) if days: self.day += days self.day %= 360 self.update_season() def broadcast_time(self): packet = make_packet("time", timestamp=int(self.time)) self.broadcast(packet) def update_season(self): """ Update the world's season. """ all_seasons = sorted(self.seasons, key=lambda s: s.day) # Get all the seasons that we have past the start date of this year. # We are looking for the season which is closest to our current day, # without going over; I call this the Price-is-Right style of season # handling. :3 past_seasons = [s for s in all_seasons if s.day <= self.day] if past_seasons: # The most recent one is the one we are in self.world.season = past_seasons[-1] elif all_seasons: # We haven't past any seasons yet this year, so grab the last one # from 'last year' self.world.season = all_seasons[-1] else: # No seasons enabled. self.world.season = None def chat(self, message): """ Relay chat messages. Chat messages are sent to all connected clients, as well as to anybody consuming this factory. """ for consumer in self.chat_consumers: consumer.write((self, message)) # Prepare the message for chat packeting. for user in self.protocols: message = message.replace(user, chat_name(user)) message = sanitize_chat(message) log.msg("Chat: %s" % message.encode("utf8")) packet = make_packet("chat", message=message) self.broadcast(packet) def broadcast(self, packet): """ Broadcast a packet to all connected players. """ for player in self.protocols.itervalues(): player.transport.write(packet) def broadcast_for_others(self, packet, protocol): """ Broadcast a packet to all players except the originating player. Useful for certain packets like player entity spawns which should never be reflexive. """ for player in self.protocols.itervalues(): if player is not protocol: player.transport.write(packet) def broadcast_for_chunk(self, packet, x, z): """ Broadcast a packet to all players that have a certain chunk loaded. `x` and `z` are chunk coordinates, not block coordinates. """ for player in self.protocols.itervalues(): if (x, z) in player.chunks: player.transport.write(packet) def scan_chunk(self, chunk): """ Tell automatons about this chunk. """ for automaton in self.automatons: automaton.scan(chunk) def flush_chunk(self, chunk): """ Flush a damaged chunk to all players that have it loaded. """ if chunk.is_damaged(): packet = chunk.get_damage_packet() for player in self.protocols.itervalues(): if (chunk.x, chunk.z) in player.chunks: player.transport.write(packet) chunk.clear_damage() def flush_all_chunks(self): """ Flush any damage anywhere in this world to all players. This is a sledgehammer which should be used sparingly at best, and is only well-suited to plugins which touch multiple chunks at once. In other words, if I catch you using this in your plugin needlessly, I'm gonna have a chat with you. """ for chunk in chain(self.world.chunk_cache.itervalues(), self.world.dirty_chunk_cache.itervalues()): self.flush_chunk(chunk) def give(self, coords, block, quantity): """ Spawn a pickup at the specified coordinates. The coordinates need to be in pixels, not blocks. If the size of the stack is too big, multiple stacks will be dropped. :param tuple coords: coordinates, in pixels :param tuple block: key of block or item to drop :param int quantity: number of blocks to drop in the stack """ x, y, z = coords while quantity > 0: entity = self.create_entity(x // 32, y // 32, z // 32, "Item", item=block, quantity=min(quantity, 64)) packet = entity.save_to_packet() packet += make_packet("create", eid=entity.eid) self.broadcast(packet) quantity -= 64 def players_near(self, player, radius): """ Obtain other players within a radius of a given player. Radius is measured in blocks. """ for i in (p for p in self.protocols.itervalues() if player.location.distance(p.location) <= radius and p.player != player): yield i.player def pauseProducing(self): pass def resumeProducing(self): pass def stopProducing(self): pass