qlpycon/formatter.py
2025-12-23 09:37:42 +01:00

203 lines
7.0 KiB
Python

#!/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}{player_part} ^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}{player_part}^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}{original_parts[0]}:^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}{player_name} ^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}{player_name} ^7killed the {colored_powerup} ^7carrier!\n"
return None