Compare commits

...

3 Commits

Author SHA1 Message Date
pfl
a03b4b0b93 Add missing Quake Live commands to autocomplete
- Match control: readyall, allready, abort, pause, unpause, lock, unlock, timeout, timein
- Player management: shuffle, put, mute, unmute, slap, slay
- Server control: restart, endgame, nextmap, forcemap
- QLX commands: qlx, elo, balance, teams, scores
- Info commands: serverinfo, players, maplist, configstrings

Total commands increased from 18 to 47
2026-01-09 14:23:50 +01:00
pfl
e6e32033ad Add live match timer countdown
- Add match_time_last_sync timestamp to track server updates
- Update timestamp in parser when TIME events received
- Calculate elapsed time in UI for live countdown
- Timer now ticks in real-time between server events
2026-01-09 14:00:32 +01:00
pfl
16b5209338 Fix exception handling in ui.py
- Replace 6 bare except: with except curses.error:
- Remove inline import logging, use module-level logger
- Improves safety by not catching KeyboardInterrupt/SystemExit
2026-01-09 13:56:14 +01:00
4 changed files with 56 additions and 10 deletions

View File

@ -119,6 +119,39 @@ COMMANDS = [
'killserver', 'killserver',
'quit', 'quit',
'team', 'team',
# Match control
'readyall',
'allready',
'abort',
'pause',
'unpause',
'lock',
'unlock',
'timeout',
'timein',
# Player management
'shuffle',
'put',
'mute',
'unmute',
'slap',
'slay',
# Server control
'restart',
'endgame',
'nextmap',
'forcemap',
# QLX commands
'qlx',
'elo',
'balance',
'teams',
'scores',
# Info commands
'serverinfo',
'players',
'maplist',
'configstrings',
] ]
# Bot names for addbot command (complete list) # Bot names for addbot command (complete list)

View File

@ -94,6 +94,7 @@ class EventParser:
# Get Match Time # Get Match Time
if 'TIME' in data: if 'TIME' in data:
self.game_state.server_info.match_time = int(data['TIME']) self.game_state.server_info.match_time = int(data['TIME'])
self.game_state.server_info.match_time_last_sync = time.time()
if 'KILLER' not in data: if 'KILLER' not in data:
return None return None
@ -138,6 +139,7 @@ class EventParser:
# Get Match Time # Get Match Time
if 'TIME' in data: if 'TIME' in data:
self.game_state.server_info.match_time = int(data['TIME']) self.game_state.server_info.match_time = int(data['TIME'])
self.game_state.server_info.match_time_last_sync = time.time()
if 'VICTIM' not in data: if 'VICTIM' not in data:
return None return None
@ -244,6 +246,7 @@ class EventParser:
# Get Match Time # Get Match Time
if 'TIME' in data: if 'TIME' in data:
self.game_state.server_info.match_time = int(data['TIME']) self.game_state.server_info.match_time = int(data['TIME'])
self.game_state.server_info.match_time_last_sync = time.time()
team_won = data.get('TEAM_WON') team_won = data.get('TEAM_WON')
round_num = data.get('ROUND', 0) round_num = data.get('ROUND', 0)
@ -265,6 +268,7 @@ class EventParser:
# Get Match Time # Get Match Time
if 'TIME' in data: if 'TIME' in data:
self.game_state.server_info.match_time = int(data['TIME']) self.game_state.server_info.match_time = int(data['TIME'])
self.game_state.server_info.match_time_last_sync = time.time()
name = data.get('NAME', 'Unknown') name = data.get('NAME', 'Unknown')
medal = data.get('MEDAL', 'UNKNOWN') medal = data.get('MEDAL', 'UNKNOWN')
@ -299,6 +303,7 @@ class EventParser:
# Get Match Time # Get Match Time
if 'TIME' in data: if 'TIME' in data:
self.game_state.server_info.match_time = int(data['TIME']) self.game_state.server_info.match_time = int(data['TIME'])
self.game_state.server_info.match_time_last_sync = time.time()
if self.game_state.server_info.is_team_mode(): if self.game_state.server_info.is_team_mode():
return f"^8^2[GAME ON]^0 ^7Match has started - ^1^8RED ^0^7vs. ^4^8BLUE\n" return f"^8^2[GAME ON]^0 ^7Match has started - ^1^8RED ^0^7vs. ^4^8BLUE\n"
@ -320,6 +325,7 @@ class EventParser:
# Get Match Time # Get Match Time
if 'TIME' in data: if 'TIME' in data:
self.game_state.server_info.match_time = int(data['TIME']) self.game_state.server_info.match_time = int(data['TIME'])
self.game_state.server_info.match_time_last_sync = time.time()
if not self.game_state.server_info.is_team_mode(): if not self.game_state.server_info.is_team_mode():
return None return None

