""" Economy Processor - Handles leetify economic data Responsibilities: - Parse bron_equipment (equipment lists) - Parse player_bron_crash (starting money) - Calculate equipment_value - Write to fact_round_player_economy and update fact_rounds """ import sqlite3 import json import logging import uuid logger = logging.getLogger(__name__) class EconomyProcessor: @staticmethod def process_classic(match_data, conn: sqlite3.Connection) -> bool: """ Process classic economy data (extracted from round_list equiped) """ try: cursor = conn.cursor() for r in match_data.rounds: if not r.economies: continue for eco in r.economies: if eco.side not in ['CT', 'T']: # Skip rounds where side cannot be determined (avoids CHECK constraint failure) continue cursor.execute(''' INSERT OR REPLACE INTO fact_round_player_economy ( match_id, round_num, steam_id_64, side, start_money, equipment_value, main_weapon, has_helmet, has_defuser, has_zeus, round_performance_score, data_source_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( match_data.match_id, r.round_num, eco.steam_id_64, eco.side, eco.start_money, eco.equipment_value, eco.main_weapon, eco.has_helmet, eco.has_defuser, eco.has_zeus, eco.round_performance_score, 'classic' )) return True except Exception as e: logger.error(f"Error processing classic economy for match {match_data.match_id}: {e}") import traceback traceback.print_exc() return False @staticmethod def process_leetify(match_data, conn: sqlite3.Connection) -> bool: """ Process leetify economy and round 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() for r in round_stats: round_num = r.get('round', 0) # Extract round-level data ct_money_start = r.get('ct_money_group', 0) t_money_start = r.get('t_money_group', 0) win_reason = r.get('win_reason', 0) # Get timestamps begin_ts = r.get('begin_ts', '') end_ts = r.get('end_ts', '') # Get sfui_event for scores sfui = r.get('sfui_event', {}) ct_score = sfui.get('score_ct', 0) t_score = sfui.get('score_t', 0) # Determine winner_side based on show_event show_events = r.get('show_event', []) winner_side = 'None' duration = 0.0 if show_events: last_event = show_events[-1] # Check if there's a win_reason in the last event if last_event.get('win_reason'): win_reason = last_event.get('win_reason', 0) # Map win_reason to winner_side # Typical mappings: 1=T_Win, 2=CT_Win, etc. winner_side = _map_win_reason_to_side(win_reason) # Calculate duration from event timestamps if 'ts' in last_event: duration = float(last_event.get('ts', 0)) # 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, ct_money_start, t_money_start, begin_ts, end_ts, data_source_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( match_data.match_id, round_num, winner_side, win_reason, _map_win_reason_desc(win_reason), duration, ct_score, t_score, ct_money_start, t_money_start, begin_ts, end_ts, 'leetify' )) # Process economy data bron_equipment = r.get('bron_equipment', {}) player_t_score = r.get('player_t_score', {}) player_ct_score = r.get('player_ct_score', {}) player_bron_crash = r.get('player_bron_crash', {}) # Build side mapping side_scores = {} for sid, val in player_t_score.items(): side_scores[str(sid)] = ("T", float(val) if val is not None else 0.0) for sid, val in player_ct_score.items(): side_scores[str(sid)] = ("CT", float(val) if val is not None else 0.0) # Process each player's economy for sid in set(list(side_scores.keys()) + [str(k) for k in bron_equipment.keys()]): if sid not in side_scores: continue side, perf_score = side_scores[sid] items = bron_equipment.get(sid) or bron_equipment.get(str(sid)) or [] start_money = _pick_money(items) equipment_value = player_bron_crash.get(sid) or player_bron_crash.get(str(sid)) equipment_value = int(equipment_value) if equipment_value is not None else 0 main_weapon = _pick_main_weapon(items) has_helmet = _has_item_type(items, ['weapon_vest', 'item_assaultsuit', 'item_kevlar']) has_defuser = _has_item_type(items, ['item_defuser']) has_zeus = _has_item_type(items, ['weapon_taser']) cursor.execute(''' INSERT OR REPLACE INTO fact_round_player_economy ( match_id, round_num, steam_id_64, side, start_money, equipment_value, main_weapon, has_helmet, has_defuser, has_zeus, round_performance_score, data_source_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( match_data.match_id, round_num, sid, side, start_money, equipment_value, main_weapon, has_helmet, has_defuser, has_zeus, perf_score, 'leetify' )) logger.debug(f"Processed {len(round_stats)} leetify rounds for match {match_data.match_id}") return True except Exception as e: logger.error(f"Error processing leetify economy for match {match_data.match_id}: {e}") import traceback traceback.print_exc() return False def _pick_main_weapon(items): """Extract main weapon from equipment list""" if not isinstance(items, list): return "" ignore = { "weapon_knife", "weapon_knife_t", "weapon_knife_gg", "weapon_knife_ct", "weapon_c4", "weapon_flashbang", "weapon_hegrenade", "weapon_smokegrenade", "weapon_molotov", "weapon_incgrenade", "weapon_decoy" } # First pass: ignore utility for it in items: if not isinstance(it, dict): continue name = it.get('WeaponName') if name and name not in ignore: return name # Second pass: any weapon for it in items: if not isinstance(it, dict): continue name = it.get('WeaponName') if name: return name return "" def _pick_money(items): """Extract starting money from equipment list""" if not isinstance(items, list): return 0 vals = [] for it in items: if isinstance(it, dict) and it.get('Money') is not None: vals.append(it.get('Money')) return int(max(vals)) if vals else 0 def _has_item_type(items, keywords): """Check if equipment list contains item matching keywords""" if not isinstance(items, list): return False for it in items: if not isinstance(it, dict): continue name = it.get('WeaponName', '') if any(kw in name for kw in keywords): return True return False def _map_win_reason_to_side(win_reason): """Map win_reason integer to winner_side""" # Common mappings from CS:GO/CS2: # 1 = Target_Bombed (T wins) # 2 = Bomb_Defused (CT wins) # 7 = CTs_Win (CT eliminates T) # 8 = Terrorists_Win (T eliminates CT) # 9 = Target_Saved (CT wins, time runs out) # etc. t_win_reasons = {1, 8, 12, 17} ct_win_reasons = {2, 7, 9, 11} if win_reason in t_win_reasons: return 'T' elif win_reason in ct_win_reasons: return 'CT' else: return 'None' def _map_win_reason_desc(win_reason): """Map win_reason integer to description""" reason_map = { 0: 'None', 1: 'TargetBombed', 2: 'BombDefused', 7: 'CTsWin', 8: 'TerroristsWin', 9: 'TargetSaved', 11: 'CTSurrender', 12: 'TSurrender', 17: 'TerroristsPlanted' } return reason_map.get(win_reason, f'Unknown_{win_reason}')