#!/usr/bin/env python3 """ Message formatting and colorization for QLPyCon Handles Quake color codes and team prefixes """ import re import time from config import TEAM_COLORS def strip_color_codes(text): """Remove Quake color codes (^N) from text""" return re.sub(r'\^\d', '', text) def get_team_prefix(player_name, player_tracker): """Get color-coded team prefix for a player""" if not player_tracker.server_info.is_team_mode(): return '' team = player_tracker.get_team(player_name) if not team: return '' return TEAM_COLORS.get(team, '') def should_add_timestamp(message): """Determine if a message should get a timestamp""" # Skip status command output skip_keywords = ['map:', 'num score', '---', 'bot', 'status'] if any(kw in message for kw in skip_keywords): # But allow "zmq RCON" lines (command echoes) if 'zmq RCON' not in message: return False # Skip very short messages or fragments stripped = message.strip() if len(stripped) <= 2: return False # Skip messages with leading spaces (status fragments) if message.startswith(' ') and len(stripped) < 50: return False # Skip pure numbers if stripped.isdigit(): return False # Skip short single words if len(stripped) < 20 and ' ' not in stripped: return False # Skip IP addresses (xxx.xxx.xxx.xxx or xxx.xxx.xxx.xxx:port) allowed_chars = set('0123456789.:') if stripped.count('.') == 3 and all(c in allowed_chars for c in stripped): return False # Skip messages starting with *** if message.startswith('***'): return False return True def format_message(message, add_timestamp=True): """ Format a message for display - Strips special characters - Adds timestamp if appropriate - Handles broadcast formatting """ # Clean up message message = message.replace("\\n", "") message = message.replace(chr(25), "") # Handle broadcast messages attributes = 0 if message[:10] == "broadcast:": message = message[11:] attributes = 1 # Bold # Handle print messages if message[:7] == "print \"": message = message[7:-2] + "\n" # Add timestamp if requested and appropriate if add_timestamp and should_add_timestamp(message): timestamp = time.strftime('%H:%M:%S') message = f"^3[^7{timestamp}^3]^7 {message}" return message, attributes def format_chat_message(message, player_tracker): """ Format chat messages with team prefixes and colors Handles both regular chat (Name: msg) and team chat ((Name): msg or (Name) (Location): msg) """ # Strip special character clean_msg = message.replace(chr(25), '') # Team chat with location: (PlayerName) (Location): message # Location can have nested parens like (Lower Floor (Near Yellow Armour)) if clean_msg.strip().startswith('(') and ')' in clean_msg: # Extract player name (first parenthetical) player_match = re.match(r'^(\([^)]+\))', clean_msg) if not player_match: return message player_part = player_match.group(1) rest = clean_msg[len(player_part):].lstrip() # Check for location (another parenthetical) if rest.startswith('('): # Count parens to handle nesting paren_count = 0 location_end = -1 for i, char in enumerate(rest): if char == '(': paren_count += 1 elif char == ')': paren_count -= 1 if paren_count == 0: location_end = i + 1 break # Check if location ends with colon if location_end > 0 and location_end < len(rest) and rest[location_end] == ':': location_part = rest[:location_end] message_part = rest[location_end + 1:] # Get team prefix name_match = re.match(r'\(([^)]+)\)', player_part) if name_match: player_name = strip_color_codes(name_match.group(1).strip()) team_prefix = get_team_prefix(player_name, player_tracker) location_clean = strip_color_codes(location_part) return f"{team_prefix}^0{player_part}^9 ^3{location_clean}^7:^5{message_part}" # Team chat without location: (PlayerName): message colon_match = re.match(r'^(\([^)]+\)):(\s*.*)', clean_msg) if colon_match: player_part = colon_match.group(1) + ':' message_part = colon_match.group(2) name_match = re.match(r'\(([^)]+)\)', player_part) if name_match: player_name = strip_color_codes(name_match.group(1).strip()) team_prefix = get_team_prefix(player_name, player_tracker) return f"{team_prefix}^0{player_part}^9^5{message_part}\n" # Regular chat: PlayerName: message parts = clean_msg.split(':', 1) if len(parts) == 2: player_name = strip_color_codes(parts[0].strip()) team_prefix = get_team_prefix(player_name, player_tracker) # Preserve original color-coded name original_parts = message.replace(chr(25), '').split(':', 1) if len(original_parts) == 2: return f"{team_prefix}^0{original_parts[0]}^9:^2{original_parts[1]}" 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 if message.startswith("broadcast:"): message = message[11:].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" # 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" return None