diff --git a/formatter.py b/formatter.py index ca109b5..044f053 100644 --- a/formatter.py +++ b/formatter.py @@ -164,39 +164,48 @@ def format_chat_message(message, player_tracker): return message - def format_powerup_message(message, player_tracker): """ Format powerup pickup and carrier kill messages Returns formatted message or None if not a powerup message """ from config import POWERUP_COLORS - + import time + if message.startswith("broadcast:"): message = message[11:].strip() - + + # Strip print " wrapper + if message.startswith('print "'): + message = message[7:] + if message.endswith('"'): + message = message[:-1] + message = message.strip() + # Powerup pickup: "PlayerName got the PowerupName!" pickup_match = re.match(r'^(.+?)\s+got the\s+(.+?)!', message) if pickup_match: player_name = pickup_match.group(1).strip() powerup_name = pickup_match.group(2).strip() - + player_clean = strip_color_codes(player_name) team_prefix = get_team_prefix(player_clean, player_tracker) - + colored_powerup = POWERUP_COLORS.get(powerup_name, f'^6{powerup_name}^7') - return f"{team_prefix}^0{player_name}^9 ^7got the {colored_powerup}!\n" - + timestamp = time.strftime('%H:%M:%S') + return f"^3[^7{timestamp}^3]^7 {team_prefix}^0{player_name}^9 ^7got the {colored_powerup}!\n" + # Powerup carrier kill: "PlayerName killed the PowerupName carrier!" carrier_match = re.match(r'^(.+?)\s+killed the\s+(.+?)\s+carrier!', message) if carrier_match: player_name = carrier_match.group(1).strip() powerup_name = carrier_match.group(2).strip() - + player_clean = strip_color_codes(player_name) team_prefix = get_team_prefix(player_clean, player_tracker) - + colored_powerup = POWERUP_COLORS.get(powerup_name, f'^6{powerup_name}^7') - return f"{team_prefix}^0{player_name}^9 ^7killed the {colored_powerup} ^7carrier!\n" - + timestamp = time.strftime('%H:%M:%S') + return f"^3[^7{timestamp}^3]^7 {team_prefix}^0{player_name}^9 ^7killed the {colored_powerup} ^7carrier!\n" + return None diff --git a/main.py b/main.py index f1115de..b774d6e 100644 --- a/main.py +++ b/main.py @@ -98,56 +98,120 @@ def parse_player_events(message, game_state, ui): Returns True if message should be suppressed """ try: + msg = message + + # Strip broadcast: print "..." wrapper with regex + broadcast_match = re.match(r'^broadcast:\s*print\s*"(.+?)(?:\\n)?"\s*$', msg) + if broadcast_match: + msg = broadcast_match.group(1) + + # Strip timestamp: [HH:MM:SS] or ^3[^7HH:MM:SS^3]^7 + msg = re.sub(r'\^\d\[\^\d[0-9:]+\^\d\]\^\d\s*', '', msg) + msg = re.sub(r'\[[0-9:]+\]\s*', '', msg) + msg = msg.strip() + + if not msg: + return False + + logger.debug(f'parse_player_events: {repr(msg)}') + # Strip color codes for matching from formatter import strip_color_codes - clean_msg = strip_color_codes(message) + clean_msg = strip_color_codes(msg) - # Match connects: "NAME connected" - connect_match = re.match(r'^(.+?)\s+connected(?:\s+with Steam ID)?', clean_msg) + # Match connects: "NAME connected" or "NAME connected with Steam ID" + connect_match = re.match(r'^(.+?)\s+connected', clean_msg) if connect_match: - player_name = message.split(' connected')[0].strip() # Keep color codes + player_name_match = re.match(r'^(.+?)\s+connected', msg) + player_name = player_name_match.group(1).strip() if player_name_match else connect_match.group(1).strip() + player_name = re.sub(r'\^\d+$', '', player_name) + + logger.info(f'CONNECT: {repr(player_name)}') if game_state.server_info.is_team_mode(): game_state.player_tracker.update_team(player_name, 'SPECTATOR') game_state.player_tracker.add_player(player_name) ui.update_server_info(game_state) - logger.debug(f'Player connected: {player_name}') - return False + + # Only print if this is NOT the Steam ID line + if 'Steam ID' not in message: + timestamp = time.strftime('%H:%M:%S') + ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^2connected^7\n") + + return True - # Match disconnects: "NAME disconnected", "NAME was kicked" - disconnect_patterns = [ - r'^(.+?)\s+disconnected', - r'^(.+?)\s+was kicked', - ] + # Regular disconnect + disconnect_match = re.match(r'^(.+?)\s+disconnected', clean_msg) + if disconnect_match: + original_match = re.match(r'^(.+?)\s+disconnected', msg) + player_name = original_match.group(1).strip() if original_match else disconnect_match.group(1).strip() + player_name = re.sub(r'\^\d+$', '', player_name) + player_name = re.sub(r'^\^\d+', '', player_name) + + logger.info(f'DISCONNECT: {repr(player_name)}') + game_state.player_tracker.remove_player(player_name) + ui.update_server_info(game_state) + + timestamp = time.strftime('%H:%M:%S') + ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^1disconnected^7\n") + return True - for pattern in disconnect_patterns: - match = re.match(pattern, clean_msg) - if match: - player_name_clean = match.group(1).strip() - # Try to find the original name with color codes - original_match = re.match(pattern, message) - player_name = original_match.group(1).strip() if original_match else player_name_clean + # Kick + kick_match = re.match(r'^(.+?)\s+was kicked', clean_msg) + if kick_match: + original_match = re.match(r'^(.+?)\s+was kicked', msg) + player_name = original_match.group(1).strip() if original_match else kick_match.group(1).strip() + player_name = re.sub(r'\^\d+$', '', player_name) + player_name = re.sub(r'^\^\d+', '', player_name) + + logger.info(f'KICK: {repr(player_name)}') + game_state.player_tracker.remove_player(player_name) + ui.update_server_info(game_state) + + timestamp = time.strftime('%H:%M:%S') + ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^1was kicked^7\n") + return True - game_state.player_tracker.remove_player(player_name) - game_state.player_tracker.remove_player(player_name_clean) # Try both - ui.update_server_info(game_state) - logger.debug(f'Player disconnected: {player_name}') - return False + # Inactivity + inactivity_match = re.match(r'^(.+?)\s+Dropped due to inactivity', clean_msg) + if inactivity_match: + original_match = re.match(r'^(.+?)\s+Dropped due to inactivity', msg) + player_name = original_match.group(1).strip() if original_match else inactivity_match.group(1).strip() + player_name = re.sub(r'\^\d+$', '', player_name) + player_name = re.sub(r'^\^\d+', '', player_name) + + logger.info(f'INACTIVITY DROP: {repr(player_name)}') + game_state.player_tracker.remove_player(player_name) + ui.update_server_info(game_state) + + timestamp = time.strftime('%H:%M:%S') + ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^3dropped due to inactivity^7\n") + return True # Match renames: "OldName renamed to NewName" rename_match = re.match(r'^(.+?)\s+renamed to\s+(.+?)$', clean_msg) if rename_match: - old_name_clean = rename_match.group(1).strip() - new_name_clean = rename_match.group(2).strip() + # Extract from original message + original_match = re.match(r'^(.+?)\s+renamed to\s+(.+?)$', msg) + if original_match: + old_name = original_match.group(1).strip() + new_name = original_match.group(2).strip() + else: + old_name = rename_match.group(1).strip() + new_name = rename_match.group(2).strip() - # Get names with color codes - original_match = re.match(r'^(.+?)\s+renamed to\s+(.+?)$', message) - old_name = original_match.group(1).strip() if original_match else old_name_clean - new_name = original_match.group(2).strip() if original_match else new_name_clean + # Remove trailing color codes from both names + old_name = re.sub(r'\^\d+$', '', old_name) + new_name = re.sub(r'^\^\d+', '', new_name) # Remove leading ^7 from new name + new_name = re.sub(r'\^\d+$', '', new_name) # Remove trailing too + old_name = old_name.rstrip('\n\r') # Remove trailing newline + new_name = new_name.rstrip('\n\r') # Remove trailing newline + logger.info(f'RENAME: {repr(old_name)} -> {repr(new_name)}') game_state.player_tracker.rename_player(old_name, new_name) ui.update_server_info(game_state) - logger.debug(f'Player renamed: {old_name} -> {new_name}') - return False + timestamp = time.strftime('%H:%M:%S') + ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{old_name}^9 ^6renamed to^7 ^0{new_name}^9\n") + return True except Exception as e: logger.error(f'Error in parse_player_events: {e}') @@ -291,14 +355,33 @@ def main_loop(screen): logger.debug(f'Received message ({len(message)} bytes): {repr(message[:100])}') + # Check for player connect/disconnect/rename events + if parse_player_events(message, game_state, ui): + continue + + if '------- Game Initialization -------' in message or 'Game Initialization' in message: + logger.info('Game initialization detected - refreshing server info') + ui.print_message("^6New Game - Refreshing Server Info^7\n") + + rcon.send_command(b'qlx_serverBrandName') + rcon.send_command(b'g_factoryTitle') + rcon.send_command(b'mapname') + rcon.send_command(b'timelimit') + rcon.send_command(b'fraglimit') + rcon.send_command(b'roundlimit') + rcon.send_command(b'capturelimit') + rcon.send_command(b'sv_maxclients') + + # Clear player list since map changed + game_state.server_info.players = [] + game_state.player_tracker.player_teams = {} + ui.update_server_info(game_state) + # Try to parse as cvar response if parse_cvar_response(message, game_state, ui): logger.debug('Suppressed cvar response') continue - # Check for player connect/disconnect/rename events - parse_player_events(message, game_state, ui) - # Check for stats connection info port, password = handle_stats_connection(message, rcon, ui, game_state) if port: diff --git a/state.py b/state.py index d442da3..b74c777 100644 --- a/state.py +++ b/state.py @@ -93,12 +93,19 @@ class PlayerTracker: 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 - }) + clean_name = re.sub(r'\^\d', '', name) + + # Check if player already exists (by either name or clean name) + for existing in self.server_info.players: + existing_clean = re.sub(r'\^\d', '', existing['name']) + if existing['name'] == name or existing_clean == clean_name: + return # Already exists + + self.server_info.players.append({ + 'name': name, + 'score': score, + 'ping': ping + }) def get_players_by_team(self): """Get players organized by team""" @@ -115,34 +122,48 @@ class PlayerTracker: """Remove player from tracking""" clean_name = re.sub(r'\^\d', '', name) - # Remove from player list (check both name and clean name) + # Count before removal + before_count = len(self.server_info.players) + + # Remove from player list - check both original and clean names 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] + # Log if anything was actually removed + after_count = len(self.server_info.players) + if before_count != after_count: + logger.info(f'Removed player: {name} (clean: {clean_name}) - {before_count} -> {after_count}') + else: + logger.warning(f'Player not found for removal: {name} (clean: {clean_name})') - logger.debug(f'Removed player: {name} (clean: {clean_name})') + # Remove from team tracking - both versions + self.player_teams.pop(name, None) + self.player_teams.pop(clean_name, None) 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 + old_clean = re.sub(r'\^\d', '', old_name) + + # Get current team (try both names) + team = self.player_teams.get(old_name) or self.player_teams.get(old_clean, 'SPECTATOR') + + # Find and update player in server list + for player in self.server_info.players: + if player['name'] == old_name or re.sub(r'\^\d', '', player['name']) == old_clean: + player['name'] = new_name + break + + # Remove old team entries + self.player_teams.pop(old_name, None) + self.player_teams.pop(old_clean, None) + + # Add new team entries with color codes preserved 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}') + + logger.debug(f'Renamed player: {old_name} -> {new_name} (team: {team})') class EventDeduplicator: """Prevents duplicate kill/death events"""