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:
@ -3,8 +3,14 @@
|
|||||||
Configuration and constants for QLPyCon
|
Configuration and constants for QLPyCon
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
VERSION = "0.8.1"
|
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
|
# Network defaults
|
||||||
DEFAULT_HOST = 'tcp://127.0.0.1:27961'
|
DEFAULT_HOST = 'tcp://127.0.0.1:27961'
|
||||||
POLL_TIMEOUT = 100
|
POLL_TIMEOUT = 100
|
||||||
|
|||||||
10
formatter.py
10
formatter.py
@ -6,12 +6,12 @@ Handles Quake color codes and team prefixes
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from config import TEAM_COLORS
|
from config import TEAM_COLORS, COLOR_CODE_PATTERN, SPECIAL_CHAR
|
||||||
|
|
||||||
|
|
||||||
def strip_color_codes(text):
|
def strip_color_codes(text):
|
||||||
"""Remove Quake color codes (^N) from 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):
|
def get_team_prefix(player_name, player_tracker):
|
||||||
@ -73,7 +73,7 @@ def format_message(message, add_timestamp=True):
|
|||||||
"""
|
"""
|
||||||
# Clean up message
|
# Clean up message
|
||||||
message = message.replace("\\n", "")
|
message = message.replace("\\n", "")
|
||||||
message = message.replace(chr(25), "")
|
message = message.replace(SPECIAL_CHAR, "")
|
||||||
|
|
||||||
# Handle broadcast messages
|
# Handle broadcast messages
|
||||||
attributes = 0
|
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)
|
Handles both regular chat (Name: msg) and team chat ((Name): msg or (Name) (Location): msg)
|
||||||
"""
|
"""
|
||||||
# Strip special character
|
# Strip special character
|
||||||
clean_msg = message.replace(chr(25), '')
|
clean_msg = message.replace(SPECIAL_CHAR, '')
|
||||||
|
|
||||||
# Team chat with location: (PlayerName) (Location): message
|
# Team chat with location: (PlayerName) (Location): message
|
||||||
# Location can have nested parens like (Lower Floor (Near Yellow Armour))
|
# 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)
|
team_prefix = get_team_prefix(player_name, player_tracker)
|
||||||
|
|
||||||
# Preserve original color-coded name
|
# 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:
|
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 f"^8^2[SAY]^7^0 {team_prefix}^8{original_parts[0]}^0^7:^2{original_parts[1]}"
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ Parses events from Quake Live stats stream
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from config import WEAPON_NAMES, WEAPON_KILL_NAMES, DEATH_MESSAGES
|
from config import WEAPON_NAMES, WEAPON_KILL_NAMES, DEATH_MESSAGES
|
||||||
from formatter import get_team_prefix, strip_color_codes
|
from formatter import get_team_prefix, strip_color_codes
|
||||||
|
|
||||||
@ -158,7 +159,6 @@ class EventParser:
|
|||||||
|
|
||||||
# Mark as dead
|
# Mark as dead
|
||||||
if not data.get('WARMUP', False):
|
if not data.get('WARMUP', False):
|
||||||
import time
|
|
||||||
self.game_state.server_info.dead_players[victim_name] = time.time()
|
self.game_state.server_info.dead_players[victim_name] = time.time()
|
||||||
|
|
||||||
victim_prefix = get_team_prefix(victim_name, self.game_state.player_tracker)
|
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
|
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})")
|
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()
|
self.game_state.server_info.round_end_time = time.time()
|
||||||
|
|
||||||
return None # Don't display in chat
|
return None # Don't display in chat
|
||||||
|
|||||||
30
qlpycon.bash
30
qlpycon.bash
@ -1,21 +1,33 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Helper script to connect to different Quake Live servers
|
# 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:-}"
|
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
|
if [ -z "$password" ]; then
|
||||||
echo "Error: QLPYCON_PASSWORD environment variable not set"
|
echo "Error: QLPYCON_PASSWORD not set"
|
||||||
echo "Set it with: export QLPYCON_PASSWORD='your_password'"
|
echo "Set: export QLPYCON_PASSWORD='your_password'"
|
||||||
echo "Or create ~/.qlpycon.conf with [connection] section"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
source venv/bin/activate
|
||||||
|
|
||||||
if [ "$1" == "ffa" ]; then
|
if [ "$1" == "ffa" ]; then
|
||||||
@ -49,6 +61,6 @@ elif [ "$1" == "leduel" ]; then
|
|||||||
python3 main.py --host tcp://"$serverip":28969 --password "$password"
|
python3 main.py --host tcp://"$serverip":28969 --password "$password"
|
||||||
|
|
||||||
else
|
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
|
fi
|
||||||
|
|||||||
18
state.py
18
state.py
@ -4,9 +4,9 @@ Game state management for QLPyCon
|
|||||||
Tracks server info, players, and teams
|
Tracks server info, players, and teams
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
from config import TEAM_MODES, TEAM_MAP, MAX_RECENT_EVENTS
|
from config import TEAM_MODES, TEAM_MAP, MAX_RECENT_EVENTS
|
||||||
|
from formatter import strip_color_codes
|
||||||
|
|
||||||
logger = logging.getLogger('state')
|
logger = logging.getLogger('state')
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ class ServerInfo:
|
|||||||
if attr:
|
if attr:
|
||||||
# Only strip color codes for non-hostname fields
|
# Only strip color codes for non-hostname fields
|
||||||
if attr != 'hostname':
|
if attr != 'hostname':
|
||||||
value = re.sub(r'\^\d', '', value)
|
value = strip_color_codes(value)
|
||||||
setattr(self, attr, value)
|
setattr(self, attr, value)
|
||||||
logger.info(f'Updated {attr}: {value}')
|
logger.info(f'Updated {attr}: {value}')
|
||||||
return True
|
return True
|
||||||
@ -90,7 +90,7 @@ class PlayerTracker:
|
|||||||
|
|
||||||
# Store both original name and color-stripped version
|
# Store both original name and color-stripped version
|
||||||
self.player_teams[name] = team
|
self.player_teams[name] = team
|
||||||
clean_name = re.sub(r'\^\d', '', name)
|
clean_name = strip_color_codes(name)
|
||||||
if clean_name != name:
|
if clean_name != name:
|
||||||
self.player_teams[clean_name] = team
|
self.player_teams[clean_name] = team
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ class PlayerTracker:
|
|||||||
|
|
||||||
def remove_player(self, name):
|
def remove_player(self, name):
|
||||||
"""Remove player from tracking"""
|
"""Remove player from tracking"""
|
||||||
clean_name = re.sub(r'\^\d', '', name)
|
clean_name = strip_color_codes(name)
|
||||||
|
|
||||||
# Try to remove by exact name first
|
# Try to remove by exact name first
|
||||||
removed = self.server_info.players.pop(name, None)
|
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 found, try to find by clean name
|
||||||
if not removed:
|
if not removed:
|
||||||
for player_name in list(self.server_info.players.keys()):
|
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)
|
removed = self.server_info.players.pop(player_name)
|
||||||
logger.info(f'Removed player: {player_name} (matched clean name: {clean_name})')
|
logger.info(f'Removed player: {player_name} (matched clean name: {clean_name})')
|
||||||
break
|
break
|
||||||
@ -146,7 +146,7 @@ class PlayerTracker:
|
|||||||
|
|
||||||
def rename_player(self, old_name, new_name):
|
def rename_player(self, old_name, new_name):
|
||||||
"""Rename a player while maintaining their team and score"""
|
"""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)
|
# Get current team (try both names)
|
||||||
team = self.player_teams.get(old_name) or self.player_teams.get(old_clean, 'SPECTATOR')
|
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 found by exact name, try clean name
|
||||||
if not player_data:
|
if not player_data:
|
||||||
for player_name in list(self.server_info.players.keys()):
|
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)
|
player_data = self.server_info.players.pop(player_name)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -184,9 +184,9 @@ class PlayerTracker:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Fallback: search by clean name (rare case)
|
# 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():
|
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))
|
current_score = int(player_data.get('score', 0))
|
||||||
player_data['score'] = str(current_score + delta)
|
player_data['score'] = str(current_score + delta)
|
||||||
logger.debug(f"Score update: {player_name} {delta:+d} -> {player_data['score']}")
|
logger.debug(f"Score update: {player_name} {delta:+d} -> {player_data['score']}")
|
||||||
|
|||||||
Reference in New Issue
Block a user