This commit is contained in:
xbl
2025-12-29 14:00:21 +01:00
parent dbc4391be2
commit bc4f62dcf8
5 changed files with 197 additions and 46 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
__pycache__/ __pycache__/
venv3/ venv3/
*.json
*.log *.log
cvarlist.txt cvarlist.txt
curztest.py curztest.py

View File

@ -10,10 +10,10 @@ DEFAULT_HOST = 'tcp://127.0.0.1:27961'
POLL_TIMEOUT = 100 POLL_TIMEOUT = 100
# UI dimensions # UI dimensions
INFO_WINDOW_HEIGHT = 10 INFO_WINDOW_HEIGHT = 11
INFO_WINDOW_Y = 2 INFO_WINDOW_Y = 2
OUTPUT_WINDOW_Y = 12 OUTPUT_WINDOW_Y = 12
INPUT_WINDOW_HEIGHT = 1 INPUT_WINDOW_HEIGHT = 2
# Event deduplication # Event deduplication
MAX_RECENT_EVENTS = 10 MAX_RECENT_EVENTS = 10

View File

@ -66,7 +66,7 @@ class EventParser:
'PLAYER_STATS': self._handle_player_stats, 'PLAYER_STATS': self._handle_player_stats,
'PLAYER_CONNECT': lambda d: None, 'PLAYER_CONNECT': lambda d: None,
'PLAYER_DISCONNECT': lambda d: None, 'PLAYER_DISCONNECT': lambda d: None,
'ROUND_OVER': lambda d: None, 'ROUND_OVER': self._handle_round_over,
} }
handler = handler_map.get(event_type) handler = handler_map.get(event_type)
@ -130,61 +130,93 @@ class EventParser:
"""Handle PLAYER_DEATH and PLAYER_KILL events""" """Handle PLAYER_DEATH and PLAYER_KILL events"""
if 'VICTIM' not in data: if 'VICTIM' not in data:
return None return None
victim = data['VICTIM'] victim = data['VICTIM']
victim_name = victim.get('NAME', 'Unknown') victim_name = victim.get('NAME', 'Unknown')
# Check for duplicate # Check for duplicate
time_val = data.get('TIME', 0) time_val = data.get('TIME', 0)
killer_name = data.get('KILLER', {}).get('NAME', '') if data.get('KILLER') else '' killer_name = data.get('KILLER', {}).get('NAME', '') if data.get('KILLER') else ''
if self.game_state.event_deduplicator.is_duplicate('PLAYER_DEATH', time_val, killer_name, victim_name): if self.game_state.event_deduplicator.is_duplicate('PLAYER_DEATH', time_val, killer_name, victim_name):
return None return None
# Update victim team # Update victim team
if 'TEAM' in victim: if 'TEAM' in victim:
self.game_state.player_tracker.update_team(victim_name, victim['TEAM']) self.game_state.player_tracker.update_team(victim_name, victim['TEAM'])
self.game_state.player_tracker.add_player(victim_name) self.game_state.player_tracker.add_player(victim_name)
victim_prefix = get_team_prefix(victim_name, self.game_state.player_tracker) victim_prefix = get_team_prefix(victim_name, self.game_state.player_tracker)
warmup = " ^0^3(Warmup)^9" if data.get('WARMUP', False) else "" warmup = " ^0^3(Warmup)^9" if data.get('WARMUP', False) else ""
score_prefix = ""
# Environmental death (no killer) # Environmental death (no killer)
if 'KILLER' not in data or not data['KILLER']: if 'KILLER' not in data or not data['KILLER']:
# -1 for environmental death
if not data.get('WARMUP', False):
self.game_state.player_tracker.update_score(victim_name, -1)
score_prefix = "^0^1[-1]^9 "
mod = data.get('MOD', 'UNKNOWN') mod = data.get('MOD', 'UNKNOWN')
msg_template = DEATH_MESSAGES.get(mod, "%s^0%s^9 ^1DIED FROM %s^7") msg_template = DEATH_MESSAGES.get(mod, "%s^0%s^9 ^1DIED FROM %s^7")
if mod in DEATH_MESSAGES: if mod in DEATH_MESSAGES:
msg = msg_template % (victim_prefix, victim_name) msg = msg_template % (victim_prefix, victim_name)
else: else:
msg = msg_template % (victim_prefix, victim_name, mod) msg = msg_template % (victim_prefix, victim_name, mod)
return f"{msg}{warmup}\n" return f"{score_prefix}{msg}{warmup}\n"
# Player killed by another player # Player killed by another player
killer = data['KILLER'] killer = data['KILLER']
killer_name = killer.get('NAME', 'Unknown') killer_name = killer.get('NAME', 'Unknown')
# Update killer team # Update killer team
if 'TEAM' in killer: if 'TEAM' in killer:
self.game_state.player_tracker.update_team(killer_name, killer['TEAM']) self.game_state.player_tracker.update_team(killer_name, killer['TEAM'])
self.game_state.player_tracker.add_player(killer_name) self.game_state.player_tracker.add_player(killer_name)
killer_prefix = get_team_prefix(killer_name, self.game_state.player_tracker) killer_prefix = get_team_prefix(killer_name, self.game_state.player_tracker)
# Suicide # Suicide
if killer_name == victim_name: if killer_name == victim_name:
# -1 for suicide
if not data.get('WARMUP', False):
self.game_state.player_tracker.update_score(victim_name, -1)
score_prefix = "^0^1[-1]^9 "
weapon = killer.get('WEAPON', 'OTHER_WEAPON') weapon = killer.get('WEAPON', 'OTHER_WEAPON')
if weapon != 'OTHER_WEAPON': if weapon != 'OTHER_WEAPON':
weapon_name = WEAPON_NAMES.get(weapon, weapon) weapon_name = WEAPON_NAMES.get(weapon, weapon)
return f"{killer_prefix}^0{killer_name}^9 ^7committed suicide with the ^7{weapon_name}{warmup}\n" return f"{score_prefix}{killer_prefix}^0{killer_name}^9 ^7committed suicide with the ^7{weapon_name}{warmup}\n"
return None return None
# Regular kill # Regular kill: +1 for killer
if not data.get('WARMUP', False):
self.game_state.player_tracker.update_score(killer_name, 1)
score_prefix = "^0^2[+1]^9 "
else:
score_prefix = ""
weapon = killer.get('WEAPON', 'UNKNOWN') weapon = killer.get('WEAPON', 'UNKNOWN')
weapon_name = WEAPON_KILL_NAMES.get(weapon, f'the {weapon}') weapon_name = WEAPON_KILL_NAMES.get(weapon, f'the {weapon}')
return f"{killer_prefix}^0{killer_name}^9 ^7fragged^7 {victim_prefix}^0{victim_name}^9 ^7with {weapon_name}{warmup}\n" return f"{score_prefix}{killer_prefix}^0{killer_name}^9 ^7fragged^7 {victim_prefix}^0{victim_name}^9 ^7with {weapon_name}{warmup}\n"
def _handle_round_over(self, data):
"""Handle ROUND_OVER events for CA"""
team_won = data.get('TEAM_WON')
round_num = data.get('ROUND', 0)
if team_won == 'RED':
self.game_state.server_info.red_rounds += 1
logger.info(f"Round {round_num}: RED wins (RED: {self.game_state.server_info.red_rounds}, BLUE: {self.game_state.server_info.blue_rounds})")
elif team_won == 'BLUE':
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})")
return None # Don't display in chat
def _handle_medal(self, data): def _handle_medal(self, data):
"""Handle PLAYER_MEDAL event""" """Handle PLAYER_MEDAL event"""

