This commit is contained in:
xbl
2025-12-29 22:40:31 +01:00
parent 1a57258a2d
commit c7b6df9932
5 changed files with 119 additions and 76 deletions

View File

@ -46,48 +46,48 @@ TEAM_COLORS = {
# Weapon names
WEAPON_NAMES = {
'ROCKET': 'Rocket Launcher',
'LIGHTNING': 'Lightning Gun',
'RAILGUN': 'Railgun',
'SHOTGUN': 'Shotgun',
'GAUNTLET': 'Gauntlet',
'GRENADE': 'Grenade Launcher',
'PLASMA': 'Plasma Gun',
'MACHINEGUN': 'Machine Gun'
'ROCKET': '^8^1Rocket Launcher^7^0',
'LIGHTNING': '^8^3Lightning Gun^7^0',
'RAILGUN': '^8^2Railgun^7^0',
'SHOTGUN': '^8^3Shotgun^7^0',
'GAUNTLET': '^8^1Gauntlet^7^0',
'GRENADE': '^8^2Grenade Launcher^7^0',
'PLASMA': '^8^6Plasma Gun^7^0',
'MACHINEGUN': '^8^3Machine Gun^7^0'
}
# Weapon names for kill messages
WEAPON_KILL_NAMES = {
'ROCKET': 'the Rocket Launcher',
'LIGHTNING': 'the Lightning Gun',
'RAILGUN': 'the Railgun',
'SHOTGUN': 'the Shotgun',
'GAUNTLET': 'the Gauntlet',
'GRENADE': 'the Grenade Launcher',
'PLASMA': 'the Plasma Gun',
'MACHINEGUN': 'the Machine Gun'
'ROCKET': 'the ^8^1Rocket Launcher',
'LIGHTNING': 'the ^8^3Lightning Gun',
'RAILGUN': 'the ^8^2Railgun',
'SHOTGUN': 'the ^8^3Shotgun',
'GAUNTLET': 'the ^8^1Gauntlet',
'GRENADE': 'the ^8^2Grenade Launcher',
'PLASMA': 'the ^8^6Plasma Gun',
'MACHINEGUN': 'the ^8^3Machine Gun'
}
# Death messages
DEATH_MESSAGES = {
'FALLING': "%s^0%s^9 ^7cratered.",
'HURT': "%s^0%s^9 ^7was in the wrong place.",
'LAVA': "%s^0%s^9 ^7does a backflip into the lava.",
'WATER': "%s^0%s^9 ^7sank like a rock.",
'SLIME': "%s^0%s^9 ^7melted.",
'CRUSH': "%s^0%s^9 ^7was crushed."
'FALLING': "%s^8%s^0 ^7cratered.",
'HURT': "%s^8%s^0 ^7was in the wrong place.",
'LAVA': "%s^8%s^0 ^7does a backflip into the lava.",
'WATER': "%s^8%s^0 ^7sank like a rock.",
'SLIME': "%s^8%s^0 ^7melted.",
'CRUSH': "%s^8%s^0 ^7was crushed."
}
# Powerup names and colors
POWERUP_COLORS = {
'Quad Damage': '^5Quad Damage^7',
'Battle Suit': '^3Battle Suit^7',
'Regeneration': '^1Regeneration^7',
'Haste': '^3Haste^7',
'Invisibility': '^5Invisibility^7',
'Flight': '^5Flight^7',
'Medkit': '^1Medkit^7',
'MegaHealth': '^4MegaHealth^7'
'Quad Damage': '^8^5Quad Damage^7^0',
'Battle Suit': '^8^3Battle Suit^7^0',
'Regeneration': '^8^1Regeneration^7^0',
'Haste': '^8^3Haste^7^0',
'Invisibility': '^8^5Invisibility^7^0',
'Flight': '^8^5Flight^7^0',
'Medkit': '^8^1Medkit^7^0',
'MegaHealth': '^8^4MegaHealth^7^0'
}
# Curses color pairs

View File

@ -137,7 +137,7 @@ def format_chat_message(message, player_tracker):
player_name = strip_color_codes(name_match.group(1).strip())
team_prefix = get_team_prefix(player_name, player_tracker)
location_clean = strip_color_codes(location_part)
return f"{team_prefix}^0{player_part}^9 ^3{location_clean}^7:^5{message_part}"
return f"{team_prefix}^8{player_part}^0 ^3{location_clean}^7:^5{message_part}"
# Team chat without location: (PlayerName): message
colon_match = re.match(r'^(\([^)]+\)):(\s*.*)', clean_msg)
@ -149,7 +149,7 @@ def format_chat_message(message, player_tracker):
if name_match:
player_name = strip_color_codes(name_match.group(1).strip())
team_prefix = get_team_prefix(player_name, player_tracker)
return f"{team_prefix}^0{player_part}^9^5{message_part}\n"
return f"{team_prefix}^8{player_part}^0^5{message_part}\n"
# Regular chat: PlayerName: message
parts = clean_msg.split(':', 1)
@ -160,7 +160,7 @@ def format_chat_message(message, player_tracker):
# Preserve original color-coded name
original_parts = message.replace(chr(25), '').split(':', 1)
if len(original_parts) == 2:
return f"{team_prefix}^0{original_parts[0]}^9:^2{original_parts[1]}"
return f"{team_prefix}^8{original_parts[0]}^0:^2{original_parts[1]}"
return message
@ -193,7 +193,7 @@ def format_powerup_message(message, player_tracker):
colored_powerup = POWERUP_COLORS.get(powerup_name, f'^6{powerup_name}^7')
timestamp = time.strftime('%H:%M:%S')
return f"^3[^7{timestamp}^3]^7 {team_prefix}^0{player_name}^9 ^7got the {colored_powerup}!\n"
return f"^3[^7{timestamp}^3]^7 {team_prefix}^8{player_name}^0 ^7got the {colored_powerup}!\n"
# Powerup carrier kill: "PlayerName killed the PowerupName carrier!"
carrier_match = re.match(r'^(.+?)\s+killed the\s+(.+?)\s+carrier!', message)
@ -206,6 +206,6 @@ def format_powerup_message(message, player_tracker):
colored_powerup = POWERUP_COLORS.get(powerup_name, f'^6{powerup_name}^7')
timestamp = time.strftime('%H:%M:%S')
return f"^3[^7{timestamp}^3]^7 {team_prefix}^0{player_name}^9 ^7killed the {colored_powerup} ^7carrier!\n"
return f"^3[^7{timestamp}^3]^7 {team_prefix}^8{player_name}^0 ^7killed the {colored_powerup} ^7carrier!\n"
return None

16
main.py
View File

@ -43,11 +43,11 @@ def signal_handler(sig, frame):
if quit_confirm_time is None or (current_time - quit_confirm_time) > 3:
# First Ctrl-C or timeout expired
logger.warning("^1^0Press Ctrl-C again within 3 seconds to quit^0")
logger.warning("^1^8Press Ctrl-C again within 3 seconds to quit^0")
quit_confirm_time = current_time
else:
# Second Ctrl-C within 3 seconds
logger.warning("^1^0Quittin'^9")
logger.warning("^1^8Quittin'^0")
sys.exit(0)
def parse_cvar_response(message, game_state, ui):
@ -146,7 +146,7 @@ def parse_player_events(message, game_state, ui):
# Only print if this is NOT the Steam ID line
if 'Steam ID' not in message:
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^2connected^7\n")
ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name} ^9^2connected\n")
return True
@ -163,7 +163,7 @@ def parse_player_events(message, game_state, ui):
ui.update_server_info(game_state)
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^1disconnected^7\n")
ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name} ^9^1disconnected\n")
return True
# Kick
@ -179,7 +179,7 @@ def parse_player_events(message, game_state, ui):
ui.update_server_info(game_state)
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^1was kicked^7\n")
ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name} ^1was kicked\n")
return True
# Inactivity
@ -195,7 +195,7 @@ def parse_player_events(message, game_state, ui):
ui.update_server_info(game_state)
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^3dropped due to inactivity^7\n")
ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name}^0 ^3dropped due to inactivity^7\n")
return True
# Match renames: "OldName renamed to NewName"
@ -221,7 +221,7 @@ def parse_player_events(message, game_state, ui):
game_state.player_tracker.rename_player(old_name, new_name)
ui.update_server_info(game_state)
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{old_name}^9 ^6renamed to^7 ^0{new_name}^9\n")
ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{old_name} ^6renamed to^7 ^8{new_name}\n")
return True
except Exception as e:
@ -374,7 +374,7 @@ def main_loop(screen):
logger.info('Game initialization detected - refreshing server info')
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3] ^0^3Game initialized - Refreshing server info^9^7\n")
ui.print_message(f"^3[^7{timestamp}^3] ^8^3Game initialized - Refreshing server info^0^7\n")
rcon.send_command(b'qlx_serverBrandName')
rcon.send_command(b'g_factoryTitle')

