Announce forfeited match winners and show LIVESTATS lines only with -v

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 12:19:23 +02:00
co-authored by Sisyphus
parent c08db27b57
commit 38f1446cde
2 changed files with 125 additions and 8 deletions
+89 -3
View File
@@ -37,6 +37,19 @@ 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+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s*$')
ROSTER_CHAT_GUARD_PATTERN = re.compile(r'^.{1,40}:\s')
VOTE_CAST_PATTERN = re.compile(r'^(.+?) voted for (.+)\.$')
CHAT_SHAPE_PATTERN = re.compile(r'^[\w(][^:]*:\s')
GAME_END_PATTERN = re.compile(
r'((?:timelimit|fraglimit|capturelimit|roundlimit)\s+hit'
r'|hit\s+the\s+(?:timelimit|fraglimit|capturelimit|roundlimit)'
r'|game\s+has\s+been\s+forfeited'
r'|wins\s+the\s+round)', re.IGNORECASE)
ENGINE_OUTPUT_PREFIXES = (
'warning:', 'server:', 'error:', 'fatal:', 'livestats',
'gamename:', 'gamedate:', 'protocol:', 'cheats:', 'sv_tags:', 'client ',
)
# Configure logging
logger = logging.getLogger('main')
@@ -134,7 +147,62 @@ def is_command_output(message):
return False
CHAT_SHAPE_PATTERN = re.compile(r'^[\w(][^:]*:\s')
def is_engine_output(message):
"""Engine/banner prints share the chat shape but are never chat
(they would surface as fake chat from players named WARNING or Server)"""
clean = strip_color_codes(message).lstrip()
return clean.lower().startswith(ENGINE_OUTPUT_PREFIXES)
def handle_livestats(message, game_state, ui, verbose=0):
"""
Parse a LIVESTATS print (engine damage counters, emitted every 10s by the
livedamage plugin) and update the player's live score/damage/ping.
Returns True when the line is consumed. The raw line displays only at -v
or higher verbosity; it is never treated as chat.
"""
match = LIVESTATS_PATTERN.match(strip_color_codes(message).strip())
if not match:
return False
name, score, dealt, taken, ping = match.groups()
game_state.player_tracker.update_livestats(name, int(score), int(dealt), int(taken), int(ping))
ui.update_server_info(game_state)
if verbose > 0:
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3]^7 {message.rstrip()}\n")
return True
def handle_vote_broadcast(message, game_state, ui):
"""
Track vote lifecycle broadcasts and display them verbatim (color codes
intact), annotating the outcome with the cast subject when every cast
agreed. Returns True when the message is a vote line and has been
handled; chat-shaped lines are never vote events.
"""
clean = strip_color_codes(message).strip()
if not clean or ROSTER_CHAT_GUARD_PATTERN.match(clean):
return False
tracker = game_state.vote_tracker
timestamp = time.strftime('%H:%M:%S')
if clean.endswith('called a vote.'):
tracker.on_call()
ui.print_message(f"^3[^7{timestamp}^3]^7 {message.rstrip()}\n")
return True
cast = VOTE_CAST_PATTERN.match(clean)
if cast:
tracker.on_cast(cast.group(2))
ui.print_message(f"^3[^7{timestamp}^3]^7 {message.rstrip()}\n")
return True
if clean in ('Vote passed.', 'Vote failed.'):
line = message.rstrip()
subject = tracker.result_annotation()
if subject:
line = f"{line}{subject}"
ui.print_message(f"^3[^7{timestamp}^3]^7 {line}\n")
tracker.on_call()
return True
return False
def is_real_content(message):
@@ -165,6 +233,8 @@ def is_real_content(message):
clean = clean[:-1]
if 'Game Initialization' in clean:
return True
if GAME_END_PATTERN.search(clean):
return True
if CHAT_SHAPE_PATTERN.match(clean):
return True
return False
@@ -281,6 +351,12 @@ def parse_player_events(message, game_state, ui):
# Strip color codes for matching
clean_msg = strip_color_codes(msg)
# Roster events are only roster events when the line is not chat-shaped:
# "Foo: I just connected my router" must not spawn a phantom join for
# a player named "Foo: I just"
if ROSTER_CHAT_GUARD_PATTERN.match(clean_msg):
return False
# Match connects: "NAME connected" or "NAME connected with Steam ID"
connect_match = CONNECT_PATTERN.match(clean_msg)
if connect_match:
@@ -524,6 +600,15 @@ def main_loop(screen, args):
if parse_player_events(message, game_state, ui):
continue
# LIVESTATS prints feed live score/damage/ping; the raw
# line only displays at -v or higher verbosity
if handle_livestats(message, game_state, ui, args.verbose):
continue
# Vote lifecycle broadcasts are tracked and shown verbatim
if handle_vote_broadcast(message, game_state, ui):
continue
# Command echoes only show at -v or higher verbosity; a
# status echo opens a window that hides its chunked output
if is_command_echo(message):
@@ -647,8 +732,9 @@ def main_loop(screen, args):
logger.debug(f'Filtered bot debug: {message[:50]}')
continue
# Check if it's a chat message
if ':' in message and not message.startswith(('print', 'broadcast', 'zmq')):
# Check if it's a chat message; engine output shares the shape
if (':' in message and not message.startswith(('print', 'broadcast', 'zmq'))
and not is_engine_output(message)):
message = format_chat_message(message, game_state.player_tracker)
# Format and display message