View File

@ -25,7 +25,9 @@ class ServerInfo:
self.maxclients = '0' self.maxclients = '0'
self.curclients = '0' self.curclients = '0'
self.red_score = 0 self.red_score = 0
self.red_rounds = 0
self.blue_score = 0 self.blue_score = 0
self.blue_rounds = 0
self.players = [] self.players = []
self.last_update = 0 self.last_update = 0
self.warmup = False self.warmup = False
@ -33,6 +35,11 @@ class ServerInfo:
def is_team_mode(self): def is_team_mode(self):
"""Check if current gametype is a team mode""" """Check if current gametype is a team mode"""
return self.gametype in TEAM_MODES return self.gametype in TEAM_MODES
def reset_round_scores(self):
"""Reset round scores (for new matches)"""
self.red_rounds = 0
self.blue_rounds = 0
def update_from_cvar(self, cvar_name, value): def update_from_cvar(self, cvar_name, value):
"""Update server info from a cvar response""" """Update server info from a cvar response"""
@ -162,6 +169,20 @@ class PlayerTracker:
logger.debug(f'Renamed player: {old_name} -> {new_name} (team: {team})') logger.debug(f'Renamed player: {old_name} -> {new_name} (team: {team})')
def update_score(self, name, delta):
"""Update player's score by delta (+1 for kill, -1 for death/suicide)"""
clean_name = re.sub(r'\^\d', '', name)
for player in self.server_info.players:
player_clean = re.sub(r'\^\d', '', player['name'])
if player['name'] == name or player_clean == clean_name:
current_score = int(player.get('score', 0))
player['score'] = str(current_score + delta)
logger.debug(f"Score update: {name} {delta:+d} -> {player['score']}")
return
logger.warning(f"Could not update score for {name} - player not found")
class EventDeduplicator: class EventDeduplicator:
"""Prevents duplicate kill/death events""" """Prevents duplicate kill/death events"""

