class World(object): """ Defines a shard of the world. This is equivalent to js/worldserver.js:World """ def __init__(self, world_id, max_players, server): self.world_id = world_id self.max_players = max_players self.server = server # updates per second. self.ups = 50. self.regen_count = self.ups * 2 self.update_count = 0 self.map = None self.entities = {} self.players = {} self.mobs = {} self.attackers = {} self.items = {} self.equipping = {} self.hurt = {} self.npcs = {} self.mob_areas = [] self.chest_areas = [] self.groups = {} self.outgoing_queues = {} self.item_count = self.player_count = 0 self.zone_groups_ready = False self.keep_running = True def on_player_connect(self, player): def on_request_position(): if player.last_checkpoint: return player.last_checkpoint.get_random_position() else: return self.map.get_random_starting_position() player.on_request_position = on_request_position def on_player_enter(self, player): logger.info("%s has joined world %d", player.name, self.world_id) player.world = self if not player.has_entered_game: self.player_count += 1 # send the population and relevant entities in this world player.send(PopulationMessage(self.player_count)) self.push_relevant_entities(player) if self.added_callback: self.added_callback() def on_entity_attack(self, attacker): "Called when an entity is attacked by another entity." target = self.get_entity_by_id(attacker.target) if target and type(attacker) is Mob: pos = self.find_position_next_to(attacker, target) self.move_entity(attacker, *pos) def regen_tick(self): logger.info('regen_tick called') for character in self.characters(): if not character.has_full_health(): character.regen_health_by(character.max_hit_points // 25.) if type(character) == Player: self.push_to_player(character, character.regen()) def run(self, map_file_path): # this function call is sync in python # don't need to wait for callbacks. self.map = Map(map_file_path) # generate_collision_grid called from Map.init_map self.map.populate_world(self) # start main loop in another thread self.main_loop = Thread(target=self.loop) self.main_loop.start() def loop(self): while self.keep_running: self.process_groups() self.process_queues() if self.update_count < self.regen_count: self.update_count += 1 else: self.regen_tick() self.update_count = 0 time.sleep(1.0 / self.ups) def characters(self): "Returns a list of players and mobs" for x in self.players.values(): yield x for x in self.mobs.values(): yield x def set_updates_per_seconds(self, ups): self.ups = ups def push_relevant_entity_list_to(self, player): entities = [] if player and player.group in self.groups: entities = self.groups[player.group].entities.keys() if player.entity_id in entities: entities.remove(player.entity_id) # before sending to client, all entities must be an int if entities: self.push_to_player(player, ListMessage((int(x) for x in entities))) def push_spawns_to_player(self, player, ids): "Push entity spawns to the client" for eid in ids: eid = str(eid) if eid in self.entities: self.push_to_player(player, SpawnMessage(self.entities[eid])) def push_to_player(self, player, message): if player and player.entity_id in self.outgoing_queues: self.outgoing_queues[player.entity_id].append(message) else: logger.warn('player was undefined') def push_to_group(self, group, message, ignored_player=None): """ API change from JS: takes a group as the argument, not a group id """ #if group_id in self.groups: #group = self.groups[group_id] for player in group.players: if player.entity_id != ignored_player: self.push_to_player(player, message) #else: # logger.warn('groupid %r is not a valid group' % group_id) def push_to_adjacent_groups(self, group, message, ignored_player=None): """ API change from JS: takes a group as the argument, not a group id """ for g in self.map.get_adjacent_group_positions(group.group_id): if g in self.groups: self.push_to_group(self.groups[g], message, ignored_player) def push_to_previous_groups(self, player, message): # push this message to all groups which are not going to be updated # anymore, as the player left them. for g in player.recently_left_groups: self.push_to_group(g, message) player.recently_left_groups = [] def push_broadcast(self, message, ignored_player=None): for player_id in self.outgoing_queues: if player_id != ignored_player: self.outgoing_queues[player_id].append(message) def process_queues(self): for player_id, q in enumerate(self.outgoing_queues): player = self.players[player_id] for msg in q: player.send(q) q = [] def add_entity(self, entity): self.entities[entity.entity_id] = entity self.handle_entity_group_membership(entity) def remove_entity(self, entity): if entity.entity_id in self.entities: del self.entities[entity.entity_id] if entity.entity_id in self.mobs: del self.items[entity.entity_id] self.clear_mob_aggro_link(entity) if entity.entity_id in self.items: del self.items[entity.entity_id] entity.destroy() self.remove_from_groups(entity) logger.info('removed entity %r' % entity) def add_player(self, player): self.add_entity(player) self.players[player.entity_id] = player self.outgoing_queues[player.entity_id] = [] def remove_player(self, player): player.broadcast(DespawnMessage(player)) self.remove_entity(player) del self.players[player.entity_id] del self.outgoing_queues[player.entity_id] def add_mob(self, mob): self.add_entity(mob) self.mobs[mob.entity_id] = mob def add_npc(self, kind, x, y): npc = Npc('8%s%s' % (x, y), kind, x, y) self.add_entity(npc) self.npcs[npc.entity_id] = npc return npc def add_item(self, item): self.add_entity(item) self.items[item.entity_id] = item return item def create_item(self, kind, x, y): eid = '9%s' % self.item_count self.item_count += 1 if kind == ENTITIES['CHEST']: item = Chest(eid, x, y) else: item = Item(eid, kind, x, y) return item def create_chest(self, x, y, items): chest = self.create_item(ENTITIES['CHEST'], x, y) chest.items = items return chest def add_static_item(self, item): item.is_static = True # TODO: on_respawn handler self.add_item(item) def add_item_from_chest(kind, x, y): item = self.create_item(kind, x, y) item.is_from_chest = True return self.add_item(item) def is_valid_position(self, x, y): if self.map: x, y = int(x), int(y) return not (self.map.is_out_of_bounds(x, y) or self.map.is_colliding(x, y)) return False def process_groups(self): if self.zone_groups_ready: for gid in self.map.groups(): group = self.groups[gid] for entity in group.incoming: if type(entity) is Player: self.push_to_group(group, SpawnMessage(entity), entity.entity_id) else: self.push_to_group(group, SpawnMessage(entity)) group.incoming = [] def try_adding_mob_to_chest_area(self, mob): for area in self.chest_areas: if area.contains(mob): area.add_to_area(mob) def add_to_group(self, entity, group): new_groups = [] for group_id in self.map.get_adjacent_group_positions(group.group_id): # FIXME: memory leak? if group_id not in self.groups: logger.warn('Invalid group id referenced by add_to_group: %r', group_id) continue self.groups[group_id].entities[entity.entity_id] = entity; new_groups.append(group_id) entity.group = group.group_id return new_groups def remove_from_groups(self, entity): old_groups = [] if entity.group: group = self.groups[entity.group] if entity in group.players: group.players.remove(entity) for group_id in self.map.get_adjacent_group_positions(group.group_id): if group_id in self.groups and entity.entity_id in self.groups[group_id].entities: del self.groups[group_id].entities[entity.entity_id] old_groups.append(group_id) entity.group = None return old_groups def handle_entity_group_membership(self, entity): has_changed_groups = False if entity: group_id = self.map.get_group_id_from_position(entity.x, entity.y) group = self.groups[group_id] if not entity.group or (entity.group != group): has_changed_groups = True self.add_as_incoming_to_group(entity, group) old_groups = set(self.remove_from_groups(entity)) new_groups = set(self.add_to_group(entity, group)) if old_groups: old_groups -= new_groups entity.recently_left_groups = old_groups logger.info('group diff: %r', entity.recently_left_groups) return has_changed_groups def add_as_incoming_to_group(self, entity, group): """ Registers an entity as incoming into several groups, meaning that it just entered them. All players inside these groups will recieve a Spawn message when World.process_groups() is called. """ is_chest = type(entity) is Chest is_item = type(entity) is Item is_dropped_item = is_item and not (entity.is_static or entity.is_from_chest) for group_id in self.map.get_adjacent_group_positions(group.group_id): if group_id in self.groups: group = self.groups[group_id] if entity in group.entities and (not is_item or is_chest or (is_item and not is_dropped_item)): group.incoming.append(entity) else: logger.warn('Invalid group id referenced by add_as_incoming_to_group: %r', group_id) def push_relevant_entities(self, player): # TODO: stub pass