diff --git a/config.py b/config.py index bd39f72..ea9847f 100644 --- a/config.py +++ b/config.py @@ -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 diff --git a/formatter.py b/formatter.py index 76db509..d3579f9 100644 --- a/formatter.py +++ b/formatter.py @@ -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]}" diff --git a/parser.py b/parser.py index c5a853f..8aaee05 100644 --- a/parser.py +++ b/parser.py @@ -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 diff --git a/qlpycon.bash b/qlpycon.bash index 2b2edb0..7e6a421 100755 --- a/qlpycon.bash +++ b/qlpycon.bash @@ -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 diff --git a/state.py b/state.py index 92bd81b..1d35fe6 100644 --- a/state.py +++ b/state.py @@ -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']}")