147
ui.py
View File

@ -97,7 +97,6 @@ class UIManager:
curses.use_default_colors() curses.use_default_colors()
curses.cbreak() curses.cbreak()
curses.curs_set(0) curses.curs_set(0)
#curses.setsyx(-1, -1)
self.screen.addstr(f"Quake Live PyCon: {self.host}") self.screen.addstr(f"Quake Live PyCon: {self.host}")
self.screen.noutrefresh() self.screen.noutrefresh()
@ -308,7 +307,8 @@ class UIManager:
# Line 1: Hostname # Line 1: Hostname
hostname = server_info.hostname hostname = server_info.hostname
print_colored(self.info_window, f"^3Name:^0 {hostname} ^7\n", 0) warmup_display = "^3Warmup: ^2YES^9" if server_info.warmup else "^3Warmup: ^1NO^9"
print_colored(self.info_window, f"^3Name:^0 {hostname} {warmup_display}\n", 0)
# Line 2: Game info # Line 2: Game info
gametype = server_info.gametype gametype = server_info.gametype
@ -328,57 +328,154 @@ class UIManager:
teams = game_state.player_tracker.get_players_by_team() teams = game_state.player_tracker.get_players_by_team()
if server_info.gametype in TEAM_MODES: if server_info.gametype in TEAM_MODES:
warmup_display = "^3Warmup: ^2YES^9" if server_info.warmup else "^3Warmup: ^1NO^9" if server_info.gametype == 'CA':
print_colored(self.info_window, f"^0^1(RED) ^4(BLUE) ^3(SPEC) {warmup_display}\n", 0) red_score = f"{server_info.red_rounds:>3} "
blue_score = f"{server_info.blue_rounds:>3} "
else:
red_total = 0
blue_total = 0
for player in server_info.players:
player_name = player['name']
team = game_state.player_tracker.get_team(player_name)
score = int(player.get('score', 0))
if team == 'RED':
red_total += score
elif team == 'BLUE':
blue_total += score
red_score = f"{red_total:>3} "
blue_score = f"{blue_total:>3} "
print_colored(self.info_window, f"^0^1RED TEAM: ^7{red_score} ^4BLUE TEAM: ^7{blue_score} ^3(SPEC)\n", 0)
red_players = teams['RED'][:4] # Sort players by score within each team
blue_players = teams['BLUE'][:4] red_players_with_scores = []
blue_players_with_scores = []
spec_players = []
for player_name in teams['RED']:
score = 0
for player in server_info.players:
if player['name'] == player_name:
score = int(player.get('score', 0))
break
red_players_with_scores.append((player_name, score))
for player_name in teams['BLUE']:
score = 0
for player in server_info.players:
if player['name'] == player_name:
score = int(player.get('score', 0))
break
blue_players_with_scores.append((player_name, score))
# Sort by score descending
red_players_with_scores.sort(key=lambda x: x[1], reverse=True)
blue_players_with_scores.sort(key=lambda x: x[1], reverse=True)
red_players = [name for name, score in red_players_with_scores[:4]]
blue_players = [name for name, score in blue_players_with_scores[:4]]
spec_players = teams['SPECTATOR'][:4] spec_players = teams['SPECTATOR'][:4]
for i in range(4): for i in range(4):
red = red_players[i] if i < len(red_players) else '' red_name = red_players[i] if i < len(red_players) else ''
blue = blue_players[i] if i < len(blue_players) else '' blue_name = blue_players[i] if i < len(blue_players) else ''
spec = spec_players[i] if i < len(spec_players) else '' spec = spec_players[i] if i < len(spec_players) else ''
# Strip color codes for padding calculation # Get scores for team players
red_score = ''
blue_score = ''
if red_name:
for player in server_info.players:
if player['name'] == red_name:
red_score = player.get('score', '0')
break
if blue_name:
for player in server_info.players:
if player['name'] == blue_name:
blue_score = player.get('score', '0')
break
# Format: " 9 PlayerName" with right-aligned score
red = f"{red_score:>3} {red_name}" if red_name else ''
blue = f"{blue_score:>3} {blue_name}" if blue_name else ''
from formatter import strip_color_codes from formatter import strip_color_codes
red_clean = strip_color_codes(red) red_clean = strip_color_codes(red)
blue_clean = strip_color_codes(blue) blue_clean = strip_color_codes(blue)
spec_clean = strip_color_codes(spec) spec_clean = strip_color_codes(spec)
# Calculate padding (24 chars per column)
red_pad = 24 - len(red_clean) red_pad = 24 - len(red_clean)
blue_pad = 24 - len(blue_clean) blue_pad = 24 - len(blue_clean)
line = f"^0{red}^9{' ' * red_pad}^0{blue}^9{' ' * blue_pad}^0{spec}^9\n" line = f"^0{red}^9{' ' * red_pad}^0{blue}^9{' ' * blue_pad}^0{spec}^9\n"
print_colored(self.info_window, line, 0) print_colored(self.info_window, line, 0)
else: else:
warmup_display = "^3Warmup: ^2YES^9" if server_info.warmup else "^3Warmup: ^1NO^9" print_colored(self.info_window, f" ^0^5(FREE) ^5(FREE) ^3(SPEC)\n", 0)
print_colored(self.info_window, f"^0^5(FREE) ^5(FREE) ^3(SPEC) {warmup_display}\n", 0) # Sort FREE players by score (highest first)
free_players = teams['FREE'] free_players = teams['FREE']
free_players_with_scores = []
for player_name in free_players:
score = 0
for player in server_info.players:
if player['name'] == player_name:
score = int(player.get('score', 0))
break
free_players_with_scores.append((player_name, score))
# Sort by score descending
free_players_with_scores.sort(key=lambda x: x[1], reverse=True)
sorted_free_players = [name for name, score in free_players_with_scores]
spec_players = teams['SPECTATOR'][:4] spec_players = teams['SPECTATOR'][:4]
free_col1 = free_players[:4] free_col1 = sorted_free_players[:4]
free_col2 = free_players[4:8] free_col2 = sorted_free_players[4:8]
for i in range(4): for i in range(4):
col1 = free_col1[i] if i < len(free_col1) else '' col1_name = free_col1[i] if i < len(free_col1) else ''
col2 = free_col2[i] if i < len(free_col2) else '' col2_name = free_col2[i] if i < len(free_col2) else ''
spec = spec_players[i] if i < len(spec_players) else '' spec = spec_players[i] if i < len(spec_players) else ''
# Get scores for FREE players
col1_score = ''
col2_score = ''
if col1_name:
for player in server_info.players:
if player['name'] == col1_name:
col1_score = player.get('score', '0')
break
if col2_name:
for player in server_info.players:
if player['name'] == col2_name:
col2_score = player.get('score', '0')
break
# Format: " 9 PlayerName" with right-aligned score
col1 = f"{col1_score:>3} {col1_name}" if col1_name else ''
col2 = f"{col2_score:>3} {col2_name}" if col2_name else ''
from formatter import strip_color_codes from formatter import strip_color_codes
col1_clean = strip_color_codes(col1) col1_clean = strip_color_codes(col1)
col2_clean = strip_color_codes(col2) col2_clean = strip_color_codes(col2)
spec_clean = strip_color_codes(spec) spec_clean = strip_color_codes(spec)
col1_pad = 24 - len(col1_clean) col1_pad = 24 - len(col1_clean)
col2_pad = 24 - len(col2_clean) col2_pad = 24 - len(col2_clean)
line = f"^0{col1}^9{' ' * col1_pad}^0{col2}^9{' ' * col2_pad}^0{spec}^9\n" line = f"^0{col1}^9{' ' * col1_pad}^0{col2}^9{' ' * col2_pad}^0{spec}^9\n"
print_colored(self.info_window, line, 0) print_colored(self.info_window, line, 0)
# Blank lines to fill # Blank lines to fill
self.info_window.addstr("\n\n") self.info_window.addstr("\n")
line=f"^0^3(SPEC)^7^9\n"
print_colored(self.info_window, line, 0)
# Blank lines to fill
self.info_window.addstr("\n")
# Separator # Separator
separator = "^7" + "" * (max_x - 1) + "^7" separator = "^7" + "" * (max_x - 1) + "^7"
print_colored(self.info_window, separator, 0) print_colored(self.info_window, separator, 0)