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)
@ -148,9 +148,15 @@ class EventParser:
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")
@ -159,7 +165,7 @@ class EventParser:
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']
@ -174,17 +180,43 @@ class EventParser:
# 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
@ -34,6 +36,11 @@ class ServerInfo:
"""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"""
# Normalize cvar name to lowercase for case-insensitive matching # Normalize cvar name to lowercase for case-insensitive matching
@ -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"""

131
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,43 +328,134 @@ 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} "
red_players = teams['RED'][:4] else:
blue_players = teams['BLUE'][:4] 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)
# Sort players by score within each team
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)
@ -377,7 +468,13 @@ class UIManager:
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"