async def get_game_stats(self, game_id): """ Gets the game information. """ query = """ SELECT `game_stats`.`startTime` AS start_time, `game_stats`.`endTime` AS end_time, `game_stats`.`gameType` AS game_type, `login`.`login` AS host, `game_stats`.`gameName` AS game_name, `game_featuredMods`.`gamemod` AS game_mod, `map`.`display_name` as map_name, `map_version`.`filename` AS file_name FROM `game_stats` LEFT JOIN `map` ON `game_stats`.`mapId` = `map`.`id` LEFT JOIN `map_version` ON `map_version`.`map_id` = `map`.`id` LEFT JOIN `login` ON `login`.id = `game_stats`.`host` LEFT JOIN `game_featuredMods` ON `game_stats`.`gameMod` = `game_featuredMods`.`id` WHERE `game_stats`.`id` = %s """ player_query = """ SELECT COUNT(*) FROM `game_player_stats` WHERE `game_player_stats`.`gameId` = %s """ game_stats = await self._db.execute(query, (game_id,)) player_count = await self._db.execute(player_query, (game_id,)) if not game_stats: raise BookkeepingError(f"No stats found for game {game_id}") if not player_count: raise BookkeepingError(f"No players found for game {game_id}") start_time = game_stats[0]['start_time'].timestamp() # We might end a replay before end_time is set in the db! end_time = game_stats[0]['end_time'] if end_time is None: end_time = time.time() else: end_time = end_time.timestamp() return { 'featured_mod': game_stats[0]['game_mod'], 'game_type': game_stats[0]['game_type'], 'recorder': game_stats[0]['host'], 'host': game_stats[0]['host'], 'launched_at': start_time, 'game_end': end_time, 'title': game_stats[0]['game_name'], 'mapname': game_stats[0]['map_name'], 'map_file_path': game_stats[0]['file_name'], 'num_players': player_count[0]['COUNT(*)'] }
async def save_replay(self, game_id, stream): if stream.header is None: raise BookkeepingError("Saved replay has no header") info = await self._get_replay_info(game_id, stream.header.struct) rfile = self._paths.get(game_id) try: with open(rfile, "wb") as f: await self._write_replay_in_thread( f, info, stream.header.data + stream.data.bytes()) except IOError as e: raise BookkeepingError(f"Failed to write replay: {short_exc(e)}")
async def execute(self, query, params=[]): if self._connection_pool is None: raise BookkeepingError("Tried to run query while pool is closed!") try: async with self._connection_pool.acquire() as conn: async with conn.cursor(aiomysql.DictCursor) as cur: await cur.execute(query, *params) data = await cur.fetchall() await conn.commit() return data except (DatabaseError, RuntimeError) as e: raise BookkeepingError(f"Failed to run database query: {str(e)}")
def get_replay_ticks(self, data): """Parse the replay data and extract the total number of ticks Parameters ---------- data: The replay body only (without the header). The first byte must be the start of a valid replay command. """ if not data: raise BookkeepingError("No replay data") try: return body_ticks(data) except ReplayReadError as e: raise BookkeepingError(f"Failed to parse replay: {e}")
async def _get_replay_info(self, game_id, header): result = {} result['uid'] = game_id result['complete'] = True result['state'] = 'PLAYING' try: result['sim_mods'] = { mod['uid']: mod['version'] for mod in header.get('mods', {}).values() } except KeyError: # TODO - validate elsewhere? raise BookkeepingError("Replay header has invalid sim_mods") game_stats = await self._database.get_game_stats(game_id) teams = await self._database.get_teams_in_game(game_id) result.update(game_stats) result['teams'] = self._fixup_team_dict(teams) game_mod = game_stats["featured_mod"] if game_mod is None: featured_mods = {} else: featured_mods = await self._database.get_mod_versions(game_mod) result['featured_mod_versions'] = featured_mods return result
def get(self, game_id): rpath = self._replay_path(game_id) os.makedirs(rpath, exist_ok=True) rfile = os.path.join(rpath, f"{str(game_id)}.fafreplay") if os.path.exists(rfile): raise BookkeepingError(f"Replay file {rfile} already exists") open(rfile, 'a').close() # Touch file return rfile
async def execute(self, query, params=[]): try: async with self._lock: async with self._conn.cursor(aiomysql.DictCursor) as cur: await cur.execute(query, *params) return await cur.fetchall() except (DatabaseError, RuntimeError) as e: raise BookkeepingError(f"Failed to execute query: {e}")
def _write_replay(self, rfile, info, data): try: rfile.write(json.dumps(info).encode('UTF-8')) rfile.write(b"\n") data = struct.pack("i", len(data)) + zlib.compress(data) data = base64.b64encode(data) rfile.write(data) except UnicodeEncodeError: raise BookkeepingError("Unicode encoding error")
def _write_replay(self, rfile, info, data): try: rfile.write(json.dumps(info).encode('UTF-8')) rfile.write(b"\n") data = struct.pack(">i", len(data)) + zlib.compress(data) data = base64.b64encode(data) rfile.write(data) # json should always produce ascii, but just in case... except UnicodeEncodeError: raise BookkeepingError("Unicode encoding error")
async def get_teams_in_game(self, game_id): query = """ SELECT `login`.`login` AS login, `game_player_stats`.`team` AS team FROM `game_stats` INNER JOIN `game_player_stats` ON `game_player_stats`.`gameId` = `game_stats`.`id` INNER JOIN `login` ON `login`.id = `game_player_stats`.`playerId` WHERE `game_stats`.`id` = %s AND `game_player_stats`.`AI` = 0 """ players = await self._db.execute(query, (game_id,)) if not players: raise BookkeepingError("No game players found") teams = {} for player in players: teams.setdefault(player['team'], []).append(player['login']) return teams
async def get_game_stats(self, game_id): """ Gets the game information. """ query = """ SELECT `game_stats`.`startTime` AS start_time, `game_stats`.`endTime` AS end_time, `game_stats`.`gameType` AS game_type, `login`.`login` AS host, `game_stats`.`gameName` AS game_name, `game_featuredMods`.`gamemod` AS game_mod, `table_map`.`filename` AS file_name FROM `game_stats` LEFT JOIN `table_map` ON `game_stats`.`mapId` = `table_map`.`id` LEFT JOIN `login` ON `login`.id = `game_stats`.`host` LEFT JOIN `game_featuredMods` ON `game_stats`.`gameMod` = `game_featuredMods`.`id` WHERE `game_stats`.`id` = %s """ player_query = """ SELECT COUNT(*) FROM `game_player_stats` WHERE `game_player_stats`.`gameId` = %s """ logger.debug(f"Performing query: {query}") game_stats = await self._db.execute(query, (game_id, )) logger.debug(f"Performing query: {player_query}") player_count = await self._db.execute(player_query, (game_id, )) player_count = player_count[0]['COUNT(*)'] if not game_stats: raise BookkeepingError(f"No stats found for game {game_id}") start_time = game_stats[0]['start_time'].timestamp() # 'mapname' is a filename on the content server containing the map mapname = game_stats[0]['file_name'] if mapname is None: # Legacy replay server is forgiving like this. We replicate its # behaviour. logger.warning(f"Map missing for game {game_id}! Saving anyway.") mapname = "None" else: mapname = os.path.splitext(os.path.basename(mapname))[0] # We might end a replay before end_time is set in the db! end_time = game_stats[0]['end_time'] if end_time is None: end_time = time.time() else: end_time = end_time.timestamp() return { 'featured_mod': game_stats[0]['game_mod'], 'game_type': game_stats[0]['game_type'], 'recorder': game_stats[0]['host'], 'host': game_stats[0]['host'], 'launched_at': start_time, 'game_end': end_time, 'title': game_stats[0]['game_name'], 'mapname': mapname, 'num_players': player_count }