""" Event Processor - Handles kill and bomb events Responsibilities: - Process leetify show_event data (kills with score impacts) - Process classic all_kill and c4_event data - Generate unique event_ids - Store twin probability changes (leetify only) - Handle bomb plant/defuse events """ import sqlite3 import json import logging import uuid logger = logging.getLogger(__name__) class EventProcessor: @staticmethod def process_leetify_events(match_data, conn: sqlite3.Connection) -> bool: """ Process leetify event data Args: match_data: MatchData object with leetify_data parsed conn: L2 database connection Returns: bool: True if successful """ try: if not hasattr(match_data, 'data_leetify') or not match_data.data_leetify: return True leetify_data = match_data.data_leetify.get('leetify_data', {}) round_stats = leetify_data.get('round_stat', []) if not round_stats: return True cursor = conn.cursor() event_count = 0 for r in round_stats: round_num = r.get('round', 0) show_events = r.get('show_event', []) for evt in show_events: event_type_code = evt.get('event_type', 0) # event_type: 3=kill, others for bomb/etc if event_type_code == 3 and evt.get('kill_event'): # Process kill event k = evt['kill_event'] event_id = str(uuid.uuid4()) event_time = evt.get('ts', 0) attacker_steam_id = str(k.get('Killer', '')) victim_steam_id = str(k.get('Victim', '')) weapon = k.get('WeaponName', '') is_headshot = bool(k.get('Headshot', False)) is_wallbang = bool(k.get('Penetrated', False)) is_blind = bool(k.get('AttackerBlind', False)) is_through_smoke = bool(k.get('ThroughSmoke', False)) is_noscope = bool(k.get('NoScope', False)) # Extract assist info assister_steam_id = None flash_assist_steam_id = None trade_killer_steam_id = None if evt.get('assist_killer_score_change'): assister_steam_id = str(list(evt['assist_killer_score_change'].keys())[0]) if evt.get('flash_assist_killer_score_change'): flash_assist_steam_id = str(list(evt['flash_assist_killer_score_change'].keys())[0]) if evt.get('trade_score_change'): trade_killer_steam_id = str(list(evt['trade_score_change'].keys())[0]) # Extract score changes score_change_attacker = 0.0 score_change_victim = 0.0 if evt.get('killer_score_change'): vals = list(evt['killer_score_change'].values()) if vals and isinstance(vals[0], dict): score_change_attacker = float(vals[0].get('score', 0)) if evt.get('victim_score_change'): vals = list(evt['victim_score_change'].values()) if vals and isinstance(vals[0], dict): score_change_victim = float(vals[0].get('score', 0)) # Extract twin (team win probability) changes twin = evt.get('twin', 0.0) c_twin = evt.get('c_twin', 0.0) twin_change = evt.get('twin_change', 0.0) c_twin_change = evt.get('c_twin_change', 0.0) cursor.execute(''' INSERT OR REPLACE INTO fact_round_events ( event_id, match_id, round_num, event_type, event_time, attacker_steam_id, victim_steam_id, assister_steam_id, flash_assist_steam_id, trade_killer_steam_id, weapon, is_headshot, is_wallbang, is_blind, is_through_smoke, is_noscope, score_change_attacker, score_change_victim, twin, c_twin, twin_change, c_twin_change, data_source_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( event_id, match_data.match_id, round_num, 'kill', event_time, attacker_steam_id, victim_steam_id, assister_steam_id, flash_assist_steam_id, trade_killer_steam_id, weapon, is_headshot, is_wallbang, is_blind, is_through_smoke, is_noscope, score_change_attacker, score_change_victim, twin, c_twin, twin_change, c_twin_change, 'leetify' )) event_count += 1 logger.debug(f"Processed {event_count} leetify events for match {match_data.match_id}") return True except Exception as e: logger.error(f"Error processing leetify events for match {match_data.match_id}: {e}") import traceback traceback.print_exc() return False @staticmethod def process_classic_events(match_data, conn: sqlite3.Connection) -> bool: """ Process classic event data (all_kill, c4_event) Args: match_data: MatchData object with round_list parsed conn: L2 database connection Returns: bool: True if successful """ try: if not hasattr(match_data, 'data_round_list') or not match_data.data_round_list: return True round_list = match_data.data_round_list.get('round_list', []) if not round_list: return True cursor = conn.cursor() event_count = 0 for idx, rd in enumerate(round_list, start=1): round_num = idx # Extract round basic info for fact_rounds current_score = rd.get('current_score', {}) ct_score = current_score.get('ct', 0) t_score = current_score.get('t', 0) win_type = current_score.get('type', 0) pasttime = current_score.get('pasttime', 0) final_round_time = current_score.get('final_round_time', 0) # Determine winner_side from win_type winner_side = _map_win_type_to_side(win_type) # Insert/update fact_rounds cursor.execute(''' INSERT OR REPLACE INTO fact_rounds ( match_id, round_num, winner_side, win_reason, win_reason_desc, duration, ct_score, t_score, end_time_stamp, final_round_time, pasttime, data_source_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( match_data.match_id, round_num, winner_side, win_type, _map_win_type_desc(win_type), float(pasttime), ct_score, t_score, '', final_round_time, pasttime, 'classic' )) # Process kill events all_kill = rd.get('all_kill', []) for kill in all_kill: event_id = str(uuid.uuid4()) event_time = kill.get('pasttime', 0) attacker = kill.get('attacker', {}) victim = kill.get('victim', {}) attacker_steam_id = str(attacker.get('steamid_64', '')) victim_steam_id = str(victim.get('steamid_64', '')) weapon = kill.get('weapon', '') is_headshot = bool(kill.get('headshot', False)) is_wallbang = bool(kill.get('penetrated', False)) is_blind = bool(kill.get('attackerblind', False)) is_through_smoke = bool(kill.get('throughsmoke', False)) is_noscope = bool(kill.get('noscope', False)) # Classic has spatial data - will be filled by spatial_processor # But we still need to insert the event cursor.execute(''' INSERT OR REPLACE INTO fact_round_events ( event_id, match_id, round_num, event_type, event_time, attacker_steam_id, victim_steam_id, weapon, is_headshot, is_wallbang, is_blind, is_through_smoke, is_noscope, data_source_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( event_id, match_data.match_id, round_num, 'kill', event_time, attacker_steam_id, victim_steam_id, weapon, is_headshot, is_wallbang, is_blind, is_through_smoke, is_noscope, 'classic' )) event_count += 1 # Process bomb events c4_events = rd.get('c4_event', []) for c4 in c4_events: event_id = str(uuid.uuid4()) event_name = c4.get('event_name', '') event_time = c4.get('pasttime', 0) steam_id = str(c4.get('steamid_64', '')) # Map event_name to event_type if 'plant' in event_name.lower(): event_type = 'bomb_plant' attacker_steam_id = steam_id victim_steam_id = None elif 'defuse' in event_name.lower(): event_type = 'bomb_defuse' attacker_steam_id = steam_id victim_steam_id = None else: event_type = 'unknown' attacker_steam_id = steam_id victim_steam_id = None cursor.execute(''' INSERT OR REPLACE INTO fact_round_events ( event_id, match_id, round_num, event_type, event_time, attacker_steam_id, victim_steam_id, data_source_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', ( event_id, match_data.match_id, round_num, event_type, event_time, attacker_steam_id, victim_steam_id, 'classic' )) event_count += 1 logger.debug(f"Processed {event_count} classic events for match {match_data.match_id}") return True except Exception as e: logger.error(f"Error processing classic events for match {match_data.match_id}: {e}") import traceback traceback.print_exc() return False def _map_win_type_to_side(win_type): """Map win_type to winner_side for classic data""" # Based on CS:GO win types t_win_types = {1, 8, 12, 17} ct_win_types = {2, 7, 9, 11} if win_type in t_win_types: return 'T' elif win_type in ct_win_types: return 'CT' else: return 'None' def _map_win_type_desc(win_type): """Map win_type to description""" type_map = { 0: 'None', 1: 'TargetBombed', 2: 'BombDefused', 7: 'CTsWin', 8: 'TerroristsWin', 9: 'TargetSaved', 11: 'CTSurrender', 12: 'TSurrender', 17: 'TerroristsPlanted' } return type_map.get(win_type, f'Unknown_{win_type}')