Files
clutch/database/L2/processors/economy_processor.py
2026-02-05 23:26:03 +08:00

272 lines
10 KiB
Python

"""
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}')