From c1b5f6d86ab7aad7485a197794d935148236b3c7 Mon Sep 17 00:00:00 2001 From: Debian Date: Fri, 18 Sep 2026 14:26:06 +0200 Subject: [PATCH] Populate team rosters from a LIVESTATS team token and tag the damage list Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- lib/state.py | 15 ++++++++++++--- lib/ui.py | 24 ++++++++++++++++-------- main.py | 14 ++++++++++---- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/lib/state.py b/lib/state.py index 77e9615..1a519d7 100644 --- a/lib/state.py +++ b/lib/state.py @@ -149,16 +149,25 @@ class PlayerTracker: return True return False - def update_livestats(self, clean_name, score, dealt, taken, ping): - """Update live score/damage/ping from a LIVESTATS console print (clean name)""" + def update_livestats(self, clean_name, score, dealt, taken, ping, team=None): + """Update live score/damage/ping from a LIVESTATS console print (clean name). + With a team token (extended livedamage format) the team is authoritative — + it sorts tracked players and adds unknown ones to the roster, which is + what populates teams correctly during warmup.""" for player_name, player_data in self.server_info.players.items(): if strip_color_codes(player_name) == clean_name: player_data['score'] = str(score) player_data['ping'] = str(ping) player_data['dealt'] = str(dealt) player_data['taken'] = str(taken) + if team is not None: + self.update_team(player_name, team) return True - return False + if team is None: + return False + self.add_player(clean_name, score=str(score), ping=str(ping)) + self.update_team(clean_name, team) + return True def remove_player(self, name): """Remove player from tracking""" diff --git a/lib/ui.py b/lib/ui.py index bf1b707..2ffa3ff 100644 --- a/lib/ui.py +++ b/lib/ui.py @@ -18,9 +18,10 @@ import time from collections import deque from .constants import (INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, - MIN_ROWS, MIN_COLS, OUTPUT_SCROLLBACK_LINES, TEAM_MODES, MAX_COMMAND_HISTORY) + MIN_ROWS, MIN_COLS, OUTPUT_SCROLLBACK_LINES, TEAM_MODES, MAX_COMMAND_HISTORY, + TEAM_COLORS) from .cvars import autocomplete, COMMAND_SIGNATURES, get_signature_with_highlight, get_argument_suggestions, COMMAND_ARGUMENTS -from .formatter import strip_color_codes +from .formatter import get_team_prefix, strip_color_codes logger = logging.getLogger('ui') @@ -431,9 +432,11 @@ def _draw_server_menu(screen, servers, names, selected): screen.refresh() -def format_damage_summary(server_info): - """One-line damage summary (top 3 by damage dealt) from LIVESTATS counters; - None when no damage data exists yet. Dealt/taken are stored as strings.""" +def format_damage_summary(server_info, player_tracker=None): + """One-line damage summary (top 3 by damage dealt) from LIVESTATS counters, + tagged with the tracked team of each player (data-driven, so tags work + during warmup too); None when no damage data exists yet. Dealt/taken are + stored as strings.""" entries = [] for player_name, player_data in server_info.players.items(): try: @@ -447,8 +450,13 @@ def format_damage_summary(server_info): if not entries: return None entries.sort(key=lambda x: x[1], reverse=True) - summary = ' '.join(f"{name} {dealt}/{taken}" for name, dealt, taken in entries[:3]) - return f"^8^3DMG^7 {summary}\n" + parts = [] + for name, dealt, taken in entries[:3]: + prefix = '' + if player_tracker is not None: + prefix = TEAM_COLORS.get(player_tracker.get_team(name), '') + parts.append(f"{prefix}{name} {dealt}/{taken}") + return f"^8^3DMG^7 {' '.join(parts)}\n" class UIManager: @@ -893,7 +901,7 @@ class UIManager: print_colored(self.info_window, line, 0) # Damage summary from LIVESTATS counters (top 3 by damage dealt) - damage_line = format_damage_summary(server_info) + damage_line = format_damage_summary(server_info, game_state.player_tracker) if damage_line: print_colored(self.info_window, damage_line, 0) diff --git a/main.py b/main.py index 1fdf904..912a90e 100644 --- a/main.py +++ b/main.py @@ -37,7 +37,8 @@ RENAME_PATTERN = re.compile(r'^(.+?)\s+renamed to\s+(.+?)$') STATUS_HEAD_PATTERN = re.compile(r'^\s*\d+\s+-?\d+\s+(bot|\d+)\s+\S', re.MULTILINE) STATUS_MAP_PATTERN = re.compile(r'^map:\s+\S+\s*$', re.MULTILINE) STEAMID_PATTERN = re.compile(r'\b\d{16,}\b') -LIVESTATS_PATTERN = re.compile(r'^LIVESTATS\s+(.+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s*$') +LIVESTATS_PATTERN = re.compile( + r'^LIVESTATS\s+(.+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+([A-Za-z]+|\d))?\s*$') STATUS_ROW_PATTERN = re.compile( r'^\s*(\d+)\s+(-?\d+)\s+(\d+)\s+(.+?)\s+(\d+)\s+(\S+)\s+(\d+)\s+(\d+)(?:\s+(\d{15,}))?\s*$') ROSTER_CHAT_GUARD_PATTERN = re.compile(r'^.{1,40}:\s') @@ -179,7 +180,9 @@ def handle_livestats(message, game_state, ui, verbose=0): Consume every LIVESTATS heartbeat line (engine damage counters emitted every 10s by the livedamage plugin, plus its summary variants). Lines matching the full player shape update live score/damage/ping; names may - contain spaces, so the counters anchor from the end. The raw lines + contain spaces, so the counters anchor from the end. An optional trailing + team token (extended livedamage format) is authoritative and populates + the roster with correct teams — including during warmup. The raw lines display only at -v or higher verbosity; they are never treated as chat. """ clean = strip_color_codes(message).strip() @@ -187,8 +190,11 @@ def handle_livestats(message, game_state, ui, verbose=0): return False match = LIVESTATS_PATTERN.match(clean) if match: - name, score, dealt, taken, ping = match.groups() - game_state.player_tracker.update_livestats(name, int(score), int(dealt), int(taken), int(ping)) + name, score, dealt, taken, ping, team = match.groups() + if team is not None and team.isdigit(): + team = int(team) + game_state.player_tracker.update_livestats( + name, int(score), int(dealt), int(taken), int(ping), team) ui.update_server_info(game_state) if verbose > 0: timestamp = time.strftime('%H:%M:%S')