lesssgo
This commit is contained in:
245
qlpycon.py
245
qlpycon.py
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# Version: 1.14.0
|
||||
|
||||
import sys
|
||||
import re
|
||||
@ -76,6 +77,15 @@ def _readSocketEvent( msg ):
|
||||
event_value = struct.unpack( '<I', msg[2:] )[0]
|
||||
return ( event_id, event_name, event_value )
|
||||
|
||||
def calculate_weapon_accuracies(weapon_data):
|
||||
accuracies = {}
|
||||
for weapon, stats in weapon_data.items():
|
||||
shots_fired = int(stats.get('S', 0))
|
||||
shots_hit = int(stats.get('H', 0))
|
||||
accuracy = shots_hit / shots_fired if shots_fired > 0 else 0
|
||||
accuracies[weapon] = accuracy
|
||||
return accuracies
|
||||
|
||||
def _checkMonitor( monitor ):
|
||||
try:
|
||||
event_monitor = monitor.recv( zmq.NOBLOCK )
|
||||
@ -162,8 +172,59 @@ def PrintMessageFormatted(window, message, add_timestamp=True):
|
||||
if message[:7] == "print \"":
|
||||
message = message[7:-2] + "\n"
|
||||
|
||||
# Add timestamp if requested
|
||||
if add_timestamp and not message.startswith('***'):
|
||||
# Don't add timestamp to server responses, especially status command
|
||||
# But DO add timestamps to "zmq RCON command" lines (server acknowledgments)
|
||||
skip_timestamp_keywords = [
|
||||
'map:', 'num score', '---', 'bot'
|
||||
]
|
||||
|
||||
# Special handling: don't skip "zmq RCON" lines - they should get timestamps
|
||||
# But skip everything else from status output
|
||||
is_status_keyword = any(keyword in message for keyword in skip_timestamp_keywords)
|
||||
|
||||
# Also don't timestamp "status" when it appears in isolation (status table context)
|
||||
# But DO timestamp it when it's part of "zmq RCON command...status" (the echo line)
|
||||
if 'status' in message and 'zmq RCON' not in message:
|
||||
is_status_keyword = True
|
||||
|
||||
# Skip very short messages or messages with leading spaces (status fragments)
|
||||
is_likely_fragment = (
|
||||
len(message.strip()) <= 2 or # Very short messages
|
||||
(message.startswith(' ') and len(message.strip()) < 50) or # Messages with leading space
|
||||
message.strip().isdigit() # Pure numbers (scores, pings, ports, etc)
|
||||
)
|
||||
|
||||
# Skip single words that are less than 20 chars (likely player names or status fragments)
|
||||
is_short_word = len(message.strip()) < 20 and ' ' not in message.strip()
|
||||
|
||||
# Skip IP addresses (pattern: xxx.xxx.xxx.xxx or xxx.xxx.xxx.xxx:port)
|
||||
is_ip_address = False
|
||||
stripped = message.strip()
|
||||
allowed_chars = set('0123456789.:')
|
||||
if stripped.count('.') == 3 and all(c in allowed_chars for c in stripped):
|
||||
is_ip_address = True
|
||||
|
||||
should_skip_timestamp = (
|
||||
message.startswith('***') or
|
||||
is_likely_fragment or
|
||||
is_short_word or
|
||||
is_ip_address or
|
||||
is_status_keyword
|
||||
)
|
||||
|
||||
# ADD THIS DEBUG BLOCK HERE:
|
||||
if 'K/D:' in message or 'WINS' in message or 'TEAM' in message:
|
||||
logger.debug(f'=== TIMESTAMP DEBUG ===')
|
||||
logger.debug(f'Message: {message[:100]}')
|
||||
logger.debug(f'is_status_keyword: {is_status_keyword}')
|
||||
logger.debug(f'is_likely_fragment: {is_likely_fragment}')
|
||||
logger.debug(f'is_short_word: {is_short_word}')
|
||||
logger.debug(f'is_ip_address: {is_ip_address}')
|
||||
logger.debug(f'should_skip_timestamp: {should_skip_timestamp}')
|
||||
logger.debug(f'add_timestamp: {add_timestamp}')
|
||||
# Add timestamp if requested and not a special message type
|
||||
|
||||
if add_timestamp and not should_skip_timestamp:
|
||||
timestamp = time.strftime('%H:%M:%S')
|
||||
message = f"^3[^7{timestamp}^3]^7 {message}"
|
||||
|
||||
@ -268,12 +329,12 @@ def ParseGameEvent(message):
|
||||
if 'KILLER' not in data or not data['KILLER']:
|
||||
mod = data.get('MOD', 'UNKNOWN')
|
||||
death_msgs = {
|
||||
'FALLING': "%s%s ^1CRATERED^7",
|
||||
'HURT': "%s%s ^1WAS IN THE WRONG PLACE^7",
|
||||
'LAVA': "%s%s ^1DOES A BACKFLIP INTO THE LAVA^7",
|
||||
'WATER': "%s%s ^1SANK LIKE A ROCK^7",
|
||||
'SLIME': "%s%s ^1MELTED^7",
|
||||
'CRUSH': "%s%s ^1WAS CRUSHED^7"
|
||||
'FALLING': "%s%s ^7cratered.",
|
||||
'HURT': "%s%s ^7was in the wrong place.",
|
||||
'LAVA': "%s%s ^7does a backflip into the lava.",
|
||||
'WATER': "%s%s ^7sank like a rock.",
|
||||
'SLIME': "%s%s ^7melted.",
|
||||
'CRUSH': "%s%s ^7was crushed."
|
||||
}
|
||||
msg_template = death_msgs.get(mod, "%s%s ^1DIED FROM %s^7")
|
||||
if mod in death_msgs:
|
||||
@ -305,7 +366,7 @@ def ParseGameEvent(message):
|
||||
}
|
||||
weapon_name = weapon_names.get(weapon, 'the %s' % weapon)
|
||||
|
||||
return "%s%s ^5killed %s%s ^7with %s%s\n" % (
|
||||
return "%s%s ^8fragged^7 %s%s ^7with %s%s\n" % (
|
||||
killer_team_prefix, killer_name,
|
||||
victim_team_prefix, victim_name,
|
||||
weapon_name, warmup_suffix
|
||||
@ -321,18 +382,60 @@ def ParseGameEvent(message):
|
||||
'GRENADE': 'Grenade Launcher'
|
||||
}
|
||||
weapon_name = weapon_names.get(weapon, weapon)
|
||||
return "%s%s ^7committed suicide with the ^1%s%s\n" % (
|
||||
return "%s%s ^7committed suicide with the ^7%s%s\n" % (
|
||||
killer_team_prefix, killer_name, weapon_name, warmup_suffix
|
||||
)
|
||||
|
||||
|
||||
elif event_type == 'PLAYER_MEDAL':
|
||||
name = data.get('NAME', 'Unknown')
|
||||
medal = data.get('MEDAL', 'UNKNOWN')
|
||||
warmup = data.get('WARMUP', False)
|
||||
warmup_suffix = " ^7(^3warmup^7)" if warmup else ""
|
||||
warmup_suffix = " ^3(warmup)^7" if warmup else ""
|
||||
team_prefix = get_team_color(name)
|
||||
return "%s%s ^7got a ^6%s ^7medal%s\n" % (team_prefix, name, medal, warmup_suffix)
|
||||
|
||||
return "%s%s ^7got a medal: ^6%s%s\n" % (team_prefix, name, medal, warmup_suffix)
|
||||
|
||||
elif event_type == 'MATCH_REPORT':
|
||||
if current_gametype in team_modes:
|
||||
redscore = int(data.get('TSCORE0', '0'))
|
||||
bluscore = int(data.get('TSCORE1', '0'))
|
||||
if redscore > bluscore:
|
||||
return "^1RED TEAM ^7WINS by a score of %d to %d\n" % (redscore, bluscore)
|
||||
elif bluscore > redscore:
|
||||
return "^4BLUE TEAM ^7WINS by a score of %d to %d\n" % (bluscore, redscore)
|
||||
else:
|
||||
return "^7The match is a TIE with a score of %d to %d\n" % (redscore, bluscore)
|
||||
else:
|
||||
return None
|
||||
|
||||
elif event_type == 'PLAYER_STATS':
|
||||
name = data.get('NAME', 'Unknown')
|
||||
team_prefix = get_team_color(name)
|
||||
kills = int(data.get('KILLS', '0'))
|
||||
deaths = int(data.get('DEATHS', '0'))
|
||||
weapon_data = data.get('WEAPONS', {})
|
||||
accuracies = calculate_weapon_accuracies(weapon_data)
|
||||
if accuracies:
|
||||
best_weapon = max(accuracies, key=accuracies.get)
|
||||
best_accuracy = accuracies[best_weapon] * 100
|
||||
weapon_stats = weapon_data.get(best_weapon, {})
|
||||
best_weapon_kills = int(weapon_stats.get('K', 0))
|
||||
best_weapon_stats = f"{best_weapon}: {best_accuracy:.2f}% (Kills: {best_weapon_kills})"
|
||||
else:
|
||||
best_weapon_stats = "No weapon stats available."
|
||||
weapon_names = {
|
||||
'ROCKET': 'Rocket Launcher',
|
||||
'LIGHTNING': 'Lightning Gun',
|
||||
'RAILGUN': 'Railgun',
|
||||
'SHOTGUN': 'Shotgun',
|
||||
'GAUNTLET': 'Gauntlet',
|
||||
'GRENADE': 'Grenade Launcher',
|
||||
'PLASMA': 'Plasma Gun',
|
||||
'MACHINEGUN': 'Machine Gun'
|
||||
}
|
||||
weapon_name = weapon_names.get(best_weapon)
|
||||
return "^7%s%s K/D: %d/%d | Best Weapon: %s - Acc: %.2f%% - Kills: %d\n" % (team_prefix, name, kills, deaths, weapon_name, best_accuracy, best_weapon_kills)
|
||||
#return "^7%s%s K/D: %d/%d\n" % (team_prefix, name, kills, deaths)
|
||||
|
||||
else:
|
||||
# Unknown event type - log at debug level and to file
|
||||
logger.debug('Unknown event type: %s' % event_type)
|
||||
@ -433,7 +536,7 @@ def main(screen):
|
||||
|
||||
input_window, output_window = InitWindows(screen, args)
|
||||
|
||||
PrintMessageFormatted(output_window, "*** QL pyCon Version 1.3.0 starting ***\n")
|
||||
PrintMessageFormatted(output_window, "*** QL pyCon Version 1.13.0 starting ***\n")
|
||||
PrintMessageFormatted(output_window, "zmq python bindings {}, libzmq version {}\n".format(repr(zmq.__version__), zmq.zmq_version()))
|
||||
|
||||
stats_port = None
|
||||
@ -477,6 +580,11 @@ def main(screen):
|
||||
while ( not q.empty() ):
|
||||
l = q.get()
|
||||
logger.info( 'Sending command: %s' % repr( l.strip() ) )
|
||||
|
||||
# Display the command being sent with timestamp (in cyan to differentiate)
|
||||
timestamp = time.strftime('%H:%M:%S')
|
||||
PrintMessageFormatted(output_window, f"^5[^7{timestamp}^5] >>> {l.strip()}^7\n", add_timestamp=False)
|
||||
|
||||
socket.send( l.encode('utf-8') )
|
||||
|
||||
if stats_connected and stats_socket:
|
||||
@ -519,6 +627,8 @@ def main(screen):
|
||||
logger.debug( 'Read %d message(s) from socket.' % msg_count )
|
||||
break
|
||||
except Exception as e:
|
||||
timestamp = time.strftime('%H:%M:%S')
|
||||
PrintMessageFormatted(output_window, f"^1[^7{timestamp}^1] Error: {e}^7\n", add_timestamp=False)
|
||||
logger.error( 'Error receiving message: %s' % e )
|
||||
break
|
||||
else:
|
||||
@ -580,6 +690,8 @@ def main(screen):
|
||||
stats_connected = True
|
||||
PrintMessageFormatted(output_window, "Stats stream connected - ready for game events\n")
|
||||
except Exception as e:
|
||||
timestamp = time.strftime('%H:%M:%S')
|
||||
PrintMessageFormatted(output_window, f"^1[^7{timestamp}^1] Error: Stats connection failed: {e}^7\n", add_timestamp=False)
|
||||
logger.error('Stats connection failed: %s' % e)
|
||||
import traceback
|
||||
logger.debug(traceback.format_exc())
|
||||
@ -616,32 +728,95 @@ def main(screen):
|
||||
# It's JSON but we didn't parse it - already logged to file
|
||||
logger.debug('Unparsed JSON event')
|
||||
else:
|
||||
# Not JSON - check if it's a chat message
|
||||
# Not JSON - check if it's a bot debug message (filter unless verbose)
|
||||
is_bot_debug = ' entered ' in msg_str and (' seek ' in msg_str or ' battle ' in msg_str or ' chase' in msg_str or ' fight' in msg_str)
|
||||
|
||||
if is_bot_debug and args.verbose == 0:
|
||||
# Skip bot debug messages in default mode
|
||||
logger.debug('Filtered bot debug message: %s' % msg_str[:50])
|
||||
continue
|
||||
|
||||
# Check if it's a chat message
|
||||
if ':' in msg_str and not msg_str.startswith(('print', 'broadcast', 'zmq')):
|
||||
# Strip the special character first to get clean player name
|
||||
clean_msg = msg_str.replace(chr(25), '')
|
||||
|
||||
# Check if it's team chat (PlayerName): or regular chat PlayerName:
|
||||
if clean_msg.strip().startswith('(') and ')' in clean_msg:
|
||||
# Team chat: (PlayerName): message
|
||||
match = re.match(r'^(\([^)]+\):)(\s*.*)', clean_msg)
|
||||
if match:
|
||||
player_part = match.group(1) # (PlayerName):
|
||||
message_part = match.group(2) # message text
|
||||
# Team chat: (PlayerName) (Location): message or (PlayerName): message
|
||||
# Location can contain nested parens like (Lower Floor (Near Yellow Armour))
|
||||
# We need to match the first (PlayerName), then optionally a second parenthetical with potential nesting
|
||||
|
||||
# First, extract player name (first parenthetical)
|
||||
player_match = re.match(r'^(\([^)]+\))', clean_msg)
|
||||
if player_match:
|
||||
player_part = player_match.group(1)
|
||||
rest_of_msg = clean_msg[len(player_part):].lstrip()
|
||||
|
||||
# Extract just the player name for team lookup
|
||||
name_match = re.match(r'\(([^)]+)\)', player_part)
|
||||
if name_match:
|
||||
player_name = name_match.group(1).strip()
|
||||
player_name_clean = re.sub(r'\^\d', '', player_name)
|
||||
# Check if there's a location (starts with another opening paren)
|
||||
if rest_of_msg.startswith('('):
|
||||
# Find the matching closing paren for the location
|
||||
# We need to count parens to handle nesting like (Floor (Near Armor))
|
||||
paren_count = 0
|
||||
location_end = -1
|
||||
for i, char in enumerate(rest_of_msg):
|
||||
if char == '(':
|
||||
paren_count += 1
|
||||
elif char == ')':
|
||||
paren_count -= 1
|
||||
if paren_count == 0:
|
||||
location_end = i + 1
|
||||
break
|
||||
|
||||
# Look up player by clean name and add team prefix
|
||||
team_prefix = ''
|
||||
if player_name_clean in player_teams:
|
||||
team_prefix = get_team_color(player_name_clean)
|
||||
|
||||
# Reconstruct with team prefix and colored message (^5 for team chat)
|
||||
msg_str = f"{team_prefix}{player_part}^5{message_part}\n"
|
||||
if location_end > 0 and location_end < len(rest_of_msg) and rest_of_msg[location_end] == ':':
|
||||
# We found a valid location with closing paren followed by colon
|
||||
location_part = rest_of_msg[:location_end]
|
||||
message_part = rest_of_msg[location_end + 1:] # After the colon
|
||||
|
||||
# Extract player name for team lookup
|
||||
name_match = re.match(r'\(([^)]+)\)', player_part)
|
||||
if name_match:
|
||||
player_name = name_match.group(1).strip()
|
||||
player_name_clean = re.sub(r'\^\d', '', player_name)
|
||||
|
||||
team_prefix = ''
|
||||
if player_name_clean in player_teams:
|
||||
team_prefix = get_team_color(player_name_clean)
|
||||
|
||||
# Strip color codes from location for consistent yellow coloring
|
||||
location_clean = re.sub(r'\^\d', '', location_part)
|
||||
msg_str = f"{team_prefix}{player_part} ^3{location_clean}^7:^5{message_part}"
|
||||
else:
|
||||
# No valid location, treat as regular team chat
|
||||
if rest_of_msg.startswith(':'):
|
||||
message_part = rest_of_msg[1:]
|
||||
name_match = re.match(r'\(([^)]+)\)', player_part)
|
||||
if name_match:
|
||||
player_name = name_match.group(1).strip()
|
||||
player_name_clean = re.sub(r'\^\d', '', player_name)
|
||||
|
||||
team_prefix = ''
|
||||
if player_name_clean in player_teams:
|
||||
team_prefix = get_team_color(player_name_clean)
|
||||
|
||||
msg_str = f"{team_prefix}{player_part}^5:{message_part}"
|
||||
else:
|
||||
# No location, check if it's just (PlayerName): message
|
||||
colon_match = re.match(r'^(\([^)]+\)):(\s*.*)', clean_msg)
|
||||
if colon_match:
|
||||
player_part = colon_match.group(1) + ':'
|
||||
message_part = colon_match.group(2)
|
||||
|
||||
name_match = re.match(r'\(([^)]+)\)', player_part)
|
||||
if name_match:
|
||||
player_name = name_match.group(1).strip()
|
||||
player_name_clean = re.sub(r'\^\d', '', player_name)
|
||||
|
||||
team_prefix = ''
|
||||
if player_name_clean in player_teams:
|
||||
team_prefix = get_team_color(player_name_clean)
|
||||
|
||||
msg_str = f"{team_prefix}{player_part}^5{message_part}\n"
|
||||
else:
|
||||
# Regular chat: PlayerName: message
|
||||
parts = clean_msg.split(':', 1)
|
||||
@ -674,6 +849,8 @@ def main(screen):
|
||||
except KeyboardInterrupt:
|
||||
PrintMessageFormatted(output_window, "\nShutting down...\n")
|
||||
except Exception as e:
|
||||
timestamp = time.strftime('%H:%M:%S')
|
||||
PrintMessageFormatted(output_window, f"^1[^7{timestamp}^1] Fatal Error: {e}^7\n", add_timestamp=False)
|
||||
logger.error( 'Fatal error: %s' % e )
|
||||
import traceback
|
||||
logger.error( traceback.format_exc() )
|
||||
@ -686,4 +863,4 @@ def main(screen):
|
||||
if ( __name__ == '__main__' ):
|
||||
curses.wrapper(main)
|
||||
|
||||
# Version: 1.8.0 - Added color coding to chat messages (^2 for regular, ^5 for team chat)
|
||||
# Version: 1.13.0
|
||||
|
||||
Reference in New Issue
Block a user