- Add pre-compiled regex pattern for color codes - Add SPECIAL_CHAR constant (chr(25)) for clarity - Move time import to module level in parser - Centralize color code stripping via strip_color_codes() - Make qlpycon.bash fully configurable (workdir, serverip) - Add validation checks for workdir and venv - Fix port numbers in help text (28960-28969)
212 lines
7.3 KiB
Python
212 lines
7.3 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, COLOR_CODE_PATTERN, SPECIAL_CHAR
|
|
|
|
|
|
def strip_color_codes(text):
|
|
"""Remove Quake color codes (^N) from text"""
|
|
return COLOR_CODE_PATTERN.sub('', 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(SPECIAL_CHAR, "")
|
|
|
|
# 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(SPECIAL_CHAR, '')
|
|
|
|
# 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"^8^5[TEAMSAY]^7^0 {team_prefix}^8{player_part}^0^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"^8^5[TEAMSAY]^7^0 {team_prefix}^8{player_part}^7^0:^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(SPECIAL_CHAR, '').split(':', 1)
|
|
if len(original_parts) == 2:
|
|
return f"^8^2[SAY]^7^0 {team_prefix}^8{original_parts[0]}^0^7:^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
|
|
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')
|
|
timestamp = time.strftime('%H:%M:%S')
|
|
return f"^3[^7{timestamp}^3] ^8^5[POWERUP]^7^0 {team_prefix}^8{player_name}^0 ^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')
|
|
timestamp = time.strftime('%H:%M:%S')
|
|
return f"^3[^7{timestamp}^3] ^8^5[POWERUP]^7^0 {team_prefix}^8{player_name}^0 ^7killed the {colored_powerup} ^7carrier!\n"
|
|
|
|
return None
|