181 lines
5.7 KiB
Python
181 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Game state management for QLPyCon
|
|
Tracks server info, players, and teams
|
|
"""
|
|
|
|
import re
|
|
import logging
|
|
from config import TEAM_MODES, TEAM_MAP, MAX_RECENT_EVENTS
|
|
|
|
logger = logging.getLogger('state')
|
|
|
|
|
|
class ServerInfo:
|
|
"""Tracks current server information"""
|
|
|
|
def __init__(self):
|
|
self.hostname = 'Unknown'
|
|
self.map = 'Unknown'
|
|
self.gametype = 'Unknown'
|
|
self.timelimit = '0'
|
|
self.fraglimit = '0'
|
|
self.roundlimit = '0'
|
|
self.capturelimit = '0'
|
|
self.maxclients = '0'
|
|
self.curclients = '0'
|
|
self.red_score = 0
|
|
self.blue_score = 0
|
|
self.players = []
|
|
self.last_update = 0
|
|
|
|
def is_team_mode(self):
|
|
"""Check if current gametype is a team mode"""
|
|
return self.gametype in TEAM_MODES
|
|
|
|
def update_from_cvar(self, cvar_name, value):
|
|
"""Update server info from a cvar response"""
|
|
# Normalize cvar name to lowercase for case-insensitive matching
|
|
cvar_lower = cvar_name.lower()
|
|
|
|
mapping = {
|
|
'qlx_serverbrandname': 'hostname',
|
|
'g_factorytitle': 'gametype',
|
|
'mapname': 'map',
|
|
'timelimit': 'timelimit',
|
|
'fraglimit': 'fraglimit',
|
|
'roundlimit': 'roundlimit',
|
|
'capturelimit': 'capturelimit',
|
|
'sv_maxclients': 'maxclients'
|
|
}
|
|
|
|
attr = mapping.get(cvar_lower)
|
|
if attr:
|
|
# Only strip color codes for non-hostname fields
|
|
if attr != 'hostname':
|
|
value = re.sub(r'\^\d', '', value)
|
|
setattr(self, attr, value)
|
|
logger.info(f'Updated {attr}: {value}')
|
|
return True
|
|
return False
|
|
|
|
|
|
class PlayerTracker:
|
|
"""Tracks player teams and information"""
|
|
|
|
def __init__(self, server_info):
|
|
self.server_info = server_info
|
|
self.player_teams = {}
|
|
|
|
def update_team(self, name, team):
|
|
"""Update player team. Team can be int or string"""
|
|
if not self.server_info.is_team_mode():
|
|
return
|
|
|
|
# Convert numeric team to string
|
|
if isinstance(team, int):
|
|
team = TEAM_MAP.get(team, 'FREE')
|
|
|
|
if team not in ['RED', 'BLUE', 'FREE', 'SPECTATOR']:
|
|
team = 'FREE'
|
|
|
|
# Store both original name and color-stripped version
|
|
self.player_teams[name] = team
|
|
clean_name = re.sub(r'\^\d', '', name)
|
|
if clean_name != name:
|
|
self.player_teams[clean_name] = team
|
|
|
|
logger.debug(f'Updated team for {name} (clean: {clean_name}): {team}')
|
|
|
|
def get_team(self, name):
|
|
"""Get player's team"""
|
|
return self.player_teams.get(name)
|
|
|
|
def add_player(self, name, score='0', ping='0'):
|
|
"""Add player to server's player list if not exists"""
|
|
if not any(p['name'] == name for p in self.server_info.players):
|
|
self.server_info.players.append({
|
|
'name': name,
|
|
'score': score,
|
|
'ping': ping
|
|
})
|
|
|
|
def get_players_by_team(self):
|
|
"""Get players organized by team"""
|
|
teams = {'RED': [], 'BLUE': [], 'SPECTATOR': [], 'FREE': []}
|
|
for player in self.server_info.players:
|
|
name = player['name']
|
|
team = self.player_teams.get(name, 'FREE')
|
|
if team not in teams:
|
|
team = 'FREE'
|
|
teams[team].append(name)
|
|
return teams
|
|
|
|
def remove_player(self, name):
|
|
"""Remove player from tracking"""
|
|
clean_name = re.sub(r'\^\d', '', name)
|
|
|
|
# Remove from player list (check both name and clean name)
|
|
self.server_info.players = [
|
|
p for p in self.server_info.players
|
|
if p['name'] != name and re.sub(r'\^\d', '', p['name']) != clean_name
|
|
]
|
|
|
|
# Remove from team tracking
|
|
if name in self.player_teams:
|
|
del self.player_teams[name]
|
|
if clean_name in self.player_teams and clean_name != name:
|
|
del self.player_teams[clean_name]
|
|
|
|
logger.debug(f'Removed player: {name} (clean: {clean_name})')
|
|
|
|
def rename_player(self, old_name, new_name):
|
|
"""Rename a player while maintaining their team"""
|
|
# Get current team
|
|
team = self.player_teams.get(old_name, 'SPECTATOR')
|
|
|
|
# Remove old entries
|
|
self.remove_player(old_name)
|
|
|
|
# Add with new name and team
|
|
if self.server_info.is_team_mode():
|
|
self.update_team(new_name, team)
|
|
self.add_player(new_name)
|
|
|
|
logger.debug(f'Renamed player: {old_name} -> {new_name}')
|
|
|
|
class EventDeduplicator:
|
|
"""Prevents duplicate kill/death events"""
|
|
|
|
def __init__(self):
|
|
self.recent_events = []
|
|
|
|
def is_duplicate(self, event_type, time_val, killer_name, victim_name):
|
|
"""Check if this kill/death event is a duplicate"""
|
|
if event_type not in ('PLAYER_DEATH', 'PLAYER_KILL'):
|
|
return False
|
|
|
|
signature = f"KILL:{time_val}:{killer_name}:{victim_name}"
|
|
|
|
if signature in self.recent_events:
|
|
logger.debug(f'Duplicate event: {signature}')
|
|
return True
|
|
|
|
# Add to recent events
|
|
self.recent_events.append(signature)
|
|
if len(self.recent_events) > MAX_RECENT_EVENTS:
|
|
self.recent_events.pop(0)
|
|
|
|
return False
|
|
|
|
|
|
class GameState:
|
|
"""Main game state container"""
|
|
|
|
def __init__(self):
|
|
self.server_info = ServerInfo()
|
|
self.player_tracker = PlayerTracker(self.server_info)
|
|
self.event_deduplicator = EventDeduplicator()
|
|
self.pending_background_commands = set()
|
|
self.status_buffer = []
|