import asyncio
import datetime
import logging
from peewee import DoesNotExist
from pyplanet.apps.core.maniaplanet.models import Player
from pyplanet.conf import settings
from pyplanet.contrib import CoreContrib
from pyplanet.contrib.player.exceptions import PlayerNotFound
from pyplanet.contrib.setting.core_settings import performance_mode
from pyplanet.core.exceptions import ImproperlyConfigured
from pyplanet.core.signals import pyplanet_performance_mode_begin, pyplanet_performance_mode_end
from pyplanet.core.storage.exceptions import StorageException
from pyplanet.utils.zone import parse_path
logger = logging.getLogger(__name__)
[docs]class PlayerManager(CoreContrib):
"""
Player Manager.
You can access this class in your app with:
.. code-block:: python
self.instance.player_manager
With the manager you can get several useful information about the players on the server. See all the properties and methods
below for more information.
.. warning::
Don't initiate this class yourself.
"""
def __init__(self, instance):
"""
Initiate, should only be done from the core instance.
:param instance: Instance.
:type instance: pyplanet.core.instance.Instance
"""
self._instance = instance
self._performance_mode = False
# self.lock = asyncio.Lock()
# Online contains all currently online players.
self._online = set()
self._online_logins = set()
# Counters.
self._counter_lock = asyncio.Lock()
self._total_count = 0
self._players_count = 0
self._spectators_count = 0
@property
def performance_mode(self):
return self._performance_mode
@performance_mode.setter
def performance_mode(self, new_value):
if self._performance_mode != new_value:
if new_value:
asyncio.ensure_future(
pyplanet_performance_mode_begin.send_robust(source=dict(
old_value=self._performance_mode, new_value=new_value
))
)
else:
asyncio.ensure_future(
pyplanet_performance_mode_end.send_robust(source=dict(
old_value=self._performance_mode, new_value=new_value
))
)
self._performance_mode = new_value
[docs] async def on_start(self):
"""
Handle startup, just before the apps will start. We will throw connects for the players so we know that the
current playing players are also initiated correctly!
"""
player_list = await self._instance.gbx('GetPlayerList', -1, 0)
await asyncio.gather(*[self.handle_connect(player['Login']) for player in player_list])
# Load and activate blacklist.
try:
await self.load_blacklist()
except:
pass # Ignore any exception thrown
self._instance.signals.listen('maniaplanet:loading_map_end', self.map_loaded)
[docs] async def map_loaded(self, *args, **kwargs):
"""
Reindex the current number of players and spectators.
:param args:
:param kwargs:
:return:
"""
# Update player and spectator counters.
player_list = await self._instance.gbx('GetPlayerList', -1, 0)
total = 0
specs = 0
players = 0
for player in player_list:
if self._instance.game.server_is_dedicated and self._instance.game.server_player_login == player['Login']:
continue
try:
info = await self._instance.gbx('GetDetailedPlayerInfo', player['Login'])
except:
# Player has left during this time.
continue
total += 1
if info['IsSpectator']:
specs += 1
else:
players += 1
async with self._counter_lock:
self._total_count = total
self._spectators_count = specs
self._players_count = players
[docs] async def handle_connect(self, login):
"""
Handle a connection of a player, this call is being called inside of the Glue of the callbacks.
:param login: Login, received from dedicated.
:return: Database Player instance.
:rtype: pyplanet.apps.core.maniaplanet.models.Player
"""
# Ignore if it's the server itself.
if self._instance.game.server_is_dedicated and self._instance.game.server_player_login == login:
return
try:
info = await self._instance.gbx('GetDetailedPlayerInfo', login)
except:
# Most likely too late, did disconnect directly after connecting..
# See #126
return
ip, _, port = info['IPAddress'].rpartition(':')
is_owner = login in settings.OWNERS[self._instance.process_name]
try:
player = await Player.get_by_login(login)
player.last_ip = ip
player.last_seen = datetime.datetime.now()
player.nickname = info['NickName']
if is_owner:
player.level = Player.LEVEL_MASTER
await player.save()
except DoesNotExist:
# Get details of player from dedicated.
player = await Player.create(
login=login,
nickname=info['NickName'],
last_ip=ip,
last_seen=datetime.datetime.now(),
level=Player.LEVEL_MASTER if is_owner else Player.LEVEL_PLAYER,
)
# Set the join time.
player.flow.joined_at = datetime.datetime.now()
# Update counter and state.
async with self._counter_lock:
player.flow.player_id = info['PlayerId']
player.flow.team_id = info['TeamId']
player.flow.is_spectator = bool(info['IsSpectator'])
player.flow.is_player = not bool(info['IsSpectator'])
player.flow.zone = parse_path(info['Path'])
self._total_count += 1
if player.flow.is_spectator:
self._spectators_count += 1
else:
self._players_count += 1
self._online.add(player)
self._online_logins.add(login)
self.performance_mode = len(self._online) >= await performance_mode.get_value()
return player
async def handle_info_change(self, player, is_spectator, is_temp_spectator, is_pure_spectator, target, team_id, **kwargs):
if not player:
return
if player not in self._online:
return
async with self._counter_lock:
if player.flow.is_spectator is True and not is_spectator:
self._spectators_count -= 1
self._players_count += 1
await self._instance.signals.get_signal('maniaplanet:player_enter_player_slot').send_robust(dict(
player=player,
), raw=True)
elif player.flow.is_player is True and is_spectator:
self._spectators_count += 1
self._players_count -= 1
await self._instance.signals.get_signal('maniaplanet:player_enter_spectator_slot').send_robust(dict(
player=player,
), raw=True)
# This is in case of desync happens. Not nice to fix, but currently one of the only options.
if self._players_count < 0:
self._players_count = 0
if self._spectators_count < 0:
self._spectators_count = 0
if self._total_count < 0:
self._total_count = 0
# Update flow state.
payload = kwargs.copy()
payload.update(dict(
is_spectator=is_spectator, is_temp_spectator=is_temp_spectator, is_pure_spectator=is_pure_spectator,
target=target, team_id=team_id
))
player.flow.update_state(**payload)
[docs] async def handle_disconnect(self, login):
"""
Handle a disconnection of a player, this call is being called inside of the Glue of the callbacks.
:param login: Login, received from dedicated.
:return: Database Player instance.
:rtype: pyplanet.apps.core.maniaplanet.models.Player
"""
try:
player = await Player.get_by_login(login=login)
except:
return
# Update counters.
async with self._counter_lock:
self._total_count -= 1
if player.flow.is_player:
self._players_count -= 1
else:
self._spectators_count -= 1
if player in self._online:
self._online.remove(player)
if login in self._online_logins:
self._online_logins.remove(login)
# Calculate the number of seconds on the server and update the total time on server.
if player.flow.joined_at:
time_on_server = datetime.datetime.now() - player.flow.joined_at
player.total_playtime += int(time_on_server.total_seconds())
try:
del Player.CACHE[login]
except:
pass
player.last_seen = datetime.datetime.now()
await player.save()
# Clear player/spec state.
player.flow.reset_state()
# Update performance mode status.
self.performance_mode = self._total_count >= await performance_mode.get_value()
return player
[docs] async def get_player(self, login=None, pk=None, lock=True):
"""
Get player by login or primary key.
:param login: Login.
:param pk: Primary Key identifier.
:param lock: Lock for a sec when receiving.
:return: Player or exception if not found
:rtype: pyplanet.apps.core.maniaplanet.models.Player
"""
try:
if login:
return await Player.get_by_login(login)
elif pk:
return await Player.get(pk=pk)
else:
raise PlayerNotFound('Player not found.')
except DoesNotExist:
if lock:
await asyncio.sleep(4)
return await self.get_player(login=login, pk=pk, lock=False)
else:
raise PlayerNotFound('Player not found.')
[docs] async def get_player_by_id(self, identifier):
"""
Get player object by ID.
:param identifier: Identifier.
:return: Player object or None
"""
for player in self._online:
if player.flow.player_id == identifier:
return player
return None
[docs] async def save_blacklist(self, filename=None):
"""
Save the current blacklisted players to file given or fetch from config.
:param filename: Give the filename of the blacklist, Leave empty to use the current loaded and configured one.
:type filename: str
:raise: pyplanet.core.exceptions.ImproperlyConfigured
:raise: pyplanet.core.storage.exceptions.StorageException
"""
setting = settings.BLACKLIST_FILE
if isinstance(setting, dict) and self._instance.process_name in setting:
setting = setting[self._instance.process_name]
if not isinstance(setting, str):
setting = None
if not filename and not setting:
raise ImproperlyConfigured(
'The setting \'BLACKLIST_FILE\' is not configured for this server! We can\'t save the Blacklist!'
)
if not filename:
filename = setting.format(server_login=self._instance.game.server_player_login)
try:
await self._instance.gbx('SaveBlackList', filename)
except Exception as e:
logging.exception(e)
raise StorageException('Can\'t save blacklist file to \'{}\'!'.format(filename)) from e
[docs] async def save_guestlist(self, filename=None):
"""
Save the current guestlisted players to file given or fetch from config.
:param filename: Give the filename of the guestlist, Leave empty to use the current loaded and configured one.
:type filename: str
:raise: pyplanet.core.exceptions.ImproperlyConfigured
:raise: pyplanet.core.storage.exceptions.StorageException
"""
setting = settings.GUESTLIST_FILE
if isinstance(setting, dict) and self._instance.process_name in setting:
setting = setting[self._instance.process_name]
if not isinstance(setting, str):
setting = None
if not filename and not setting:
raise ImproperlyConfigured(
'The setting \'GUESTLIST_FILE\' is not configured for this server! We can\'t save the Guestlist!'
)
if not filename:
filename = setting.format(server_login=self._instance.game.server_player_login)
try:
await self._instance.gbx('SaveGuestList', filename)
except Exception as e:
logging.exception(e)
raise StorageException('Can\'t save guestlist file to \'{}\'!'.format(filename)) from e
[docs] async def load_guestlist(self, filename=None):
"""
Load guestlist file.
:param filename: File to load or will get from settings.
:raise: pyplanet.core.exceptions.ImproperlyConfigured
:raise: pyplanet.core.storage.exceptions.StorageException
:return: Boolean if loaded.
"""
setting = settings.GUESTLIST_FILE
if isinstance(setting, dict) and self._instance.process_name in setting:
setting = setting[self._instance.process_name]
if not isinstance(setting, str):
setting = None
if not filename and not setting:
raise ImproperlyConfigured(
'The setting \'GUESTLIST_FILE\' is not configured for this server! We can\'t load the Guestlist!'
)
if not filename:
filename = setting.format(server_login=self._instance.game.server_player_login)
try:
self._instance.gbx('LoadGuestList', filename)
except Exception as e:
logging.exception(e)
raise StorageException('Can\'t load guestlist according the dedicated server, tried loading from \'{}\'!'.format(
filename
)) from e
[docs] async def load_blacklist(self, filename=None):
"""
Load blacklist file.
:param filename: File to load or will get from settings.
:raise: pyplanet.core.exceptions.ImproperlyConfigured
:raise: pyplanet.core.storage.exceptions.StorageException
:return: Boolean if loaded.
"""
setting = settings.BLACKLIST_FILE
if isinstance(setting, dict) and self._instance.process_name in setting:
setting = setting[self._instance.process_name]
if not isinstance(setting, str):
setting = None
if not filename and not setting:
raise ImproperlyConfigured(
'The setting \'BLACKLIST_FILE\' is not configured for this server! We can\'t load the Blacklist!'
)
if not filename:
filename = setting.format(server_login=self._instance.game.server_player_login)
try:
self._instance.gbx('LoadBlackList', filename)
except Exception as e:
logging.exception(e)
raise StorageException('Can\'t load blacklist according the dedicated server, tried loading from \'{}\'!'.format(
filename
)) from e
@property
def online(self):
"""
Online player list.
"""
return self._online.copy()
@property
def online_logins(self):
"""
Online player logins list.
"""
return self._online_logins.copy()
@property
def count_all(self):
"""
Get all player counts (players + spectators).
"""
return self._total_count
@property
def count_players(self):
"""
Get number of playing players.
"""
return self._players_count
@property
def count_spectators(self):
"""
Get number of spectating players.
"""
return self._spectators_count
@property
def max_players(self):
"""
Get maximum number of players.
"""
return self._instance.game.server_max_players
@property
def max_spectators(self):
"""
Get maximum number of spectators.
"""
return self._instance.game.server_max_specs