View File

@ -104,17 +104,17 @@ class EventParser:
if team == old_team:
return None
warmup = " ^0^3(Warmup)^9" if data.get('WARMUP', False) else ""
warmup = " ^8^3(Warmup)^0" if data.get('WARMUP', False) else ""
team_messages = {
'FREE': ' ^7joined the ^0fight^9',
'FREE': ' ^7joined the ^8fight^0',
'SPECTATOR': ' ^7joined the ^3Spectators^7',
'RED': ' ^7joined the ^1RED Team^7',
'BLUE': ' ^7joined the ^4BLUE Team^7'
}
old_team_messages = {
'FREE': 'the ^0fight^9',
'FREE': 'the ^8fight^0',
'SPECTATOR': 'the ^3Spectators^7',
'RED': '^7the ^1RED Team^7',
'BLUE': '^7the ^4BLUE Team^7'
@ -124,7 +124,7 @@ class EventParser:
old_team_msg = old_team_messages.get(old_team, f'team {old_team}')
team_prefix = get_team_prefix(name, self.game_state.player_tracker)
return f"{team_prefix}^0{name}^9{team_msg} from {old_team_msg}{warmup}\n"
return f"{team_prefix}^8{name}^0{team_msg} from {old_team_msg}{warmup}\n"
def _handle_death(self, data):
"""Handle PLAYER_DEATH and PLAYER_KILL events"""
@ -147,7 +147,7 @@ class EventParser:
self.game_state.player_tracker.add_player(victim_name)
victim_prefix = get_team_prefix(victim_name, self.game_state.player_tracker)
warmup = " ^0^3(Warmup)^9" if data.get('WARMUP', False) else ""
warmup = " ^8^3(Warmup)^0" if data.get('WARMUP', False) else ""
score_prefix = ""
# Environmental death (no killer)
@ -155,10 +155,10 @@ class EventParser:
# -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]^7^9 "
score_prefix = "^8^1[-1]^7^0 "
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^8%s^0 ^1DIED FROM %s^7")
if mod in DEATH_MESSAGES:
msg = msg_template % (victim_prefix, victim_name)
@ -183,18 +183,24 @@ class EventParser:
# -1 for suicide
if not data.get('WARMUP', False):
self.game_state.player_tracker.update_score(victim_name, -1)
score_prefix = "^0^1[-1]^7^9 "
score_prefix = "^8^1[-1]^7^0 "
weapon = killer.get('WEAPON', 'OTHER_WEAPON')
if weapon != 'OTHER_WEAPON':
if weapon == 'ROCKET':
return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7blew herself up.{warmup}\n"
elif weapon == 'GRENADE':
return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7tripped on her own grenade.{warmup}\n"
elif weapon == 'PLASMA':
return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7melted herself.{warmup}\n"
else:
weapon_name = WEAPON_NAMES.get(weapon, weapon)
return f"{score_prefix}{killer_prefix}^0{killer_name}^9 ^7committed suicide with the ^7{weapon_name}{warmup}\n"
return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7committed suicide with the ^7{weapon_name}{warmup}\n"
return None
# 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]^7^9 "
score_prefix = "^8^2[+1]^7^0 "
else:
score_prefix = ""
@ -202,7 +208,7 @@ class EventParser:
weapon = killer.get('WEAPON', 'UNKNOWN')
weapon_name = WEAPON_KILL_NAMES.get(weapon, f'the {weapon}')
return f"{score_prefix}{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}^8{killer_name}^0 ^7fragged^7 {victim_prefix}^8{victim_name}^0 ^7with {weapon_name}{warmup}\n"
def _handle_round_over(self, data):
"""Handle ROUND_OVER events for CA"""
@ -222,10 +228,29 @@ class EventParser:
"""Handle PLAYER_MEDAL event"""
name = data.get('NAME', 'Unknown')
medal = data.get('MEDAL', 'UNKNOWN')
warmup = " ^0^3(Warmup)^7^9" if data.get('WARMUP', False) else ""
warmup = " ^8^3(Warmup)^7^0" if data.get('WARMUP', False) else ""
team_prefix = get_team_prefix(name, self.game_state.player_tracker)
return f"{team_prefix}^0{name}^9 ^7got a medal: ^0^6{medal}{warmup}\n"
# RED Medals (^1)
if medal in ["FIRSTFRAG", "HUMILIATION", "REVENGE"]:
return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^1{medal}^0{warmup}\n"
# GREEN Medals (^2)
elif medal in ["MIDAIR", "PERFECT"]:
return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^2{medal}^0{warmup}\n"
# YELLOW Medals (^3)
elif medal in ["EXCELLENT", "HEADSHOT", "RAMPAGE"]:
return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^3{medal}^0{warmup}\n"
# BLUE Medals (^4)
elif medal in ["ASSIST", "DEFENSE", "QUADGOD"]:
return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^4{medal}^0{warmup}\n"
# CYAN Medals (^5)
elif medal in ["CAPTURE", "COMBOKILL", "IMPRESSIVE"]:
return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^5{medal}^0{warmup}\n"
# PINK Medals (^6)
elif medal in ["ACCURACY", "PERFORATED"]:
return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^6{medal}^0{warmup}\n"
else:
return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^7{medal}^0{warmup}\n"
def _handle_match_started(self, data):
"""Handle MATCH_STARTED event"""
@ -238,8 +263,8 @@ class EventParser:
players.append(name)
if players:
formatted = "^9 vs. ^0".join(players)
return f"^0^3Match has started - ^0^7{formatted}\n"
formatted = "^0 vs. ^8".join(players)
return f"^8^3Match has started - ^8^7{formatted}\n"
return None
@ -279,4 +304,4 @@ class EventParser:
weapon_name = WEAPON_NAMES.get(best_weapon, best_weapon)
return f"{team_prefix}^0^7{name}^9^7 K/D: {kills}/{deaths} | Best Weapon: {weapon_name} - Acc: {best_accuracy:.2f}% - Kills: {best_weapon_kills}\n"
return f"{team_prefix}^8^7{name}^0^7 K/D: {kills}/{deaths} | Best Weapon: {weapon_name} - Acc: {best_accuracy:.2f}% - Kills: {best_weapon_kills}\n"

50
ui.py
View File

@ -41,7 +41,7 @@ class CursesHandler(logging.Handler):
def print_colored(window, message, attributes=0):
"""
Print message with Quake color codes (^N)
^0 = bold, ^1 = red, ^2 = green, ^3 = yellow, ^4 = blue, ^5 = cyan, ^6 = magenta, ^7 = white, ^9 = reset
^0 = reset, ^1 = red, ^2 = green, ^3 = yellow, ^4 = blue, ^5 = cyan, ^6 = magenta, ^7 = white, ^8 = bold, ^9 = underline
"""
if not curses.has_colors:
window.addstr(message)
@ -49,27 +49,31 @@ def print_colored(window, message, attributes=0):
color = 0
bold = False
underline = False
parse_color = False
for ch in message:
val = ord(ch)
if parse_color:
if ch == '0':
if ch == '8':
bold = True
elif ch == '9':
underline = True
elif ch == '0':
bold = False
underline = False
elif ch == '7':
color = 0
elif ord('1') <= val <= ord('6'):
color = val - ord('0')
else:
window.addch('^', curses.color_pair(color) | (curses.A_BOLD if bold else 0) | attributes)
window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | attributes)
window.addch('^', curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes)
window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes)
parse_color = False
elif ch == '^':
parse_color = True
else:
window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | attributes)
window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes)
class UIManager:
"""Manages curses windows and display"""
@ -307,8 +311,8 @@ class UIManager:
# Line 1: Hostname
hostname = server_info.hostname
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)
warmup_display = "^3Warmup: ^2YES^0" if server_info.warmup else "^3Warmup: ^1NO^0"
print_colored(self.info_window, f"^3Name:^8 {hostname} {warmup_display}\n", 0)
# Line 2: Game info
gametype = server_info.gametype
@ -317,12 +321,24 @@ class UIManager:
fraglimit = server_info.fraglimit
roundlimit = server_info.roundlimit
caplimit = server_info.capturelimit
curclients = server_info.curclients
curclients = len(server_info.players)
maxclients = server_info.maxclients
# Context-sensitive limit display based on gametype
if gametype == 'Capture the Flag':
limit_display = f"^3^0| Capturelimit:^7^8 {caplimit}"
elif gametype == 'Clan Arena':
limit_display = f"^3^0| Roundlimit:^7^8 {roundlimit}"
elif gametype == 'Duel':
limit_display = f"^3^0| Timelimit:^7^8 {timelimit}"
elif gametype == 'Race':
limit_display = f"^3^0| Timelimit:^7^8 {timelimit}"
else:
limit_display = f"^3^0| Timelimit:^7^8 {timelimit} ^0^3| Fraglimit:^7^8 {fraglimit}"
print_colored(self.info_window,
f"^3^9Type:^7^0 {gametype} ^9^3| Map:^7^0 {mapname} ^9^3| Players:^7^0 {curclients}/{maxclients} "
f"^3^9| Limits (T/F/R/C):^7^0 {timelimit}/{fraglimit}/{roundlimit}/{caplimit}^9\n", 0)
print_colored(self.info_window,
f"^3^0Type:^7^8 {gametype} ^8^3| Map:^7^8 {mapname} ^0^3| Players:^7^8 {curclients}/{maxclients} "
f"{limit_display}^0\n", 0)
# Blank lines to fill
self.info_window.addstr("\n")
@ -351,7 +367,7 @@ class UIManager:
red_score = f"{red_total:>3} "
blue_score = f"{blue_total:>3} "
print_colored(self.info_window, f"^0^7{red_score}^1RED TEAM ^7{blue_score}^4BLUE TEAM\n", 0)
print_colored(self.info_window, f"^8^7{red_score}^9^1RED TEAM^0 ^7^8{blue_score}^9^4BLUE TEAM\n", 0)
# Sort players by score within each team
red_players_with_scores = []
@ -410,10 +426,10 @@ class UIManager:
red_pad = 24 - len(red_clean)
line = f"^0{red}^9{' ' * red_pad}^0{blue}^9\n"
line = f"^8{red}^0{' ' * red_pad}^8{blue}^0\n"
print_colored(self.info_window, line, 0)
else:
print_colored(self.info_window, f"^0 ^5FREE\n", 0)
print_colored(self.info_window, f" ^8^9^5FREE\n", 0)
# Sort FREE players by score (highest first)
free_players = teams['FREE']
free_players_with_scores = []
@ -461,7 +477,7 @@ class UIManager:
col1_pad = 24 - len(col1_clean)
line = f"^0{col1}^9{' ' * col1_pad}^0{col2}^9\n"
line = f"^8{col1}^0{' ' * col1_pad}^8{col2}^0\n"
print_colored(self.info_window, line, 0)
# Blank lines to fill
@ -469,7 +485,7 @@ class UIManager:
# List spectators on one line
spec_list = " ".join(spec_players)
line = f"^0 ^3SPEC^7 {spec_list}\n"
line = f" ^8^3SPEC^0:^7^8 {spec_list}\n"
print_colored(self.info_window, line, 0)
# Blank lines to fill
@ -480,3 +496,5 @@ class UIManager:
print_colored(self.info_window, separator, 0)
self.info_window.noutrefresh()
curses.doupdate()