Module server.player_service
Manages connected and authenticated players
Classes
class PlayerService (database: FAFDatabase)
-
Expand source code
@with_logger class PlayerService(Service): def __init__(self, database: FAFDatabase): self._db = database self._players = dict() # Static-ish data fields. self.uniqueid_exempt = {} self._dirty_players = set() async def initialize(self) -> None: await self.update_data() self._update_cron = aiocron.crontab( "*/10 * * * *", func=self.update_data ) def __len__(self): return len(self._players) def __iter__(self): return self._players.values().__iter__() def __getitem__(self, player_id: int) -> Optional[Player]: return self._players.get(player_id) def __setitem__(self, player_id: int, player: Player): self._players[player_id] = player metrics.players_online.set(len(self._players)) @property def all_players(self) -> ValuesView[Player]: return self._players.values() def mark_dirty(self, player: Player): self._dirty_players.add(player) def pop_dirty_players(self) -> set[Player]: dirty_players = self._dirty_players self._dirty_players = set() return dirty_players async def fetch_player_data(self, player): async with self._db.acquire() as conn: result = await conn.execute( select(user_group.c.technical_name) .select_from(user_group_assignment.join(user_group)) .where(user_group_assignment.c.user_id == player.id) ) player.user_groups = {row.technical_name for row in result} sql = select( avatars_list.c.url, avatars_list.c.tooltip, clan.c.tag ).select_from( login .outerjoin(clan_membership) .outerjoin(clan) .outerjoin( avatars, onclause=and_( avatars.c.idUser == login.c.id, avatars.c.selected == 1 ) ) .outerjoin(avatars_list) ).where(login.c.id == player.id) # yapf: disable result = await conn.execute(sql) row = result.fetchone() if not row: self._logger.warning( "Did not find data for player with id %i", player.id ) return row = row._mapping player.clan = row.get(clan.c.tag) url, tooltip = ( row.get(avatars_list.c.url), row.get(avatars_list.c.tooltip) ) if url and tooltip: player.avatar = {"url": url, "tooltip": tooltip} await self._fetch_player_ratings(player, conn) async def _fetch_player_ratings(self, player, conn): sql = select( leaderboard_rating.c.mean, leaderboard_rating.c.deviation, leaderboard_rating.c.total_games, leaderboard.c.technical_name, ).select_from( leaderboard.join(leaderboard_rating) ).where( leaderboard_rating.c.login_id == player.id ) result = await conn.execute(sql) retrieved_ratings = { row.technical_name: ( (row.mean, row.deviation), row.total_games ) for row in result } for rating_type, (rating, total_games) in retrieved_ratings.items(): player.ratings[rating_type] = rating player.game_count[rating_type] = total_games def remove_player(self, player: Player): if player.id in self._players: # This signals that the player is now disconnected del player.lobby_connection del self._players[player.id] metrics.players_online.set(len(self._players)) self.mark_dirty(player) async def has_permission_role(self, player: Player, role_name: str) -> bool: async with self._db.acquire() as conn: result = await conn.execute( select(group_permission.c.id) .select_from( user_group_assignment .join(group_permission_assignment, onclause=( user_group_assignment.c.group_id == group_permission_assignment.c.group_id )) .join(group_permission) ) .where( and_( user_group_assignment.c.user_id == player.id, group_permission.c.technical_name == role_name ) ) ) row = result.fetchone() return row is not None def is_uniqueid_exempt(self, user_id: int) -> bool: return user_id in self.uniqueid_exempt def get_player(self, player_id: int) -> Optional[Player]: return self._players.get(player_id) def signal_player_rating_change( self, player_id: int, rating_type: str, new_rating: Rating ) -> None: player = self.get_player(player_id) if player is None: self._logger.debug( "Received rating change for player with id %i not in PlayerService.", player_id ) return self._logger.debug( "Received rating change for player %s.", player ) player.ratings[rating_type] = new_rating player.game_count[rating_type] += 1 self.mark_dirty(player) async def update_data(self): """ Update rarely-changing data, such as the admin list and the list of users exempt from the uniqueid check. """ async with self._db.acquire() as conn: # UniqueID-exempt users. result = await conn.execute( "SELECT `user_id` FROM uniqueid_exempt" ) self.uniqueid_exempt = frozenset(map(lambda x: x[0], result)) async def kick_idle_players(self): for fut in asyncio.as_completed([ player.lobby_connection.abort("Graceful shutdown.") for player in self.all_players if player.state == PlayerState.IDLE if player.lobby_connection is not None ]): try: await fut except Exception: self._logger.debug( "Error while aborting connection", exc_info=True ) def on_connection_lost(self, conn: "LobbyConnection") -> None: if not conn.player: return self.remove_player(conn.player) self._logger.debug( "Removed player %d, %s, %d", conn.player.id, conn.player.login, conn.session ) async def graceful_shutdown(self): if config.SHUTDOWN_KICK_IDLE_PLAYERS: self._kick_idle_task = at_interval(1, self.kick_idle_players)
All services should inherit from this class.
Services are singleton objects which manage some server task.
Ancestors
Instance variables
prop all_players : ValuesView[Player]
-
Expand source code
@property def all_players(self) -> ValuesView[Player]: return self._players.values()
Methods
async def fetch_player_data(self, player)
-
Expand source code
async def fetch_player_data(self, player): async with self._db.acquire() as conn: result = await conn.execute( select(user_group.c.technical_name) .select_from(user_group_assignment.join(user_group)) .where(user_group_assignment.c.user_id == player.id) ) player.user_groups = {row.technical_name for row in result} sql = select( avatars_list.c.url, avatars_list.c.tooltip, clan.c.tag ).select_from( login .outerjoin(clan_membership) .outerjoin(clan) .outerjoin( avatars, onclause=and_( avatars.c.idUser == login.c.id, avatars.c.selected == 1 ) ) .outerjoin(avatars_list) ).where(login.c.id == player.id) # yapf: disable result = await conn.execute(sql) row = result.fetchone() if not row: self._logger.warning( "Did not find data for player with id %i", player.id ) return row = row._mapping player.clan = row.get(clan.c.tag) url, tooltip = ( row.get(avatars_list.c.url), row.get(avatars_list.c.tooltip) ) if url and tooltip: player.avatar = {"url": url, "tooltip": tooltip} await self._fetch_player_ratings(player, conn)
def get_player(self, player_id: int) ‑> Player | None
-
Expand source code
def get_player(self, player_id: int) -> Optional[Player]: return self._players.get(player_id)
async def has_permission_role(self,
player: Player,
role_name: str) ‑> bool-
Expand source code
async def has_permission_role(self, player: Player, role_name: str) -> bool: async with self._db.acquire() as conn: result = await conn.execute( select(group_permission.c.id) .select_from( user_group_assignment .join(group_permission_assignment, onclause=( user_group_assignment.c.group_id == group_permission_assignment.c.group_id )) .join(group_permission) ) .where( and_( user_group_assignment.c.user_id == player.id, group_permission.c.technical_name == role_name ) ) ) row = result.fetchone() return row is not None
def is_uniqueid_exempt(self, user_id: int) ‑> bool
-
Expand source code
def is_uniqueid_exempt(self, user_id: int) -> bool: return user_id in self.uniqueid_exempt
async def kick_idle_players(self)
-
Expand source code
async def kick_idle_players(self): for fut in asyncio.as_completed([ player.lobby_connection.abort("Graceful shutdown.") for player in self.all_players if player.state == PlayerState.IDLE if player.lobby_connection is not None ]): try: await fut except Exception: self._logger.debug( "Error while aborting connection", exc_info=True )
def mark_dirty(self,
player: Player)-
Expand source code
def mark_dirty(self, player: Player): self._dirty_players.add(player)
def pop_dirty_players(self) ‑> set[Player]
-
Expand source code
def pop_dirty_players(self) -> set[Player]: dirty_players = self._dirty_players self._dirty_players = set() return dirty_players
def remove_player(self,
player: Player)-
Expand source code
def remove_player(self, player: Player): if player.id in self._players: # This signals that the player is now disconnected del player.lobby_connection del self._players[player.id] metrics.players_online.set(len(self._players)) self.mark_dirty(player)
def signal_player_rating_change(self, player_id: int, rating_type: str, new_rating: trueskill.Rating) ‑> None
-
Expand source code
def signal_player_rating_change( self, player_id: int, rating_type: str, new_rating: Rating ) -> None: player = self.get_player(player_id) if player is None: self._logger.debug( "Received rating change for player with id %i not in PlayerService.", player_id ) return self._logger.debug( "Received rating change for player %s.", player ) player.ratings[rating_type] = new_rating player.game_count[rating_type] += 1 self.mark_dirty(player)
async def update_data(self)
-
Expand source code
async def update_data(self): """ Update rarely-changing data, such as the admin list and the list of users exempt from the uniqueid check. """ async with self._db.acquire() as conn: # UniqueID-exempt users. result = await conn.execute( "SELECT `user_id` FROM uniqueid_exempt" ) self.uniqueid_exempt = frozenset(map(lambda x: x[0], result))
Update rarely-changing data, such as the admin list and the list of users exempt from the uniqueid check.
Inherited members