diff --git a/qlpycon.py b/qlpycon.py index 8cb9a89..9fbd34d 100644 --- a/qlpycon.py +++ b/qlpycon.py @@ -15,6 +15,10 @@ import logging logger = logging.getLogger('logger') logger.setLevel(logging.DEBUG) +# Separate logger for all JSON events when --json flag is used +all_json_logger = logging.getLogger('all_json') +all_json_logger.setLevel(logging.DEBUG) + # Separate logger for unknown JSON events unknown_json_logger = logging.getLogger('unknown_json') unknown_json_logger.setLevel(logging.DEBUG) @@ -246,12 +250,80 @@ def get_team_color(player_name): 'SPECTATOR': '^3(SPEC)^7' }.get(team, '') +def format_powerup_message(msg_str): + """Format powerup pickup and kill messages with colors""" + + if msg_str.startswith("broadcast:"): + msg_str = msg_str[11:].strip() + + # Check for powerup pickup: "PlayerName got the PowerupName!" + pickup_match = re.match(r'^(.+?)\s+got the\s+(.+?)!', msg_str) + if pickup_match: + player_name = pickup_match.group(1).strip() + powerup_name = pickup_match.group(2).strip() + + # Get team color for player + player_clean = re.sub(r'\^\d', '', player_name) + team_prefix = get_team_color(player_clean) + + # Color code powerups + 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' + } + + colored_powerup = powerup_colors.get(powerup_name, f'^6{powerup_name}^7') + + return f"{team_prefix}{player_name} ^7got the {colored_powerup}!\n" + + # Check for powerup carrier kill: "PlayerName killed the PowerupName carrier!" + carrier_match = re.match(r'^(.+?)\s+killed the\s+(.+?)\s+carrier!', msg_str) + if carrier_match: + player_name = carrier_match.group(1).strip() + powerup_name = carrier_match.group(2).strip() + + # Get team color for player + player_clean = re.sub(r'\^\d', '', player_name) + team_prefix = get_team_color(player_clean) + + # Color code powerups (same as above) + 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' + } + + colored_powerup = powerup_colors.get(powerup_name, f'^6{powerup_name}^7') + + return f"{team_prefix}{player_name} ^7killed the {colored_powerup} ^7carrier!\n" + + # Not a powerup message + return None + def ParseGameEvent(message): """Parse JSON game events and return formatted message""" global current_gametype, recent_events try: jObject = json.loads(message) + + # Log ALL JSON events to file if --json flag was provided + if all_json_logger.handlers: + all_json_logger.info('JSON Event received:') + all_json_logger.info(json.dumps(jObject, indent=2)) + all_json_logger.info('---') + if 'TYPE' not in jObject or 'DATA' not in jObject: logger.debug('JSON missing TYPE or DATA fields') return None @@ -279,7 +351,7 @@ def ParseGameEvent(message): recent_events.pop(0) warmup = data.get('WARMUP', False) - warmup_suffix = " ^7(^3warmup^7)" if warmup else "" + warmup_suffix = " ^3(warmup)" if warmup else "" if event_type == 'PLAYER_SWITCHTEAM': if 'KILLER' not in data: @@ -366,7 +438,7 @@ def ParseGameEvent(message): } weapon_name = weapon_names.get(weapon, 'the %s' % weapon) - return "%s%s ^8fragged^7 %s%s ^7with %s%s\n" % ( + return "%s%s ^7fragged^7 %s%s ^7with %s%s\n" % ( killer_team_prefix, killer_name, victim_team_prefix, victim_name, weapon_name, warmup_suffix @@ -459,6 +531,7 @@ def InitWindows(screen, args): curses.initscr() screen.nodelay(1) curses.start_color() + curses.use_default_colors() curses.cbreak() curses.setsyx(-1, -1) screen.addstr("Quake Live rcon: %s" % args.host) @@ -514,6 +587,7 @@ def main(screen): parser.add_argument( '--identity', default = uuid.uuid1().hex, help = 'Specify the socket identity. Random UUID used by default' ) parser.add_argument( '-v', '--verbose', action='count', default=0, help = 'Increase verbosity (use -v for INFO, -vv for DEBUG)' ) parser.add_argument( '--unknown-log', default='unknown_events.log', help = 'File to log unknown JSON events. Defaults to unknown_events.log' ) + parser.add_argument('--json', '-j', dest='json_log', default=None, help='File to log all JSON events. If specified, all JSON messages from server will be captured') args = parser.parse_args() # Set logging level based on verbosity @@ -689,6 +763,15 @@ def main(screen): stats_connected = True PrintMessageFormatted(output_window, "Stats stream connected - ready for game events\n") + + # Set up file handler for all JSON events if --json flag is provided + if args.json_log: + json_file_handler = logging.FileHandler(args.json_log, mode='a') + json_file_formatter = logging.Formatter('%(asctime)s - %(message)s', '%Y-%m-%d %H:%M:%S') + json_file_handler.setFormatter(json_file_formatter) + all_json_logger.addHandler(json_file_handler) + all_json_logger.propagate = False + PrintMessageFormatted(output_window, f"*** JSON capture enabled: {args.json_log} ***\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) @@ -728,6 +811,13 @@ def main(screen): # It's JSON but we didn't parse it - already logged to file logger.debug('Unparsed JSON event') else: + # Check for powerup messages + formatted_powerup = format_powerup_message(msg_str) + if formatted_powerup: + PrintMessageFormatted(output_window, formatted_powerup) + curses.setsyx(y,x) + curses.doupdate() + continue # 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)