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 <[email protected]>
This commit is contained in:
Debian
2026-09-18 14:26:06 +02:00
co-authored by Sisyphus
parent cbeab1ae45
commit c1b5f6d86a
3 changed files with 38 additions and 15 deletions
+11 -2
View File
@@ -149,16 +149,25 @@ class PlayerTracker:
return True return True
return False return False
def update_livestats(self, clean_name, score, dealt, taken, ping): def update_livestats(self, clean_name, score, dealt, taken, ping, team=None):
"""Update live score/damage/ping from a LIVESTATS console print (clean name)""" """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(): for player_name, player_data in self.server_info.players.items():
if strip_color_codes(player_name) == clean_name: if strip_color_codes(player_name) == clean_name:
player_data['score'] = str(score) player_data['score'] = str(score)
player_data['ping'] = str(ping) player_data['ping'] = str(ping)
player_data['dealt'] = str(dealt) player_data['dealt'] = str(dealt)
player_data['taken'] = str(taken) player_data['taken'] = str(taken)
if team is not None:
self.update_team(player_name, team)
return True return True
if team is None:
return False 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): def remove_player(self, name):
"""Remove player from tracking""" """Remove player from tracking"""
+16 -8
View File
@@ -18,9 +18,10 @@ import time
from collections import deque from collections import deque
from .constants import (INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, 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 .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') logger = logging.getLogger('ui')
@@ -431,9 +432,11 @@ def _draw_server_menu(screen, servers, names, selected):
screen.refresh() screen.refresh()
def format_damage_summary(server_info): def format_damage_summary(server_info, player_tracker=None):
"""One-line damage summary (top 3 by damage dealt) from LIVESTATS counters; """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.""" 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 = [] entries = []
for player_name, player_data in server_info.players.items(): for player_name, player_data in server_info.players.items():
try: try:
@@ -447,8 +450,13 @@ def format_damage_summary(server_info):
if not entries: if not entries:
return None return None
entries.sort(key=lambda x: x[1], reverse=True) entries.sort(key=lambda x: x[1], reverse=True)
summary = ' '.join(f"{name} {dealt}/{taken}" for name, dealt, taken in entries[:3]) parts = []
return f"^8^3DMG^7 {summary}\n" 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: class UIManager:
@@ -893,7 +901,7 @@ class UIManager:
print_colored(self.info_window, line, 0) print_colored(self.info_window, line, 0)
# Damage summary from LIVESTATS counters (top 3 by damage dealt) # 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: if damage_line:
print_colored(self.info_window, damage_line, 0) print_colored(self.info_window, damage_line, 0)
+10 -4
View File
@@ -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_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) STATUS_MAP_PATTERN = re.compile(r'^map:\s+\S+\s*$', re.MULTILINE)
STEAMID_PATTERN = re.compile(r'\b\d{16,}\b') 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( 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*$') 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') 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 Consume every LIVESTATS heartbeat line (engine damage counters emitted
every 10s by the livedamage plugin, plus its summary variants). Lines every 10s by the livedamage plugin, plus its summary variants). Lines
matching the full player shape update live score/damage/ping; names may 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. display only at -v or higher verbosity; they are never treated as chat.
""" """
clean = strip_color_codes(message).strip() clean = strip_color_codes(message).strip()
@@ -187,8 +190,11 @@ def handle_livestats(message, game_state, ui, verbose=0):
return False return False
match = LIVESTATS_PATTERN.match(clean) match = LIVESTATS_PATTERN.match(clean)
if match: if match:
name, score, dealt, taken, ping = match.groups() name, score, dealt, taken, ping, team = match.groups()
game_state.player_tracker.update_livestats(name, int(score), int(dealt), int(taken), int(ping)) 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) ui.update_server_info(game_state)
if verbose > 0: if verbose > 0:
timestamp = time.strftime('%H:%M:%S') timestamp = time.strftime('%H:%M:%S')