View File

@ -34,6 +34,7 @@ class ServerInfo:
self.dead_players = {} self.dead_players = {}
self.round_end_time = None self.round_end_time = None
self.match_time = 0 self.match_time = 0
self.match_time_last_sync = 0 # Timestamp of last TIME update from server
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"""

26
ui.py
View File

@ -9,6 +9,7 @@ import curses.textpad
import threading import threading
import queue import queue
import logging import logging
import time
from config import COLOR_PAIRS, INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, TEAM_MODES, MAX_COMMAND_HISTORY from config import COLOR_PAIRS, INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, TEAM_MODES, MAX_COMMAND_HISTORY
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
@ -105,7 +106,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
else: else:
window.addstr(1, x_pos, arg_text, curses.A_DIM) window.addstr(1, x_pos, arg_text, curses.A_DIM)
x_pos += len(arg_text) + 1 x_pos += len(arg_text) + 1
except: except curses.error:
pass pass
else: else:
# User is typing arguments # User is typing arguments
@ -134,7 +135,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
match_line = f'<{arg_type}>: {" ".join(display_suggestions)}{more_indicator}' match_line = f'<{arg_type}>: {" ".join(display_suggestions)}{more_indicator}'
try: try:
window.addstr(1, 0, match_line, curses.A_DIM) window.addstr(1, 0, match_line, curses.A_DIM)
except: except curses.error:
pass pass
suggestions = arg_suggestions # Store for Tab cycling suggestions = arg_suggestions # Store for Tab cycling
suggestion_index = -1 suggestion_index = -1
@ -151,7 +152,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
else: else:
window.addstr(1, x_pos, arg_text, curses.A_DIM) window.addstr(1, x_pos, arg_text, curses.A_DIM)
x_pos += len(arg_text) + 1 x_pos += len(arg_text) + 1
except: except curses.error:
pass pass
elif first_word in COMMAND_SIGNATURES and COMMAND_SIGNATURES[first_word]: elif first_word in COMMAND_SIGNATURES and COMMAND_SIGNATURES[first_word]:
@ -166,7 +167,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
else: else:
window.addstr(1, x_pos, arg_text, curses.A_DIM) window.addstr(1, x_pos, arg_text, curses.A_DIM)
x_pos += len(arg_text) + 1 x_pos += len(arg_text) + 1
except: except curses.error:
pass pass
else: else:
@ -180,7 +181,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
match_line = ' '.join(suggestions) match_line = ' '.join(suggestions)
try: try:
window.addstr(1, 0, match_line, curses.A_DIM) window.addstr(1, 0, match_line, curses.A_DIM)
except: except curses.error:
pass pass
return suggestions, suggestion_index, original_word return suggestions, suggestion_index, original_word
@ -367,7 +368,7 @@ class UIManager:
try: try:
window.addstr(1, 0, display_line, curses.A_DIM) window.addstr(1, 0, display_line, curses.A_DIM)
except: except curses.error:
pass pass
window.move(0, cursor_pos) window.move(0, cursor_pos)
@ -496,8 +497,7 @@ class UIManager:
curses.doupdate() # Immediate screen update curses.doupdate() # Immediate screen update
except Exception as e: except Exception as e:
import logging logger.error(f'Input error: {e}')
logging.getLogger('ui').error(f'Input error: {e}')
# Log but continue - input thread should stay alive # Log but continue - input thread should stay alive
window.move(0, cursor_pos) window.move(0, cursor_pos)
@ -536,8 +536,14 @@ class UIManager:
timer_display = "" timer_display = ""
if server_info.match_time > 0 and not server_info.warmup: if server_info.match_time > 0 and not server_info.warmup:
mins = server_info.match_time // 60 # Calculate live time: add elapsed seconds since last server update
secs = server_info.match_time % 60 current_time = server_info.match_time
if server_info.match_time_last_sync > 0:
elapsed = int(time.time() - server_info.match_time_last_sync)
current_time += elapsed
mins = current_time // 60
secs = current_time % 60
timer_display = f"^3^0Time:^8^7 {mins}:{secs:02d}^0" timer_display = f"^3^0Time:^8^7 {mins}:{secs:02d}^0"
else: else:
timer_display = "^3^0Time:^8^7 0:00^0" timer_display = "^3^0Time:^8^7 0:00^0"