Optimize patterns and improve script configurability

- 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)
This commit is contained in:
pfl
2026-01-09 13:51:53 +01:00
parent 224782467d
commit f75adac97a
5 changed files with 42 additions and 25 deletions

View File

@ -3,8 +3,14 @@
Configuration and constants for QLPyCon
"""
import re
VERSION = "0.8.1"
# Pattern matching
COLOR_CODE_PATTERN = re.compile(r'\^\d') # Quake color codes (^0-^9)
SPECIAL_CHAR = chr(25) # ASCII EM (End of Medium) - used in Quake messages
# Network defaults
DEFAULT_HOST = 'tcp://127.0.0.1:27961'
POLL_TIMEOUT = 100

View File

@ -6,12 +6,12 @@ Handles Quake color codes and team prefixes
import re
import time
from config import TEAM_COLORS
from config import TEAM_COLORS, COLOR_CODE_PATTERN, SPECIAL_CHAR
def strip_color_codes(text):
"""Remove Quake color codes (^N) from text"""
return re.sub(r'\^\d', '', text)
return COLOR_CODE_PATTERN.sub('', text)
def get_team_prefix(player_name, player_tracker):
@ -73,7 +73,7 @@ def format_message(message, add_timestamp=True):
"""
# Clean up message
message = message.replace("\\n", "")
message = message.replace(chr(25), "")
message = message.replace(SPECIAL_CHAR, "")
# Handle broadcast messages
attributes = 0
@ -99,7 +99,7 @@ def format_chat_message(message, player_tracker):
Handles both regular chat (Name: msg) and team chat ((Name): msg or (Name) (Location): msg)
"""
# Strip special character
clean_msg = message.replace(chr(25), '')
clean_msg = message.replace(SPECIAL_CHAR, '')
# Team chat with location: (PlayerName) (Location): message
# Location can have nested parens like (Lower Floor (Near Yellow Armour))
@ -158,7 +158,7 @@ def format_chat_message(message, player_tracker):
team_prefix = get_team_prefix(player_name, player_tracker)
# Preserve original color-coded name
original_parts = message.replace(chr(25), '').split(':', 1)
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]}"

View File

@ -6,6 +6,7 @@ Parses events from Quake Live stats stream
import json
import logging
import time
from config import WEAPON_NAMES, WEAPON_KILL_NAMES, DEATH_MESSAGES
from formatter import get_team_prefix, strip_color_codes
@ -158,7 +159,6 @@ class EventParser:
# Mark as dead
if not data.get('WARMUP', False):
import time
self.game_state.server_info.dead_players[victim_name] = time.time()
victim_prefix = get_team_prefix(victim_name, self.game_state.player_tracker)
@ -255,7 +255,6 @@ class EventParser:
self.game_state.server_info.blue_rounds += 1
logger.info(f"Round {round_num}: BLUE wins (RED: {self.game_state.server_info.red_rounds}, BLUE: {self.game_state.server_info.blue_rounds})")
import time
self.game_state.server_info.round_end_time = time.time()
return None # Don't display in chat

View File

@ -1,21 +1,33 @@
#!/usr/bin/env bash
#
# Helper script to connect to different Quake Live servers
# Set QLPYCON_PASSWORD environment variable or create ~/.qlpycon.conf
# Required: QLPYCON_PASSWORD
# Optional: QLPYCON_WORKDIR (default: /home/marc/git/qlpycon.git)
# QLPYCON_SERVERIP (default: 10.13.12.93)
workdir="${QLPYCON_WORKDIR:-/home/marc/git/qlpycon}"
# Required
password="${QLPYCON_PASSWORD:-}"
serverip="10.13.12.93"
workdir="${QLPYCON_WORKDIR:-/home/marc/git/qlpycon.git}"
serverip="${QLPYCON_SERVERIP:-10.13.12.93}"
# Check if password is set
# Validate
if [ -z "$password" ]; then
echo "Error: QLPYCON_PASSWORD environment variable not set"
echo "Set it with: export QLPYCON_PASSWORD='your_password'"
echo "Or create ~/.qlpycon.conf with [connection] section"
echo "Error: QLPYCON_PASSWORD not set"
echo "Set: export QLPYCON_PASSWORD='your_password'"
exit 1
fi
cd "$workdir"
if [ ! -d "$workdir" ]; then
echo "Error: Working directory does not exist: $workdir"
exit 1
fi
if [ ! -d "$workdir/venv" ]; then
echo "Error: venv not found in $workdir"
exit 1
fi
cd "$workdir" || exit 1
source venv/bin/activate
if [ "$1" == "ffa" ]; then
@ -49,6 +61,6 @@ elif [ "$1" == "leduel" ]; then
python3 main.py --host tcp://"$serverip":28969 --password "$password"
else
echo "[ ffa (27960), duel (27961), rcpma (27962), iffa (27963), ca (27964), ctf (27965), rcvq3 (27966), test (27967), ft (27968), leduel (27969) ]"
echo "[ ffa (28960), duel (28961), rcpma (28962), iffa (28963), ca (28964), ctf (28965), rcvq3 (28966), test (28967), ft (28968), leduel (28969) ]"
fi

View File

@ -4,9 +4,9 @@ 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
from formatter import strip_color_codes
logger = logging.getLogger('state')
@ -65,7 +65,7 @@ class ServerInfo:
if attr:
# Only strip color codes for non-hostname fields
if attr != 'hostname':
value = re.sub(r'\^\d', '', value)
value = strip_color_codes(value)
setattr(self, attr, value)
logger.info(f'Updated {attr}: {value}')
return True
@ -90,7 +90,7 @@ class PlayerTracker:
# Store both original name and color-stripped version
self.player_teams[name] = team
clean_name = re.sub(r'\^\d', '', name)
clean_name = strip_color_codes(name)
if clean_name != name:
self.player_teams[clean_name] = team
@ -122,7 +122,7 @@ class PlayerTracker:
def remove_player(self, name):
"""Remove player from tracking"""
clean_name = re.sub(r'\^\d', '', name)
clean_name = strip_color_codes(name)
# Try to remove by exact name first
removed = self.server_info.players.pop(name, None)
@ -130,7 +130,7 @@ class PlayerTracker:
# If not found, try to find by clean name
if not removed:
for player_name in list(self.server_info.players.keys()):
if re.sub(r'\^\d', '', player_name) == clean_name:
if strip_color_codes(player_name) == clean_name:
removed = self.server_info.players.pop(player_name)
logger.info(f'Removed player: {player_name} (matched clean name: {clean_name})')
break
@ -146,7 +146,7 @@ class PlayerTracker:
def rename_player(self, old_name, new_name):
"""Rename a player while maintaining their team and score"""
old_clean = re.sub(r'\^\d', '', old_name)
old_clean = strip_color_codes(old_name)
# Get current team (try both names)
team = self.player_teams.get(old_name) or self.player_teams.get(old_clean, 'SPECTATOR')
@ -157,7 +157,7 @@ class PlayerTracker:
# If not found by exact name, try clean name
if not player_data:
for player_name in list(self.server_info.players.keys()):
if re.sub(r'\^\d', '', player_name) == old_clean:
if strip_color_codes(player_name) == old_clean:
player_data = self.server_info.players.pop(player_name)
break
@ -184,9 +184,9 @@ class PlayerTracker:
return
# Fallback: search by clean name (rare case)
clean_name = re.sub(r'\^\d', '', name)
clean_name = strip_color_codes(name)
for player_name, player_data in self.server_info.players.items():
if re.sub(r'\^\d', '', player_name) == clean_name:
if strip_color_codes(player_name) == clean_name:
current_score = int(player_data.get('score', 0))
player_data['score'] = str(current_score + delta)
logger.debug(f"Score update: {player_name} {delta:+d} -> {player_data['score']}")