def __init__(self, *arg, **kw): # +2 to allow server->master and master->server connection since enet # allocates peers for both clients and hosts. this is done at # enet-level, not application-level, so even for masterless-servers, # this should not allow additional players. self.max_connections = self.max_players + 2 BaseProtocol.__init__(self, *arg, **kw) self.entities = [] self.players = MultikeyDict() self.player_ids = IDPool() self._create_teams() self.world = world.World() self.set_master() # safe position LUT # # Generates a LUT to check for safe positions. The slighly weird # sorting is used to sort by increasing distance so the nearest spots # get chosen first # product(repeat=3) is the equivalent of 3 nested for loops self.pos_table = list(product(range(-5, 6), repeat=3)) self.pos_table.sort(key=lambda vec: abs(vec[0] * 1.03) + abs(vec[ 1] * 1.02) + abs(vec[2] * 1.01))
def test_misc_funcs(self): dic = MultikeyDict() dic[1, 'bar'] = 2 dic[3, 'baz'] = 5 self.assertEqual(len(dic), 2) del dic[1] self.assertEqual(len(dic), 1) dic.clear() self.assertEqual(len(dic), 0)
def set_map(self, map_obj): self.map = map_obj self.world.map = map_obj self.on_map_change(map_obj) self.team_1.initialize() self.team_2.initialize() if self.game_mode == TC_MODE: self.reset_tc() self.players = MultikeyDict() if self.connections: data = ProgressiveMapGenerator(self.map, parent=True) for connection in list(self.connections.values()): if connection.player_id is None: continue if connection.map_data is not None: connection.disconnect() continue connection.reset() connection._send_connection_data() connection.send_map(data.get_child()) self.update_entities()
def __init__(self, protocol): BaseConnection.__init__(self) self.protocol = protocol self.auth_val = random.randint(0, 0xFFFF) self.map = ByteWriter() self.connections = MultikeyDict() self.spammy = { Ping: 0, loaders.MapChunk: 0, loaders.OrientationData: 0, loaders.PositionData: 0, loaders.InputData: 0 } connect_request = ConnectionRequest() connect_request.auth_val = self.auth_val connect_request.client = True connect_request.version = self.get_version() self.send_loader(connect_request, False, 255)
class ServerProtocol(BaseProtocol): connection_class = ServerConnection name = 'pyspades server' game_mode = CTF_MODE max_players = 32 connections = None player_ids = None master = False max_score = 10 map = None spade_teamkills_on_grief = False friendly_fire = False friendly_fire_time = 2 server_prefix = '[*] ' respawn_time = 5 refill_interval = 20 master_connection = None speedhack_detect = True fog_color = (128, 232, 255) winning_player = None world = None team_class = Team team1_color = (0, 0, 196) team2_color = (0, 196, 0) team1_name = 'Blue' team2_name = 'Green' spectator_name = 'Spectator' loop_count = 0 melee_damage = 100 version = GAME_VERSION respawn_waves = False def __init__(self, *arg, **kw): # +2 to allow server->master and master->server connection since enet # allocates peers for both clients and hosts. this is done at # enet-level, not application-level, so even for masterless-servers, # this should not allow additional players. self.max_connections = self.max_players + 2 BaseProtocol.__init__(self, *arg, **kw) self.entities = [] self.players = MultikeyDict() self.player_ids = IDPool() self._create_teams() self.world = world.World() self.set_master() # safe position LUT # # Generates a LUT to check for safe positions. The slighly weird # sorting is used to sort by increasing distance so the nearest spots # get chosen first # product(repeat=3) is the equivalent of 3 nested for loops self.pos_table = list(product(range(-5, 6), repeat=3)) self.pos_table.sort(key=lambda vec: abs(vec[0] * 1.03) + abs(vec[ 1] * 1.02) + abs(vec[2] * 1.01)) def _create_teams(self): """create the teams This Method is separate to simplify unit testing """ self.team_spectator = self.team_class(-1, self.spectator_name, (0, 0, 0), True, self) self.team_1 = self.team_class(0, self.team1_name, self.team1_color, False, self) self.team_2 = self.team_class(1, self.team2_name, self.team2_color, False, self) self.teams = {-1: self.team_spectator, 0: self.team_1, 1: self.team_2} self.team_1.other = self.team_2 self.team_2.other = self.team_1 @property def blue_team(self): """alias to team_1 for backwards-compatibility""" return self.team_1 @property def green_team(self): """alias to team_2 for backwards-compatibility""" return self.team_2 @property def spectator_team(self): """alias to team_spectator for backwards-compatibility""" return self.team_spectator def broadcast_contained(self, contained, unsequenced=False, sender=None, team=None, save=False, rule=None): """send a Contained `Loader` to all or a selection of connected players Parameters: contained: the `Loader` object to send unsequenced: set the enet ``UNSEQUENCED`` flag on this packet sender: if set to a connection object, do not send this packet to that player, as they are the sender. team: if set to a team, only send the packet to that team save: if the player has not downloaded the map yet, save this packet and send it when the map transfer has completed rule: if set to a callable, this function is called with the player as parameter to determine if a given player should receive the packet """ if unsequenced: flags = enet.PACKET_FLAG_UNSEQUENCED else: flags = enet.PACKET_FLAG_RELIABLE writer = ByteWriter() contained.write(writer) data = bytes(writer) packet = enet.Packet(data, flags) for player in self.connections.values(): if player is sender or player.player_id is None: continue if team is not None and player.team is not team: continue if rule is not None and not rule(player): continue if player.saved_loaders is not None: if save: player.saved_loaders.append(data) else: player.peer.send(0, packet) # backwards compatability send_contained = broadcast_contained def reset_tc(self): self.entities = self.get_cp_entities() for entity in self.entities: team = entity.team if team is None: entity.progress = 0.5 else: team.score += 1 entity.progress = float(team.id) tc_data.set_entities(self.entities) self.max_score = len(self.entities) def get_cp_entities(self): # cool algorithm number 1 entities = [] land_count = self.map.count_land(0, 0, 512, 512) territory_count = int((land_count / (512.0 * 512.0)) * (MAX_TERRITORY_COUNT - MIN_TERRITORY_COUNT) + MIN_TERRITORY_COUNT) j = 512.0 / territory_count for i in range(territory_count): x1 = i * j y1 = 512 / 4 x2 = (i + 1) * j y2 = y1 * 3 flag = Territory(i, self, *self.get_random_location(zone=(x1, y1, x2, y2))) if i < territory_count / 2: team = self.team_1 elif i > (territory_count - 1) / 2: team = self.team_2 else: # odd number - neutral team = None flag.team = team entities.append(flag) return entities def update(self): self.loop_count += 1 BaseProtocol.update(self) for player in self.connections.values(): if (player.map_data is not None and not player.peer.reliableDataInTransit): player.continue_map_transfer() self.world.update(UPDATE_FREQUENCY) self.on_world_update() if self.loop_count % int(UPDATE_FPS / NETWORK_FPS) == 0: self.update_network() def update_network(self): items = [] highest_player_id = 0 for i in range(32): position = orientation = None try: player = self.players[i] highest_player_id = i if (not player.filter_visibility_data and not player.team.spectator): world_object = player.world_object position = world_object.position.get() orientation = world_object.orientation.get() except (KeyError, TypeError, AttributeError): pass if position is None: position = (0.0, 0.0, 0.0) orientation = (0.0, 0.0, 0.0) items.append((position, orientation)) world_update = loaders.WorldUpdate() # we only want to send as many items of the player list as needed, so # we slice it off at the highest player id world_update.items = items[:highest_player_id + 1] self.send_contained(world_update, unsequenced=True) def set_map(self, map_obj): self.map = map_obj self.world.map = map_obj self.on_map_change(map_obj) self.team_1.initialize() self.team_2.initialize() if self.game_mode == TC_MODE: self.reset_tc() self.players = MultikeyDict() if self.connections: data = ProgressiveMapGenerator(self.map, parent=True) for connection in list(self.connections.values()): if connection.player_id is None: continue if connection.map_data is not None: connection.disconnect() continue connection.reset() connection._send_connection_data() connection.send_map(data.get_child()) self.update_entities() def reset_game(self, player=None, territory=None): """reset the score of the game player is the player which should be awarded the necessary captures to end the game """ self.team_1.initialize() self.team_2.initialize() if self.game_mode == CTF_MODE: if player is None: player = list(self.players.values())[0] intel_capture = loaders.IntelCapture() intel_capture.player_id = player.player_id intel_capture.winning = True self.send_contained(intel_capture, save=True) elif self.game_mode == TC_MODE: if territory is None: territory = self.entities[0] territory_capture = loaders.TerritoryCapture() territory_capture.object_index = territory.id territory_capture.winning = True territory_capture.state = territory.team.id self.send_contained(territory_capture) self.reset_tc() for entity in self.entities: entity.update() for player in self.players.values(): if player.team is not None: player.spawn() def get_name(self, name): ''' Sanitizes `name` and modifies it so that it doesn't collide with other names connected to the server. Returns the fixed name. ''' name = name.replace('%', '') new_name = name names = [p.name.lower() for p in self.players.values()] i = 0 while new_name.lower() in names: i += 1 new_name = name + str(i) return new_name def get_mode_mode(self): if self.game_mode == CTF_MODE: return 'ctf' elif self.game_mode == TC_MODE: return 'tc' return 'unknown' def get_random_location(self, force_land=True, zone=(0, 0, 512, 512)): x1, y1, x2, y2 = zone if force_land: x, y = self.map.get_random_point(x1, y1, x2, y2) else: x = random.randrange(x1, x2) y = random.randrange(y1, y2) z = self.map.get_z(x, y) return x, y, z def set_master(self): if self.master: get_master_connection(self).addCallbacks( self.got_master_connection, self.master_disconnected) def got_master_connection(self, connection): self.master_connection = connection connection.disconnect_callback = self.master_disconnected self.update_master() def master_disconnected(self, client=None): self.master_connection = None def get_player_count(self): count = 0 for connection in self.connections.values(): if connection.player_id is not None: count += 1 return count def update_master(self): if self.master_connection is None: return self.master_connection.set_count(self.get_player_count()) def update_entities(self): map_obj = self.map for entity in self.entities: moved = False if map_obj.get_solid(entity.x, entity.y, entity.z - 1): moved = True entity.z -= 1 # while solid in block above (ie. in the space in which the # entity is sitting), move entity up) while map_obj.get_solid(entity.x, entity.y, entity.z - 1): entity.z -= 1 else: # get_solid can return None, so a more specific check is used while map_obj.get_solid(entity.x, entity.y, entity.z) is False: moved = True entity.z += 1 if moved or self.on_update_entity(entity): entity.update() def broadcast_chat(self, message, global_message=None, sender=None, team=None): for player in self.players.values(): if player is sender: continue if player.deaf: continue if team is not None and player.team is not team: continue player.send_chat(message, global_message) # backwards compatability send_chat = broadcast_chat def broadcast_chat_warning(self, message, team=None): """ Send a warning message. This gets displayed as a yellow popup with sound for OpenSpades clients """ self.send_chat(self, "%% " + str(message), team=team) def broadcast_chat_notice(self, message, team=None): """ Send a warning message. This gets displayed as a popup for OpenSpades clients """ self.send_chat(self, "N% " + str(message), team=team) def broadcast_chat_error(self, message, team=None): """ Send a warning message. This gets displayed as a red popup with sound for OpenSpades clients """ self.send_chat(self, "!% " + str(message), team=team) def broadcast_chat_status(self, message, team=None): """ Send a warning message. This gets displayed as a red popup with sound for OpenSpades clients """ self.send_chat(self, "C% " + str(message), team=team) def set_fog_color(self, color): self.fog_color = color fog_color = loaders.FogColor() fog_color.color = make_color(*color) self.send_contained(fog_color, save=True) def get_fog_color(self): return self.fog_color # events def on_cp_capture(self, cp): pass def on_game_end(self): pass def on_world_update(self): pass def on_map_change(self, map_): pass def on_base_spawn(self, x, y, z, base, entity_id): pass def on_flag_spawn(self, x, y, z, flag, entity_id): pass def on_update_entity(self, entity): pass
def test_get(self): dic = MultikeyDict() dic[7, 'egg'] = 42 self.assertEqual(dic.get(7), 42) self.assertEqual(dic.get("egg"), 42) self.assertEqual(dic.get("spam", "def"), "def")
def test_assign_multiple(self): dic = MultikeyDict() dic[1, 'bar'] = 2 with self.assertRaises(KeyError): dic[3, 'bar'] = 5
def test_identity(self): dic = MultikeyDict() lst = ("hi", ) dic["key", ("tup", "le")] = lst self.assertIs(dic["key"], lst) self.assertIs(dic["tup", "le"], lst)
def test_create(self): dic = MultikeyDict() dic[1, 'bar'] = 2 self.assertEqual(dic[1], 2) self.assertIs(dic[1], dic['bar'])
def __init__(self, *args, **kwargs): self._buttons = MultikeyDict() self._platforms = {} self._distance_triggers = set() self._autosave_loop = LoopingCall(self.dump_platform_json) protocol.__init__(self, *args, **kwargs)
class PlatformProtocol(protocol): _next_id = 0 def __init__(self, *args, **kwargs): self._buttons = MultikeyDict() self._platforms = {} self._distance_triggers = set() self._autosave_loop = LoopingCall(self.dump_platform_json) protocol.__init__(self, *args, **kwargs) def add_distance_trigger(self, trigger): self._distance_triggers.add(trigger) trigger.signal_remove.connect(self._distance_triggers.remove) def update_distance_triggers(self, player): for trigger in self._distance_triggers: trigger.update(player) def on_world_update(self): for player in self.players.values(): self.update_distance_triggers(player) protocol.on_world_update(self) def assign_id(self): id_ = self._next_id self._next_id += 1 return id_ def add_button(self, button): self._buttons[(button.id, button.location)] = button def create_button(self, location, color, label): if self.is_platform_or_button(location): return None button = Button(self, self._next_id, location, color, label) button.add_trigger(PressTrigger(self, False, button)) self._buttons[(self._next_id, location)] = button self._next_id += 1 return button def destroy_button(self, button): button.destroy() del self._buttons[button] for player in self.players.values( ): # clear last button memory from players if player.last_button is button: player.last_button = None def create_platform(self, location1, location2, z, color, label): for location in prism_range(*location1, z, *location2, z + 1): if self.is_platform_or_button(location): return None platform = Platform(self, self._next_id, location1, location2, z, color, label) self._platforms[self._next_id] = platform self._next_id += 1 return platform def destroy_platform(self, platform): platform.destroy() del self._platforms[platform.id] for player in self.players.values( ): # clear last platform memory from players if player.last_platform is platform: player.last_platform = None def get_platform(self, location_or_id): if location_or_id in self._platforms: return self._platforms[location_or_id] for platform in self._platforms.values(): if platform.contains(location_or_id): return platform def get_button(self, location_or_id): if location_or_id in self._buttons: return self._buttons[location_or_id] def is_platform_or_button(self, location): return self.get_platform(location) or location in self._buttons def on_map_change(self, map): self._next_id = 0 self._platforms.clear() self._buttons.clear() self._distance_triggers.clear() self.load_platform_json() if AUTOSAVE_EVERY: self._autosave_loop.start(AUTOSAVE_EVERY * 60.0, now=False) protocol.on_map_change(self, map) def on_map_leave(self): if SAVE_ON_MAP_CHANGE: self.dump_platform_json() if self._autosave_loop.running: self._autosave_loop.stop() protocol.on_map_leave(self) def _get_platform_json_path(self): filename = self.map_info.rot_info.full_name + '_platform.txt' return os.path.join(DEFAULT_LOAD_DIR, filename) def load_platform_json(self): path = self._get_platform_json_path() if not os.path.isfile(path): return with open(path, 'r') as file: data = json.load(file) ids = [] for platform_data in data['platforms']: platform = Platform.unserialize(self, platform_data) self._platforms[platform.id] = platform ids.append(platform.id) for button_data in data['buttons']: button = Button.unserialize(self, button_data) self._buttons[(button.id, button.location)] = button ids.append(button.id) self._next_id = max(ids) + 1 if ids else 0 for button in self._buttons.values(): button.trigger_check() def dump_platform_json(self): data = { 'platforms': [ platform.serialize() for platform in self._platforms.values() ], 'buttons': [button.serialize() for button in self._buttons.values()] } path = self.get_platform_json_path() with open(path, 'w') as file: json.dump(data, file, indent=4)