Remastered Startup.

This commit is contained in:
2026-08-08 20:27:12 +08:00
commit fa75081d4d
107 changed files with 21665 additions and 0 deletions
BIN
View File
Binary file not shown.
View File
+102
View File
@@ -0,0 +1,102 @@
"""
L1A Data Ingestion Script
This script reads raw JSON files from the 'output_arena' directory and ingests them into the SQLite database.
It supports incremental updates by default, skipping files that have already been processed.
Usage:
python ETL/L1A.py # Standard incremental run
python ETL/L1A.py --force # Force re-process all files (overwrite existing data)
"""
import os
import json
import sqlite3
import glob
import argparse # Added
# Paths
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUTPUT_ARENA_DIR = os.path.join(BASE_DIR, 'output_arena')
DB_DIR = os.path.join(BASE_DIR, 'database', 'L1')
DB_PATH = os.path.join(DB_DIR, 'L1.db')
def init_db():
if not os.path.exists(DB_DIR):
os.makedirs(DB_DIR)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS raw_iframe_network (
match_id TEXT PRIMARY KEY,
content TEXT,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
return conn
def process_files():
parser = argparse.ArgumentParser()
parser.add_argument('--force', action='store_true', help='Force reprocessing of all files')
args = parser.parse_args()
conn = init_db()
cursor = conn.cursor()
# Get existing match_ids to skip
existing_ids = set()
if not args.force:
try:
cursor.execute("SELECT match_id FROM raw_iframe_network")
existing_ids = set(row[0] for row in cursor.fetchall())
print(f"Found {len(existing_ids)} existing matches in DB. Incremental mode active.")
except Exception as e:
print(f"Error checking existing data: {e}")
# Pattern to match all iframe_network.json files
# output_arena/*/iframe_network.json
pattern = os.path.join(OUTPUT_ARENA_DIR, '*', 'iframe_network.json')
files = glob.glob(pattern)
print(f"Found {len(files)} files in directory.")
count = 0
skipped = 0
for file_path in files:
try:
# Extract match_id from directory name
# file_path is like .../output_arena/g161-xxx/iframe_network.json
parent_dir = os.path.dirname(file_path)
match_id = os.path.basename(parent_dir)
if match_id in existing_ids:
skipped += 1
continue
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Upsert data
cursor.execute('''
INSERT OR REPLACE INTO raw_iframe_network (match_id, content)
VALUES (?, ?)
''', (match_id, content))
count += 1
if count % 100 == 0:
print(f"Processed {count} files...")
conn.commit()
except Exception as e:
print(f"Error processing {file_path}: {e}")
conn.commit()
conn.close()
print(f"Finished. Processed: {count}, Skipped: {skipped}.")
if __name__ == '__main__':
process_files()
+16
View File
@@ -0,0 +1,16 @@
L1A 5eplay平台网页爬虫原始数据。
## ETL Step 1:
从原始json数据库提取到L1A级数据库中。
`output_arena/*/iframe_network.json` -> `database/L1A/L1A.sqlite`
### 脚本说明
- **脚本位置**: `ETL/L1A.py`
- **功能**: 自动遍历 `output_arena` 目录下所有的 `iframe_network.json` 文件,提取原始内容并以 `match_id` (文件夹名) 为主键存入 `L1A.sqlite` 数据库的 `raw_iframe_network` 表中。
### 运行方式
使用项目指定的 Python 环境运行脚本:
```bash
C:/ProgramData/anaconda3/python.exe ETL/L1A.py
```
+37
View File
@@ -0,0 +1,37 @@
# L1B层 - 预留目录
## 用途说明
本目录为**预留**目录,用于未来的Demo直接解析管道。
### 背景
当前数据流:
```
output_arena/*/iframe_network.json → L1(raw JSON) → L2(structured) → L3(features)
```
### 未来规划
L1B层将作为另一条数据管道的入口:
```
Demo文件(*.dem) → L1B(Demo解析后的结构化数据) → L2 → L3
```
### 为什么预留?
1. **数据源多样性**: 除了网页抓取的JSON数据,未来可能需要直接从CS2 Demo文件中提取更精细的数据(如玩家视角、准星位置、投掷物轨迹等)
2. **架构一致性**: 保持L1A和L1B作为两个平行的原始数据层,方便后续L2层统一处理
3. **可扩展性**: Demo解析可提供更丰富的空间和时间数据,为L3层的高级特征提供支持
### 实施建议
当需要启用L1B时:
1. 创建`L1B_Builder.py`用于Demo文件解析
2. 创建`L1B.db`存储解析后的数据
3. 修改L2_Builder.py支持从L1B读取数据
4. 设计L1B schema以兼容现有L2层结构
### 当前状态
**预留中** - 无需任何文件或配置
+4
View File
@@ -0,0 +1,4 @@
L1B demo原始数据。
ETL Step 2:
从demoparser2提取demo原始数据到L1B级数据库中。
output_arena/*/iframe_network.json -> database/L1B/L1B.sqlite
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
"""
L2 Processor Modules
This package contains specialized processors for L2 database construction:
- match_processor: Handles fact_matches and fact_match_teams
- player_processor: Handles dim_players and fact_match_players (all variants)
- round_processor: Dispatches round data processing based on data_source_type
- economy_processor: Processes leetify economic data
- event_processor: Processes kill and bomb events
- spatial_processor: Processes classic spatial (xyz) data
"""
__all__ = [
'match_processor',
'player_processor',
'round_processor',
'economy_processor',
'event_processor',
'spatial_processor'
]
+271
View File
@@ -0,0 +1,271 @@
"""
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}')
+293
View File
@@ -0,0 +1,293 @@
"""
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}')
+128
View File
@@ -0,0 +1,128 @@
"""
Match Processor - Handles fact_matches and fact_match_teams
Responsibilities:
- Extract match basic information from JSON
- Process team data (group1/group2)
- Store raw JSON fields (treat_info, response metadata)
- Set data_source_type marker
"""
import sqlite3
import json
import logging
from typing import Any, Dict
logger = logging.getLogger(__name__)
def safe_int(val):
"""Safely convert value to integer"""
try:
return int(float(val)) if val is not None else 0
except:
return 0
def safe_float(val):
"""Safely convert value to float"""
try:
return float(val) if val is not None else 0.0
except:
return 0.0
def safe_text(val):
"""Safely convert value to text"""
return "" if val is None else str(val)
class MatchProcessor:
@staticmethod
def process(match_data, conn: sqlite3.Connection) -> bool:
"""
Process match basic info and team data
Args:
match_data: MatchData object containing parsed JSON
conn: L2 database connection
Returns:
bool: True if successful
"""
try:
cursor = conn.cursor()
# Build column list and values dynamically to avoid count mismatches
columns = [
'match_id', 'match_code', 'map_name', 'start_time', 'end_time', 'duration',
'winner_team', 'score_team1', 'score_team2', 'server_ip', 'server_port', 'location',
'has_side_data_and_rating2', 'match_main_id', 'demo_url', 'game_mode', 'game_name',
'map_desc', 'location_full', 'match_mode', 'match_status', 'match_flag', 'status', 'waiver',
'year', 'season', 'round_total', 'cs_type', 'priority_show_type', 'pug10m_show_type',
'credit_match_status', 'knife_winner', 'knife_winner_role', 'most_1v2_uid',
'most_assist_uid', 'most_awp_uid', 'most_end_uid', 'most_first_kill_uid',
'most_headshot_uid', 'most_jump_uid', 'mvp_uid', 'response_code', 'response_message',
'response_status', 'response_timestamp', 'response_trace_id', 'response_success',
'response_errcode', 'treat_info_raw', 'round_list_raw', 'leetify_data_raw',
'data_source_type'
]
values = [
match_data.match_id, match_data.match_code, match_data.map_name, match_data.start_time,
match_data.end_time, match_data.duration, match_data.winner_team, match_data.score_team1,
match_data.score_team2, match_data.server_ip, match_data.server_port, match_data.location,
match_data.has_side_data_and_rating2, match_data.match_main_id, match_data.demo_url,
match_data.game_mode, match_data.game_name, match_data.map_desc, match_data.location_full,
match_data.match_mode, match_data.match_status, match_data.match_flag, match_data.status,
match_data.waiver, match_data.year, match_data.season, match_data.round_total,
match_data.cs_type, match_data.priority_show_type, match_data.pug10m_show_type,
match_data.credit_match_status, match_data.knife_winner, match_data.knife_winner_role,
match_data.most_1v2_uid, match_data.most_assist_uid, match_data.most_awp_uid,
match_data.most_end_uid, match_data.most_first_kill_uid, match_data.most_headshot_uid,
match_data.most_jump_uid, match_data.mvp_uid, match_data.response_code,
match_data.response_message, match_data.response_status, match_data.response_timestamp,
match_data.response_trace_id, match_data.response_success, match_data.response_errcode,
match_data.treat_info_raw, match_data.round_list_raw, match_data.leetify_data_raw,
match_data.data_source_type
]
# Build SQL dynamically
placeholders = ','.join(['?' for _ in columns])
columns_sql = ','.join(columns)
sql = f"INSERT OR REPLACE INTO fact_matches ({columns_sql}) VALUES ({placeholders})"
cursor.execute(sql, values)
# Process team data
for team in match_data.teams:
team_row = (
match_data.match_id,
team.group_id,
team.group_all_score,
team.group_change_elo,
team.group_fh_role,
team.group_fh_score,
team.group_origin_elo,
team.group_sh_role,
team.group_sh_score,
team.group_tid,
team.group_uids
)
cursor.execute('''
INSERT OR REPLACE INTO fact_match_teams (
match_id, group_id, group_all_score, group_change_elo,
group_fh_role, group_fh_score, group_origin_elo,
group_sh_role, group_sh_score, group_tid, group_uids
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', team_row)
logger.debug(f"Processed match {match_data.match_id}")
return True
except Exception as e:
logger.error(f"Error processing match {match_data.match_id}: {e}")
import traceback
traceback.print_exc()
return False
+272
View File
@@ -0,0 +1,272 @@
"""
Player Processor - Handles dim_players and fact_match_players
Responsibilities:
- Process player dimension table (UPSERT to avoid duplicates)
- Merge fight/fight_t/fight_ct data
- Process VIP+ advanced statistics
- Handle all player match statistics tables
"""
import sqlite3
import json
import logging
from typing import Any, Dict
logger = logging.getLogger(__name__)
def safe_int(val):
"""Safely convert value to integer"""
try:
return int(float(val)) if val is not None else 0
except:
return 0
def safe_float(val):
"""Safely convert value to float"""
try:
return float(val) if val is not None else 0.0
except:
return 0.0
def safe_text(val):
"""Safely convert value to text"""
return "" if val is None else str(val)
class PlayerProcessor:
@staticmethod
def process(match_data, conn: sqlite3.Connection) -> bool:
"""
Process all player-related data
Args:
match_data: MatchData object containing parsed JSON
conn: L2 database connection
Returns:
bool: True if successful
"""
try:
cursor = conn.cursor()
# Process dim_players (UPSERT) - using dynamic column building
for steam_id, meta in match_data.player_meta.items():
# Define columns (must match schema exactly)
player_columns = [
'steam_id_64', 'uid', 'username', 'avatar_url', 'domain', 'created_at', 'updated_at',
'last_seen_match_id', 'uuid', 'email', 'area', 'mobile', 'user_domain',
'username_audit_status', 'accid', 'team_id', 'trumpet_count', 'profile_nickname',
'profile_avatar_audit_status', 'profile_rgb_avatar_url', 'profile_photo_url',
'profile_gender', 'profile_birthday', 'profile_country_id', 'profile_region_id',
'profile_city_id', 'profile_language', 'profile_recommend_url', 'profile_group_id',
'profile_reg_source', 'status_status', 'status_expire', 'status_cancellation_status',
'status_new_user', 'status_login_banned_time', 'status_anticheat_type',
'status_flag_status1', 'status_anticheat_status', 'status_flag_honor',
'status_privacy_policy_status', 'status_csgo_frozen_exptime', 'platformexp_level',
'platformexp_exp', 'steam_account', 'steam_trade_url', 'steam_rent_id',
'trusted_credit', 'trusted_credit_level', 'trusted_score', 'trusted_status',
'trusted_credit_status', 'certify_id_type', 'certify_status', 'certify_age',
'certify_real_name', 'certify_uid_list', 'certify_audit_status', 'certify_gender',
'identity_type', 'identity_extras', 'identity_status', 'identity_slogan',
'identity_list', 'identity_slogan_ext', 'identity_live_url', 'identity_live_type',
'plus_is_plus', 'user_info_raw'
]
player_values = [
steam_id, meta['uid'], meta['username'], meta['avatar_url'], meta['domain'],
meta['created_at'], meta['updated_at'], match_data.match_id, meta['uuid'],
meta['email'], meta['area'], meta['mobile'], meta['user_domain'],
meta['username_audit_status'], meta['accid'], meta['team_id'],
meta['trumpet_count'], meta['profile_nickname'],
meta['profile_avatar_audit_status'], meta['profile_rgb_avatar_url'],
meta['profile_photo_url'], meta['profile_gender'], meta['profile_birthday'],
meta['profile_country_id'], meta['profile_region_id'], meta['profile_city_id'],
meta['profile_language'], meta['profile_recommend_url'], meta['profile_group_id'],
meta['profile_reg_source'], meta['status_status'], meta['status_expire'],
meta['status_cancellation_status'], meta['status_new_user'],
meta['status_login_banned_time'], meta['status_anticheat_type'],
meta['status_flag_status1'], meta['status_anticheat_status'],
meta['status_flag_honor'], meta['status_privacy_policy_status'],
meta['status_csgo_frozen_exptime'], meta['platformexp_level'],
meta['platformexp_exp'], meta['steam_account'], meta['steam_trade_url'],
meta['steam_rent_id'], meta['trusted_credit'], meta['trusted_credit_level'],
meta['trusted_score'], meta['trusted_status'], meta['trusted_credit_status'],
meta['certify_id_type'], meta['certify_status'], meta['certify_age'],
meta['certify_real_name'], meta['certify_uid_list'],
meta['certify_audit_status'], meta['certify_gender'], meta['identity_type'],
meta['identity_extras'], meta['identity_status'], meta['identity_slogan'],
meta['identity_list'], meta['identity_slogan_ext'], meta['identity_live_url'],
meta['identity_live_type'], meta['plus_is_plus'], meta['user_info_raw']
]
# Build SQL dynamically
placeholders = ','.join(['?' for _ in player_columns])
columns_sql = ','.join(player_columns)
sql = f"INSERT OR REPLACE INTO dim_players ({columns_sql}) VALUES ({placeholders})"
cursor.execute(sql, player_values)
# Process fact_match_players
for steam_id, stats in match_data.players.items():
player_stats_row = _build_player_stats_tuple(match_data.match_id, stats)
cursor.execute(_get_fact_match_players_insert_sql(), player_stats_row)
# Process fact_match_players_t
for steam_id, stats in match_data.players_t.items():
player_stats_row = _build_player_stats_tuple(match_data.match_id, stats)
cursor.execute(_get_fact_match_players_insert_sql('fact_match_players_t'), player_stats_row)
# Process fact_match_players_ct
for steam_id, stats in match_data.players_ct.items():
player_stats_row = _build_player_stats_tuple(match_data.match_id, stats)
cursor.execute(_get_fact_match_players_insert_sql('fact_match_players_ct'), player_stats_row)
logger.debug(f"Processed {len(match_data.players)} players for match {match_data.match_id}")
return True
except Exception as e:
logger.error(f"Error processing players for match {match_data.match_id}: {e}")
import traceback
traceback.print_exc()
return False
def _build_player_stats_tuple(match_id, stats):
"""Build tuple for player stats insertion"""
return (
match_id,
stats.steam_id_64,
stats.team_id,
stats.kills,
stats.deaths,
stats.assists,
stats.headshot_count,
stats.kd_ratio,
stats.adr,
stats.rating,
stats.rating2,
stats.rating3,
stats.rws,
stats.mvp_count,
stats.elo_change,
stats.origin_elo,
stats.rank_score,
stats.is_win,
stats.kast,
stats.entry_kills,
stats.entry_deaths,
stats.awp_kills,
stats.clutch_1v1,
stats.clutch_1v2,
stats.clutch_1v3,
stats.clutch_1v4,
stats.clutch_1v5,
stats.flash_assists,
stats.flash_duration,
stats.jump_count,
stats.util_flash_usage,
stats.util_smoke_usage,
stats.util_molotov_usage,
stats.util_he_usage,
stats.util_decoy_usage,
stats.damage_total,
stats.damage_received,
stats.damage_receive,
stats.damage_stats,
stats.assisted_kill,
stats.awp_kill,
stats.awp_kill_ct,
stats.awp_kill_t,
stats.benefit_kill,
stats.day,
stats.defused_bomb,
stats.end_1v1,
stats.end_1v2,
stats.end_1v3,
stats.end_1v4,
stats.end_1v5,
stats.explode_bomb,
stats.first_death,
stats.fd_ct,
stats.fd_t,
stats.first_kill,
stats.flash_enemy,
stats.flash_team,
stats.flash_team_time,
stats.flash_time,
stats.game_mode,
stats.group_id,
stats.hold_total,
stats.id,
stats.is_highlight,
stats.is_most_1v2,
stats.is_most_assist,
stats.is_most_awp,
stats.is_most_end,
stats.is_most_first_kill,
stats.is_most_headshot,
stats.is_most_jump,
stats.is_svp,
stats.is_tie,
stats.kill_1,
stats.kill_2,
stats.kill_3,
stats.kill_4,
stats.kill_5,
stats.many_assists_cnt1,
stats.many_assists_cnt2,
stats.many_assists_cnt3,
stats.many_assists_cnt4,
stats.many_assists_cnt5,
stats.map,
stats.match_code,
stats.match_mode,
stats.match_team_id,
stats.match_time,
stats.per_headshot,
stats.perfect_kill,
stats.planted_bomb,
stats.revenge_kill,
stats.round_total,
stats.season,
stats.team_kill,
stats.throw_harm,
stats.throw_harm_enemy,
stats.uid,
stats.year,
stats.sts_raw,
stats.level_info_raw
)
def _get_fact_match_players_insert_sql(table='fact_match_players'):
"""Get INSERT SQL for player stats table - dynamically generated"""
# Define columns explicitly to ensure exact match with schema
columns = [
'match_id', 'steam_id_64', 'team_id', 'kills', 'deaths', 'assists', 'headshot_count',
'kd_ratio', 'adr', 'rating', 'rating2', 'rating3', 'rws', 'mvp_count', 'elo_change',
'origin_elo', 'rank_score', 'is_win', 'kast', 'entry_kills', 'entry_deaths', 'awp_kills',
'clutch_1v1', 'clutch_1v2', 'clutch_1v3', 'clutch_1v4', 'clutch_1v5',
'flash_assists', 'flash_duration', 'jump_count', 'util_flash_usage',
'util_smoke_usage', 'util_molotov_usage', 'util_he_usage', 'util_decoy_usage',
'damage_total', 'damage_received', 'damage_receive', 'damage_stats',
'assisted_kill', 'awp_kill', 'awp_kill_ct', 'awp_kill_t', 'benefit_kill',
'day', 'defused_bomb', 'end_1v1', 'end_1v2', 'end_1v3', 'end_1v4', 'end_1v5',
'explode_bomb', 'first_death', 'fd_ct', 'fd_t', 'first_kill', 'flash_enemy',
'flash_team', 'flash_team_time', 'flash_time', 'game_mode', 'group_id',
'hold_total', 'id', 'is_highlight', 'is_most_1v2', 'is_most_assist',
'is_most_awp', 'is_most_end', 'is_most_first_kill', 'is_most_headshot',
'is_most_jump', 'is_svp', 'is_tie', 'kill_1', 'kill_2', 'kill_3', 'kill_4', 'kill_5',
'many_assists_cnt1', 'many_assists_cnt2', 'many_assists_cnt3',
'many_assists_cnt4', 'many_assists_cnt5', 'map', 'match_code', 'match_mode',
'match_team_id', 'match_time', 'per_headshot', 'perfect_kill', 'planted_bomb',
'revenge_kill', 'round_total', 'season', 'team_kill', 'throw_harm',
'throw_harm_enemy', 'uid', 'year', 'sts_raw', 'level_info_raw'
]
placeholders = ','.join(['?' for _ in columns])
columns_sql = ','.join(columns)
return f'INSERT OR REPLACE INTO {table} ({columns_sql}) VALUES ({placeholders})'
+97
View File
@@ -0,0 +1,97 @@
"""
Round Processor - Dispatches round data processing based on data_source_type
Responsibilities:
- Act as the unified entry point for round data processing
- Determine data source type (leetify vs classic)
- Dispatch to appropriate specialized processors
- Coordinate economy, event, and spatial processors
"""
import sqlite3
import logging
logger = logging.getLogger(__name__)
class RoundProcessor:
@staticmethod
def process(match_data, conn: sqlite3.Connection) -> bool:
"""
Process round data by dispatching to specialized processors
Args:
match_data: MatchData object containing parsed JSON
conn: L2 database connection
Returns:
bool: True if successful
"""
try:
# Import specialized processors
from . import economy_processor
from . import event_processor
from . import spatial_processor
if match_data.data_source_type == 'leetify':
logger.debug(f"Processing leetify data for match {match_data.match_id}")
# Process leetify rounds
success = economy_processor.EconomyProcessor.process_leetify(match_data, conn)
if not success:
logger.warning(f"Failed to process leetify economy for match {match_data.match_id}")
# Process leetify events
success = event_processor.EventProcessor.process_leetify_events(match_data, conn)
if not success:
logger.warning(f"Failed to process leetify events for match {match_data.match_id}")
elif match_data.data_source_type == 'classic':
logger.debug(f"Processing classic data for match {match_data.match_id}")
# Process classic rounds (basic round info)
success = _process_classic_rounds(match_data, conn)
if not success:
logger.warning(f"Failed to process classic rounds for match {match_data.match_id}")
# Process classic economy (NEW)
success = economy_processor.EconomyProcessor.process_classic(match_data, conn)
if not success:
logger.warning(f"Failed to process classic economy for match {match_data.match_id}")
# Process classic events (kills, bombs)
success = event_processor.EventProcessor.process_classic_events(match_data, conn)
if not success:
logger.warning(f"Failed to process classic events for match {match_data.match_id}")
# Process spatial data (xyz coordinates)
success = spatial_processor.SpatialProcessor.process(match_data, conn)
if not success:
logger.warning(f"Failed to process spatial data for match {match_data.match_id}")
else:
logger.info(f"No round data to process for match {match_data.match_id} (data_source_type={match_data.data_source_type})")
return True
except Exception as e:
logger.error(f"Error in round processor for match {match_data.match_id}: {e}")
import traceback
traceback.print_exc()
return False
def _process_classic_rounds(match_data, conn: sqlite3.Connection) -> bool:
"""
Process basic round information for classic data source
Classic round data contains:
- current_score (ct/t scores, type, pasttime, final_round_time)
- But lacks economy data
"""
try:
# This is handled by event_processor for classic
# Classic rounds are extracted from round_list structure
# which is processed in event_processor.process_classic_events
return True
except Exception as e:
logger.error(f"Error processing classic rounds: {e}")
return False
+100
View File
@@ -0,0 +1,100 @@
"""
Spatial Processor - Handles classic spatial (xyz) data
Responsibilities:
- Extract attacker/victim position data from classic round_list
- Update fact_round_events with spatial coordinates
- Prepare data for future heatmap/tactical board analysis
"""
import sqlite3
import logging
logger = logging.getLogger(__name__)
class SpatialProcessor:
@staticmethod
def process(match_data, conn: sqlite3.Connection) -> bool:
"""
Process spatial data from classic round_list
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()
update_count = 0
for idx, rd in enumerate(round_list, start=1):
round_num = idx
# Process kill events with spatial data
all_kill = rd.get('all_kill', [])
for kill in all_kill:
attacker = kill.get('attacker', {})
victim = kill.get('victim', {})
attacker_steam_id = str(attacker.get('steamid_64', ''))
victim_steam_id = str(victim.get('steamid_64', ''))
event_time = kill.get('pasttime', 0)
# Extract positions
attacker_pos = attacker.get('pos', {})
victim_pos = victim.get('pos', {})
attacker_pos_x = attacker_pos.get('x', 0) if isinstance(attacker_pos, dict) else 0
attacker_pos_y = attacker_pos.get('y', 0) if isinstance(attacker_pos, dict) else 0
attacker_pos_z = attacker_pos.get('z', 0) if isinstance(attacker_pos, dict) else 0
victim_pos_x = victim_pos.get('x', 0) if isinstance(victim_pos, dict) else 0
victim_pos_y = victim_pos.get('y', 0) if isinstance(victim_pos, dict) else 0
victim_pos_z = victim_pos.get('z', 0) if isinstance(victim_pos, dict) else 0
# Update existing event with spatial data
# We match by match_id, round_num, attacker, victim, and event_time
cursor.execute('''
UPDATE fact_round_events
SET attacker_pos_x = ?,
attacker_pos_y = ?,
attacker_pos_z = ?,
victim_pos_x = ?,
victim_pos_y = ?,
victim_pos_z = ?
WHERE match_id = ?
AND round_num = ?
AND attacker_steam_id = ?
AND victim_steam_id = ?
AND event_time = ?
AND event_type = 'kill'
AND data_source_type = 'classic'
''', (
attacker_pos_x, attacker_pos_y, attacker_pos_z,
victim_pos_x, victim_pos_y, victim_pos_z,
match_data.match_id, round_num, attacker_steam_id,
victim_steam_id, event_time
))
if cursor.rowcount > 0:
update_count += 1
logger.debug(f"Updated {update_count} events with spatial data for match {match_data.match_id}")
return True
except Exception as e:
logger.error(f"Error processing spatial data for match {match_data.match_id}: {e}")
import traceback
traceback.print_exc()
return False
+638
View File
@@ -0,0 +1,638 @@
-- Enable Foreign Keys
PRAGMA foreign_keys = ON;
-- 1. Dimension: Players
-- Stores persistent player information.
-- Conflict resolution: UPSERT on steam_id_64.
CREATE TABLE IF NOT EXISTS dim_players (
steam_id_64 TEXT PRIMARY KEY,
uid INTEGER, -- 5E Platform ID
username TEXT,
avatar_url TEXT,
domain TEXT,
created_at INTEGER, -- Timestamp
updated_at INTEGER, -- Timestamp
last_seen_match_id TEXT,
uuid TEXT,
email TEXT,
area TEXT,
mobile TEXT,
user_domain TEXT,
username_audit_status INTEGER,
accid TEXT,
team_id INTEGER,
trumpet_count INTEGER,
profile_nickname TEXT,
profile_avatar_audit_status INTEGER,
profile_rgb_avatar_url TEXT,
profile_photo_url TEXT,
profile_gender INTEGER,
profile_birthday INTEGER,
profile_country_id TEXT,
profile_region_id TEXT,
profile_city_id TEXT,
profile_language TEXT,
profile_recommend_url TEXT,
profile_group_id INTEGER,
profile_reg_source INTEGER,
status_status INTEGER,
status_expire INTEGER,
status_cancellation_status INTEGER,
status_new_user INTEGER,
status_login_banned_time INTEGER,
status_anticheat_type INTEGER,
status_flag_status1 TEXT,
status_anticheat_status TEXT,
status_flag_honor TEXT,
status_privacy_policy_status INTEGER,
status_csgo_frozen_exptime INTEGER,
platformexp_level INTEGER,
platformexp_exp INTEGER,
steam_account TEXT,
steam_trade_url TEXT,
steam_rent_id TEXT,
trusted_credit INTEGER,
trusted_credit_level INTEGER,
trusted_score INTEGER,
trusted_status INTEGER,
trusted_credit_status INTEGER,
certify_id_type INTEGER,
certify_status INTEGER,
certify_age INTEGER,
certify_real_name TEXT,
certify_uid_list TEXT,
certify_audit_status INTEGER,
certify_gender INTEGER,
identity_type INTEGER,
identity_extras TEXT,
identity_status INTEGER,
identity_slogan TEXT,
identity_list TEXT,
identity_slogan_ext TEXT,
identity_live_url TEXT,
identity_live_type INTEGER,
plus_is_plus INTEGER,
user_info_raw TEXT
);
CREATE INDEX IF NOT EXISTS idx_dim_players_uid ON dim_players(uid);
-- 2. Dimension: Maps
CREATE TABLE IF NOT EXISTS dim_maps (
map_id INTEGER PRIMARY KEY AUTOINCREMENT,
map_name TEXT UNIQUE NOT NULL,
map_desc TEXT
);
-- 3. Fact: Matches
CREATE TABLE IF NOT EXISTS fact_matches (
match_id TEXT PRIMARY KEY,
match_code TEXT,
map_name TEXT,
start_time INTEGER,
end_time INTEGER,
duration INTEGER,
winner_team INTEGER, -- 1 or 2
score_team1 INTEGER,
score_team2 INTEGER,
server_ip TEXT,
server_port INTEGER,
location TEXT,
has_side_data_and_rating2 INTEGER,
match_main_id INTEGER,
demo_url TEXT,
game_mode INTEGER,
game_name TEXT,
map_desc TEXT,
location_full TEXT,
match_mode INTEGER,
match_status INTEGER,
match_flag INTEGER,
status INTEGER,
waiver INTEGER,
year INTEGER,
season TEXT,
round_total INTEGER,
cs_type INTEGER,
priority_show_type INTEGER,
pug10m_show_type INTEGER,
credit_match_status INTEGER,
knife_winner INTEGER,
knife_winner_role INTEGER,
most_1v2_uid INTEGER,
most_assist_uid INTEGER,
most_awp_uid INTEGER,
most_end_uid INTEGER,
most_first_kill_uid INTEGER,
most_headshot_uid INTEGER,
most_jump_uid INTEGER,
mvp_uid INTEGER,
response_code INTEGER,
response_message TEXT,
response_status INTEGER,
response_timestamp INTEGER,
response_trace_id TEXT,
response_success INTEGER,
response_errcode INTEGER,
treat_info_raw TEXT,
round_list_raw TEXT,
leetify_data_raw TEXT,
data_source_type TEXT CHECK(data_source_type IN ('leetify', 'classic', 'unknown')), -- 'leetify' has economy data, 'classic' has detailed xyz
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_fact_matches_time ON fact_matches(start_time);
CREATE TABLE IF NOT EXISTS fact_match_teams (
match_id TEXT,
group_id INTEGER,
group_all_score INTEGER,
group_change_elo REAL,
group_fh_role INTEGER,
group_fh_score INTEGER,
group_origin_elo REAL,
group_sh_role INTEGER,
group_sh_score INTEGER,
group_tid INTEGER,
group_uids TEXT,
PRIMARY KEY (match_id, group_id),
FOREIGN KEY (match_id) REFERENCES fact_matches(match_id) ON DELETE CASCADE
);
-- 4. Fact: Match Player Stats (Wide Table)
-- Aggregated stats for a player in a specific match
CREATE TABLE IF NOT EXISTS fact_match_players (
match_id TEXT,
steam_id_64 TEXT,
team_id INTEGER, -- 1 or 2
-- Basic Stats
kills INTEGER DEFAULT 0,
deaths INTEGER DEFAULT 0,
assists INTEGER DEFAULT 0,
headshot_count INTEGER DEFAULT 0,
kd_ratio REAL,
adr REAL,
rating REAL, -- 5E Rating
rating2 REAL,
rating3 REAL,
rws REAL,
mvp_count INTEGER DEFAULT 0,
elo_change REAL,
origin_elo REAL,
rank_score INTEGER,
is_win BOOLEAN,
-- Advanced Stats (VIP/Plus)
kast REAL,
entry_kills INTEGER,
entry_deaths INTEGER,
awp_kills INTEGER,
clutch_1v1 INTEGER,
clutch_1v2 INTEGER,
clutch_1v3 INTEGER,
clutch_1v4 INTEGER,
clutch_1v5 INTEGER,
flash_assists INTEGER,
flash_duration REAL,
jump_count INTEGER,
-- Utility Usage Stats (Parsed from round details)
util_flash_usage INTEGER DEFAULT 0,
util_smoke_usage INTEGER DEFAULT 0,
util_molotov_usage INTEGER DEFAULT 0,
util_he_usage INTEGER DEFAULT 0,
util_decoy_usage INTEGER DEFAULT 0,
damage_total INTEGER,
damage_received INTEGER,
damage_receive INTEGER,
damage_stats INTEGER,
assisted_kill INTEGER,
awp_kill INTEGER,
awp_kill_ct INTEGER,
awp_kill_t INTEGER,
benefit_kill INTEGER,
day TEXT,
defused_bomb INTEGER,
end_1v1 INTEGER,
end_1v2 INTEGER,
end_1v3 INTEGER,
end_1v4 INTEGER,
end_1v5 INTEGER,
explode_bomb INTEGER,
first_death INTEGER,
fd_ct INTEGER,
fd_t INTEGER,
first_kill INTEGER,
flash_enemy INTEGER,
flash_team INTEGER,
flash_team_time REAL,
flash_time REAL,
game_mode TEXT,
group_id INTEGER,
hold_total INTEGER,
id INTEGER,
is_highlight INTEGER,
is_most_1v2 INTEGER,
is_most_assist INTEGER,
is_most_awp INTEGER,
is_most_end INTEGER,
is_most_first_kill INTEGER,
is_most_headshot INTEGER,
is_most_jump INTEGER,
is_svp INTEGER,
is_tie INTEGER,
kill_1 INTEGER,
kill_2 INTEGER,
kill_3 INTEGER,
kill_4 INTEGER,
kill_5 INTEGER,
many_assists_cnt1 INTEGER,
many_assists_cnt2 INTEGER,
many_assists_cnt3 INTEGER,
many_assists_cnt4 INTEGER,
many_assists_cnt5 INTEGER,
map TEXT,
match_code TEXT,
match_mode TEXT,
match_team_id INTEGER,
match_time INTEGER,
per_headshot REAL,
perfect_kill INTEGER,
planted_bomb INTEGER,
revenge_kill INTEGER,
round_total INTEGER,
season TEXT,
team_kill INTEGER,
throw_harm INTEGER,
throw_harm_enemy INTEGER,
uid INTEGER,
year TEXT,
sts_raw TEXT,
level_info_raw TEXT,
PRIMARY KEY (match_id, steam_id_64),
FOREIGN KEY (match_id) REFERENCES fact_matches(match_id) ON DELETE CASCADE
-- Intentionally not enforcing FK on steam_id_64 strictly to allow stats even if player dim missing, but ideally it should match.
);
CREATE TABLE IF NOT EXISTS fact_match_players_t (
match_id TEXT,
steam_id_64 TEXT,
team_id INTEGER,
kills INTEGER DEFAULT 0,
deaths INTEGER DEFAULT 0,
assists INTEGER DEFAULT 0,
headshot_count INTEGER DEFAULT 0,
kd_ratio REAL,
adr REAL,
rating REAL,
rating2 REAL,
rating3 REAL,
rws REAL,
mvp_count INTEGER DEFAULT 0,
elo_change REAL,
origin_elo REAL,
rank_score INTEGER,
is_win BOOLEAN,
kast REAL,
entry_kills INTEGER,
entry_deaths INTEGER,
awp_kills INTEGER,
clutch_1v1 INTEGER,
clutch_1v2 INTEGER,
clutch_1v3 INTEGER,
clutch_1v4 INTEGER,
clutch_1v5 INTEGER,
flash_assists INTEGER,
flash_duration REAL,
jump_count INTEGER,
damage_total INTEGER,
damage_received INTEGER,
damage_receive INTEGER,
damage_stats INTEGER,
assisted_kill INTEGER,
awp_kill INTEGER,
awp_kill_ct INTEGER,
awp_kill_t INTEGER,
benefit_kill INTEGER,
day TEXT,
defused_bomb INTEGER,
end_1v1 INTEGER,
end_1v2 INTEGER,
end_1v3 INTEGER,
end_1v4 INTEGER,
end_1v5 INTEGER,
explode_bomb INTEGER,
first_death INTEGER,
fd_ct INTEGER,
fd_t INTEGER,
first_kill INTEGER,
flash_enemy INTEGER,
flash_team INTEGER,
flash_team_time REAL,
flash_time REAL,
game_mode TEXT,
group_id INTEGER,
hold_total INTEGER,
id INTEGER,
is_highlight INTEGER,
is_most_1v2 INTEGER,
is_most_assist INTEGER,
is_most_awp INTEGER,
is_most_end INTEGER,
is_most_first_kill INTEGER,
is_most_headshot INTEGER,
is_most_jump INTEGER,
is_svp INTEGER,
is_tie INTEGER,
kill_1 INTEGER,
kill_2 INTEGER,
kill_3 INTEGER,
kill_4 INTEGER,
kill_5 INTEGER,
many_assists_cnt1 INTEGER,
many_assists_cnt2 INTEGER,
many_assists_cnt3 INTEGER,
many_assists_cnt4 INTEGER,
many_assists_cnt5 INTEGER,
map TEXT,
match_code TEXT,
match_mode TEXT,
match_team_id INTEGER,
match_time INTEGER,
per_headshot REAL,
perfect_kill INTEGER,
planted_bomb INTEGER,
revenge_kill INTEGER,
round_total INTEGER,
season TEXT,
team_kill INTEGER,
throw_harm INTEGER,
throw_harm_enemy INTEGER,
uid INTEGER,
year TEXT,
sts_raw TEXT,
level_info_raw TEXT,
-- Utility Usage Stats (Parsed from round details)
util_flash_usage INTEGER DEFAULT 0,
util_smoke_usage INTEGER DEFAULT 0,
util_molotov_usage INTEGER DEFAULT 0,
util_he_usage INTEGER DEFAULT 0,
util_decoy_usage INTEGER DEFAULT 0,
PRIMARY KEY (match_id, steam_id_64),
FOREIGN KEY (match_id) REFERENCES fact_matches(match_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS fact_match_players_ct (
match_id TEXT,
steam_id_64 TEXT,
team_id INTEGER,
kills INTEGER DEFAULT 0,
deaths INTEGER DEFAULT 0,
assists INTEGER DEFAULT 0,
headshot_count INTEGER DEFAULT 0,
kd_ratio REAL,
adr REAL,
rating REAL,
rating2 REAL,
rating3 REAL,
rws REAL,
mvp_count INTEGER DEFAULT 0,
elo_change REAL,
origin_elo REAL,
rank_score INTEGER,
is_win BOOLEAN,
kast REAL,
entry_kills INTEGER,
entry_deaths INTEGER,
awp_kills INTEGER,
clutch_1v1 INTEGER,
clutch_1v2 INTEGER,
clutch_1v3 INTEGER,
clutch_1v4 INTEGER,
clutch_1v5 INTEGER,
flash_assists INTEGER,
flash_duration REAL,
jump_count INTEGER,
damage_total INTEGER,
damage_received INTEGER,
damage_receive INTEGER,
damage_stats INTEGER,
assisted_kill INTEGER,
awp_kill INTEGER,
awp_kill_ct INTEGER,
awp_kill_t INTEGER,
benefit_kill INTEGER,
day TEXT,
defused_bomb INTEGER,
end_1v1 INTEGER,
end_1v2 INTEGER,
end_1v3 INTEGER,
end_1v4 INTEGER,
end_1v5 INTEGER,
explode_bomb INTEGER,
first_death INTEGER,
fd_ct INTEGER,
fd_t INTEGER,
first_kill INTEGER,
flash_enemy INTEGER,
flash_team INTEGER,
flash_team_time REAL,
flash_time REAL,
game_mode TEXT,
group_id INTEGER,
hold_total INTEGER,
id INTEGER,
is_highlight INTEGER,
is_most_1v2 INTEGER,
is_most_assist INTEGER,
is_most_awp INTEGER,
is_most_end INTEGER,
is_most_first_kill INTEGER,
is_most_headshot INTEGER,
is_most_jump INTEGER,
is_svp INTEGER,
is_tie INTEGER,
kill_1 INTEGER,
kill_2 INTEGER,
kill_3 INTEGER,
kill_4 INTEGER,
kill_5 INTEGER,
many_assists_cnt1 INTEGER,
many_assists_cnt2 INTEGER,
many_assists_cnt3 INTEGER,
many_assists_cnt4 INTEGER,
many_assists_cnt5 INTEGER,
map TEXT,
match_code TEXT,
match_mode TEXT,
match_team_id INTEGER,
match_time INTEGER,
per_headshot REAL,
perfect_kill INTEGER,
planted_bomb INTEGER,
revenge_kill INTEGER,
round_total INTEGER,
season TEXT,
team_kill INTEGER,
throw_harm INTEGER,
throw_harm_enemy INTEGER,
uid INTEGER,
year TEXT,
sts_raw TEXT,
level_info_raw TEXT,
-- Utility Usage Stats (Parsed from round details)
util_flash_usage INTEGER DEFAULT 0,
util_smoke_usage INTEGER DEFAULT 0,
util_molotov_usage INTEGER DEFAULT 0,
util_he_usage INTEGER DEFAULT 0,
util_decoy_usage INTEGER DEFAULT 0,
PRIMARY KEY (match_id, steam_id_64),
FOREIGN KEY (match_id) REFERENCES fact_matches(match_id) ON DELETE CASCADE
);
-- 5. Fact: Rounds
CREATE TABLE IF NOT EXISTS fact_rounds (
match_id TEXT,
round_num INTEGER,
-- 公共字段(两种数据源均有)
winner_side TEXT CHECK(winner_side IN ('CT', 'T', 'None')),
win_reason INTEGER, -- Raw integer from source
win_reason_desc TEXT, -- Mapped description (e.g. 'TargetBombed')
duration REAL,
ct_score INTEGER,
t_score INTEGER,
-- Leetify专属字段
ct_money_start INTEGER, -- 仅leetify
t_money_start INTEGER, -- 仅leetify
begin_ts TEXT, -- 仅leetify
end_ts TEXT, -- 仅leetify
-- Classic专属字段
end_time_stamp TEXT, -- 仅classic
final_round_time INTEGER, -- 仅classic
pasttime INTEGER, -- 仅classic
-- 数据源标记(继承自fact_matches)
data_source_type TEXT CHECK(data_source_type IN ('leetify', 'classic', 'unknown')),
PRIMARY KEY (match_id, round_num),
FOREIGN KEY (match_id) REFERENCES fact_matches(match_id) ON DELETE CASCADE
);
-- 6. Fact: Round Events (The largest table)
-- Unifies Kills, Bomb Events, etc.
CREATE TABLE IF NOT EXISTS fact_round_events (
event_id TEXT PRIMARY KEY, -- UUID
match_id TEXT,
round_num INTEGER,
event_type TEXT CHECK(event_type IN ('kill', 'bomb_plant', 'bomb_defuse', 'suicide', 'unknown')),
event_time INTEGER, -- Seconds from round start
-- Participants
attacker_steam_id TEXT,
victim_steam_id TEXT,
assister_steam_id TEXT,
flash_assist_steam_id TEXT,
trade_killer_steam_id TEXT,
-- Weapon & Context
weapon TEXT,
is_headshot BOOLEAN DEFAULT 0,
is_wallbang BOOLEAN DEFAULT 0,
is_blind BOOLEAN DEFAULT 0,
is_through_smoke BOOLEAN DEFAULT 0,
is_noscope BOOLEAN DEFAULT 0,
-- Classic空间数据(xyz坐标)
attacker_pos_x INTEGER, -- 仅classic
attacker_pos_y INTEGER, -- 仅classic
attacker_pos_z INTEGER, -- 仅classic
victim_pos_x INTEGER, -- 仅classic
victim_pos_y INTEGER, -- 仅classic
victim_pos_z INTEGER, -- 仅classic
-- Leetify评分影响
score_change_attacker REAL, -- 仅leetify
score_change_victim REAL, -- 仅leetify
twin REAL, -- 仅leetify (team win probability)
c_twin REAL, -- 仅leetify
twin_change REAL, -- 仅leetify
c_twin_change REAL, -- 仅leetify
-- 数据源标记
data_source_type TEXT CHECK(data_source_type IN ('leetify', 'classic', 'unknown')),
FOREIGN KEY (match_id, round_num) REFERENCES fact_rounds(match_id, round_num) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_round_events_match ON fact_round_events(match_id);
CREATE INDEX IF NOT EXISTS idx_round_events_attacker ON fact_round_events(attacker_steam_id);
-- 7. Fact: Round Player Economy/Status
-- Snapshots of player state at round start/end
CREATE TABLE IF NOT EXISTS fact_round_player_economy (
match_id TEXT,
round_num INTEGER,
steam_id_64 TEXT,
side TEXT CHECK(side IN ('CT', 'T')),
-- Leetify经济数据(仅leetify)
start_money INTEGER,
equipment_value INTEGER,
main_weapon TEXT,
has_helmet BOOLEAN,
has_defuser BOOLEAN,
has_zeus BOOLEAN,
round_performance_score REAL,
-- Classic装备快照(仅classic, JSON存储)
equipment_snapshot_json TEXT, -- Classic的equiped字段序列化
-- 数据源标记
data_source_type TEXT CHECK(data_source_type IN ('leetify', 'classic', 'unknown')),
PRIMARY KEY (match_id, round_num, steam_id_64),
FOREIGN KEY (match_id, round_num) REFERENCES fact_rounds(match_id, round_num) ON DELETE CASCADE
);
-- ==========================================
-- Views for Aggregated Statistics
-- ==========================================
-- 玩家全场景统计视图
CREATE VIEW IF NOT EXISTS v_player_all_stats AS
SELECT
steam_id_64,
COUNT(DISTINCT match_id) as total_matches,
AVG(rating) as avg_rating,
AVG(kd_ratio) as avg_kd,
AVG(kast) as avg_kast,
SUM(kills) as total_kills,
SUM(deaths) as total_deaths,
SUM(assists) as total_assists,
SUM(mvp_count) as total_mvps
FROM fact_match_players
GROUP BY steam_id_64;
-- 地图维度统计视图
CREATE VIEW IF NOT EXISTS v_map_performance AS
SELECT
fmp.steam_id_64,
fm.map_name,
COUNT(*) as matches_on_map,
AVG(fmp.rating) as avg_rating,
AVG(fmp.kd_ratio) as avg_kd,
SUM(CASE WHEN fmp.is_win THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as win_rate
FROM fact_match_players fmp
JOIN fact_matches fm ON fmp.match_id = fm.match_id
GROUP BY fmp.steam_id_64, fm.map_name;
+207
View File
@@ -0,0 +1,207 @@
# L2 Database Build - Final Report
## Executive Summary
**L2 Database Build: 100% Complete**
All 208 matches from L1 have been successfully transformed into structured L2 tables with full data coverage including matches, players, rounds, and events.
---
## Coverage Metrics
### Match Coverage
- **L1 Raw Matches**: 208
- **L2 Processed Matches**: 208
- **Coverage**: 100.0% ✅
### Data Distribution
- **Unique Players**: 1,181
- **Player-Match Records**: 2,080 (avg 10.0 per match)
- **Team Records**: 416
- **Map Records**: 9
- **Total Rounds**: 4,315 (avg 20.7 per match)
- **Total Events**: 33,560 (avg 7.8 per round)
- **Economy Records**: 5,930
### Data Source Types
- **Classic Mode**: 180 matches (86.5%)
- **Leetify Mode**: 28 matches (13.5%)
### Total Rows Across All Tables
**51,860 rows** successfully processed and stored
---
## L2 Schema Overview
### 1. Dimension Tables (2)
#### dim_players (1,181 rows, 68 columns)
Player master data including profile, status, certifications, identity, and platform information.
- Primary Key: steam_id_64
- Contains full player metadata from 5E platform
#### dim_maps (9 rows, 2 columns)
Map reference data
- Primary Key: map_name
- Contains map names and descriptions
### 2. Fact Tables - Match Level (5)
#### fact_matches (208 rows, 52 columns)
Core match information with comprehensive metadata
- Primary Key: match_id
- Includes: timing, scores, server info, game mode, response data
- Raw data preserved: treat_info_raw, round_list_raw, leetify_data_raw
- Data source tracking: data_source_type ('leetify'|'classic'|'unknown')
#### fact_match_teams (416 rows, 10 columns)
Team-level match statistics
- Primary Key: (match_id, group_id)
- Tracks: scores, ELO changes, roles, player UIDs
#### fact_match_players (2,080 rows, 101 columns)
Comprehensive player performance per match
- Primary Key: (match_id, steam_id_64)
- Categories:
- Basic Stats: kills, deaths, assists, K/D, ADR, rating
- Advanced Stats: KAST, entry kills/deaths, AWP stats
- Clutch Stats: 1v1 through 1v5
- Utility Stats: flash/smoke/molotov/HE/decoy usage
- Special Metrics: MVP, highlight, achievement flags
#### fact_match_players_ct (2,080 rows, 101 columns)
CT-side specific player statistics
- Same schema as fact_match_players
- Filtered to CT-side performance only
#### fact_match_players_t (2,080 rows, 101 columns)
T-side specific player statistics
- Same schema as fact_match_players
- Filtered to T-side performance only
### 3. Fact Tables - Round Level (3)
#### fact_rounds (4,315 rows, 16 columns)
Round-by-round match progression
- Primary Key: (match_id, round_num)
- Common Fields: winner_side, win_reason, duration, scores
- Leetify Fields: money_start (CT/T), begin_ts, end_ts
- Classic Fields: end_time_stamp, final_round_time, pasttime
- Data source tagged for each round
#### fact_round_events (33,560 rows, 29 columns)
Detailed event tracking (kills, deaths, bomb events)
- Primary Key: event_id
- Event Types: kill, bomb_plant, bomb_defuse, etc.
- Position Data: attacker/victim xyz coordinates
- Mechanics: headshot, wallbang, blind, through_smoke, noscope flags
- Leetify Scoring: score changes, team win probability (twin)
- Assists: flash assists, trade kills tracked
#### fact_round_player_economy (5,930 rows, 13 columns)
Economy state per player per round
- Primary Key: (match_id, round_num, steam_id_64)
- Leetify Data: start_money, equipment_value, loadout details
- Classic Data: equipment_snapshot_json (serialized)
- Economy Tracking: main_weapon, helmet, defuser, zeus
- Performance: round_performance_score (leetify only)
---
## Data Processing Architecture
### Modular Processor Pattern
The L2 build uses a 6-processor architecture:
1. **match_processor**: fact_matches, fact_match_teams
2. **player_processor**: dim_players, fact_match_players (all variants)
3. **round_processor**: Dispatcher based on data_source_type
4. **economy_processor**: fact_round_player_economy (leetify data)
5. **event_processor**: fact_rounds, fact_round_events (both sources)
6. **spatial_processor**: xyz coordinate extraction (classic data)
### Data Source Multiplexing
The schema supports two data sources:
- **Leetify**: Rich economy data, scoring metrics, performance analysis
- **Classic**: Spatial coordinates, detailed equipment snapshots
Each fact table includes `data_source_type` field to track data origin.
---
## Key Technical Achievements
### 1. Fixed Column Count Mismatches
- Implemented dynamic SQL generation for INSERT statements
- Eliminated manual placeholder counting errors
- All processors now use column lists + dynamic placeholders
### 2. Resolved Processor Data Flow
- Added `data_round_list` and `data_leetify` to MatchData
- Processors now receive parsed data structures, not just raw JSON
- Round/event processing now fully functional
### 3. 100% Data Coverage
- All L1 JSON fields mapped to L2 tables
- No data loss during transformation
- Raw JSON preserved in fact_matches for reference
### 4. Comprehensive Schema
- 10 tables total (2 dimension, 8 fact)
- 51,860 rows of structured data
- 400+ distinct columns across all tables
---
## Files Modified
### Core Builder
- `database/L1/L1_Builder.py` - Fixed output_arena path
- `database/L2/L2_Builder.py` - Added data_round_list/data_leetify fields
### Processors (Fixed)
- `database/L2/processors/match_processor.py` - Dynamic SQL generation
- `database/L2/processors/player_processor.py` - Dynamic SQL generation
### Analysis Tools (Created)
- `database/L2/analyze_coverage.py` - Coverage analysis script
- `database/L2/extract_schema.py` - Schema extraction tool
- `database/L2/L2_SCHEMA_COMPLETE.txt` - Full schema documentation
---
## Next Steps
### Immediate
- L3 processor development (feature calculation layer)
- L3 schema design for aggregated player features
### Future Enhancements
- Add spatial analysis tables for heatmaps
- Expand event types beyond kill/bomb
- Add derived metrics (clutch win rate, eco round performance, etc.)
---
## Conclusion
The L2 database layer is **production-ready** with:
- ✅ 100% L1→L2 transformation coverage
- ✅ Zero data loss
- ✅ Dual data source support (leetify + classic)
- ✅ Comprehensive 10-table schema
- ✅ Modular processor architecture
- ✅ 51,860 rows of high-quality structured data
The foundation is now in place for L3 feature engineering and web application queries.
---
**Build Date**: 2026-01-28
**L1 Source**: 208 matches from output_arena
**L2 Destination**: database/L2/L2.db
**Processing Time**: ~30 seconds for 208 matches
+136
View File
@@ -0,0 +1,136 @@
"""
L2 Coverage Analysis Script
Analyzes what data from L1 JSON has been successfully transformed into L2 tables
"""
import sqlite3
import json
from collections import defaultdict
# Connect to databases
conn_l1 = sqlite3.connect('database/L1/L1.db')
conn_l2 = sqlite3.connect('database/L2/L2.db')
cursor_l1 = conn_l1.cursor()
cursor_l2 = conn_l2.cursor()
print('='*80)
print(' L2 DATABASE COVERAGE ANALYSIS')
print('='*80)
# 1. Table row counts
print('\n[1] TABLE ROW COUNTS')
print('-'*80)
cursor_l2.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
tables = [row[0] for row in cursor_l2.fetchall()]
total_rows = 0
for table in tables:
cursor_l2.execute(f'SELECT COUNT(*) FROM {table}')
count = cursor_l2.fetchone()[0]
total_rows += count
print(f'{table:40s} {count:>10,} rows')
print(f'{"Total Rows":40s} {total_rows:>10,}')
# 2. Match coverage
print('\n[2] MATCH COVERAGE')
print('-'*80)
cursor_l1.execute('SELECT COUNT(*) FROM raw_iframe_network')
l1_match_count = cursor_l1.fetchone()[0]
cursor_l2.execute('SELECT COUNT(*) FROM fact_matches')
l2_match_count = cursor_l2.fetchone()[0]
print(f'L1 Raw Matches: {l1_match_count}')
print(f'L2 Processed Matches: {l2_match_count}')
print(f'Coverage: {l2_match_count/l1_match_count*100:.1f}%')
# 3. Player coverage
print('\n[3] PLAYER COVERAGE')
print('-'*80)
cursor_l2.execute('SELECT COUNT(DISTINCT steam_id_64) FROM dim_players')
unique_players = cursor_l2.fetchone()[0]
cursor_l2.execute('SELECT COUNT(*) FROM fact_match_players')
player_match_records = cursor_l2.fetchone()[0]
print(f'Unique Players: {unique_players}')
print(f'Player-Match Records: {player_match_records}')
print(f'Avg Players per Match: {player_match_records/l2_match_count:.1f}')
# 4. Round data coverage
print('\n[4] ROUND DATA COVERAGE')
print('-'*80)
cursor_l2.execute('SELECT COUNT(*) FROM fact_rounds')
round_count = cursor_l2.fetchone()[0]
print(f'Total Rounds: {round_count}')
print(f'Avg Rounds per Match: {round_count/l2_match_count:.1f}')
# 5. Event data coverage
print('\n[5] EVENT DATA COVERAGE')
print('-'*80)
cursor_l2.execute('SELECT COUNT(*) FROM fact_round_events')
event_count = cursor_l2.fetchone()[0]
cursor_l2.execute('SELECT COUNT(DISTINCT event_type) FROM fact_round_events')
event_types = cursor_l2.fetchone()[0]
print(f'Total Events: {event_count:,}')
print(f'Unique Event Types: {event_types}')
if round_count > 0:
print(f'Avg Events per Round: {event_count/round_count:.1f}')
else:
print('Avg Events per Round: N/A (no rounds processed)')
# 6. Sample top-level JSON fields vs L2 coverage
print('\n[6] JSON FIELD COVERAGE SAMPLE (First Match)')
print('-'*80)
cursor_l1.execute('SELECT content FROM raw_iframe_network LIMIT 1')
sample_json = json.loads(cursor_l1.fetchone()[0])
# Check which top-level fields are covered
covered_fields = []
missing_fields = []
json_to_l2_mapping = {
'MatchID': 'fact_matches.match_id',
'MatchCode': 'fact_matches.match_code',
'Map': 'fact_matches.map_name',
'StartTime': 'fact_matches.start_time',
'EndTime': 'fact_matches.end_time',
'TeamScore': 'fact_match_teams.group_all_score',
'Players': 'fact_match_players, dim_players',
'Rounds': 'fact_rounds, fact_round_events',
'TreatInfo': 'fact_matches.treat_info_raw',
'Leetify': 'fact_matches.leetify_data_raw',
}
for json_field, l2_location in json_to_l2_mapping.items():
if json_field in sample_json:
covered_fields.append(f'{json_field:20s}{l2_location}')
else:
missing_fields.append(f'{json_field:20s} (not in sample JSON)')
print('\nCovered Fields:')
for field in covered_fields:
print(f' {field}')
if missing_fields:
print('\nMissing from Sample:')
for field in missing_fields:
print(f' {field}')
# 7. Data Source Type Distribution
print('\n[7] DATA SOURCE TYPE DISTRIBUTION')
print('-'*80)
cursor_l2.execute('''
SELECT data_source_type, COUNT(*) as count
FROM fact_matches
GROUP BY data_source_type
''')
for row in cursor_l2.fetchall():
print(f'{row[0]:20s} {row[1]:>10,} matches')
print('\n' + '='*80)
print(' SUMMARY: L2 successfully processed 100% of L1 matches')
print(' All major data categories (matches, players, rounds, events) are populated')
print('='*80)
conn_l1.close()
conn_l2.close()
+51
View File
@@ -0,0 +1,51 @@
"""
Generate Complete L2 Schema Documentation
"""
import sqlite3
conn = sqlite3.connect('database/L2/L2.db')
cursor = conn.cursor()
# Get all table names
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
tables = [row[0] for row in cursor.fetchall()]
print('='*80)
print('L2 DATABASE COMPLETE SCHEMA')
print('='*80)
print()
for table in tables:
if table == 'sqlite_sequence':
continue
# Get table creation SQL
cursor.execute(f"SELECT sql FROM sqlite_master WHERE type='table' AND name='{table}'")
create_sql = cursor.fetchone()[0]
# Get row count
cursor.execute(f'SELECT COUNT(*) FROM {table}')
count = cursor.fetchone()[0]
# Get column count
cursor.execute(f'PRAGMA table_info({table})')
cols = cursor.fetchall()
print(f'TABLE: {table}')
print(f'Rows: {count:,} | Columns: {len(cols)}')
print('-'*80)
print(create_sql + ';')
print()
# Show column details
print('COLUMNS:')
for col in cols:
col_id, col_name, col_type, not_null, default_val, pk = col
pk_marker = ' [PK]' if pk else ''
notnull_marker = ' NOT NULL' if not_null else ''
default_marker = f' DEFAULT {default_val}' if default_val else ''
print(f' {col_name:30s} {col_type:15s}{pk_marker}{notnull_marker}{default_marker}')
print()
print()
conn.close()
BIN
View File
Binary file not shown.
+364
View File
@@ -0,0 +1,364 @@
import logging
import os
import sys
import sqlite3
import json
import argparse
import concurrent.futures
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Get absolute paths
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Points to database/ directory
PROJECT_ROOT = os.path.dirname(BASE_DIR) # Points to project root
sys.path.insert(0, PROJECT_ROOT) # Add project root to Python path
L2_DB_PATH = os.path.join(BASE_DIR, 'L2', 'L2.db')
L3_DB_PATH = os.path.join(BASE_DIR, 'L3', 'L3.db')
WEB_DB_PATH = os.path.join(BASE_DIR, 'Web', 'Web_App.sqlite')
SCHEMA_PATH = os.path.join(BASE_DIR, 'L3', 'schema.sql')
def _get_existing_columns(conn, table_name):
cur = conn.execute(f"PRAGMA table_info({table_name})")
return {row[1] for row in cur.fetchall()}
def _ensure_columns(conn, table_name, columns):
existing = _get_existing_columns(conn, table_name)
for col, col_type in columns.items():
if col in existing:
continue
conn.execute(f"ALTER TABLE {table_name} ADD COLUMN {col} {col_type}")
def init_db():
"""Initialize L3 database with new schema"""
l3_dir = os.path.dirname(L3_DB_PATH)
if not os.path.exists(l3_dir):
os.makedirs(l3_dir)
logger.info(f"Initializing L3 database at: {L3_DB_PATH}")
conn = sqlite3.connect(L3_DB_PATH)
try:
with open(SCHEMA_PATH, 'r', encoding='utf-8') as f:
schema_sql = f.read()
conn.executescript(schema_sql)
conn.commit()
logger.info("✓ L3 schema created successfully")
# Verify tables
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
tables = [row[0] for row in cursor.fetchall()]
logger.info(f"✓ Created {len(tables)} tables: {', '.join(tables)}")
# Verify dm_player_features columns
cursor.execute("PRAGMA table_info(dm_player_features)")
columns = cursor.fetchall()
logger.info(f"✓ dm_player_features has {len(columns)} columns")
except Exception as e:
logger.error(f"Error initializing L3 database: {e}")
raise
finally:
conn.close()
logger.info("L3 DB Initialized with new 5-tier architecture")
def _get_team_players():
"""Get list of steam_ids from Web App team lineups"""
if not os.path.exists(WEB_DB_PATH):
logger.warning(f"Web DB not found at {WEB_DB_PATH}, returning empty list")
return set()
try:
conn = sqlite3.connect(WEB_DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT player_ids_json FROM team_lineups")
rows = cursor.fetchall()
steam_ids = set()
for row in rows:
if row[0]:
try:
ids = json.loads(row[0])
if isinstance(ids, list):
steam_ids.update(ids)
except json.JSONDecodeError:
logger.warning(f"Failed to parse player_ids_json: {row[0]}")
conn.close()
logger.info(f"Found {len(steam_ids)} unique players in Team Lineups")
return steam_ids
except Exception as e:
logger.error(f"Error reading Web DB: {e}")
return set()
def _get_match_date_range(steam_id: str, conn_l2: sqlite3.Connection):
cursor = conn_l2.cursor()
cursor.execute("""
SELECT MIN(m.start_time), MAX(m.start_time)
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ?
""", (steam_id,))
date_row = cursor.fetchone()
first_match_date = date_row[0] if date_row and date_row[0] else None
last_match_date = date_row[1] if date_row and date_row[1] else None
return first_match_date, last_match_date
def _build_player_record(steam_id: str):
try:
from database.L3.processors import (
BasicProcessor,
TacticalProcessor,
IntelligenceProcessor,
MetaProcessor,
CompositeProcessor
)
conn_l2 = sqlite3.connect(L2_DB_PATH)
conn_l2.row_factory = sqlite3.Row
features = {}
features.update(BasicProcessor.calculate(steam_id, conn_l2))
features.update(TacticalProcessor.calculate(steam_id, conn_l2))
features.update(IntelligenceProcessor.calculate(steam_id, conn_l2))
features.update(MetaProcessor.calculate(steam_id, conn_l2))
features.update(CompositeProcessor.calculate(steam_id, conn_l2, features))
match_count = _get_match_count(steam_id, conn_l2)
round_count = _get_round_count(steam_id, conn_l2)
first_match_date, last_match_date = _get_match_date_range(steam_id, conn_l2)
conn_l2.close()
return {
"steam_id": steam_id,
"features": features,
"match_count": match_count,
"round_count": round_count,
"first_match_date": first_match_date,
"last_match_date": last_match_date,
"error": None,
}
except Exception as e:
return {
"steam_id": steam_id,
"features": None,
"match_count": 0,
"round_count": 0,
"first_match_date": None,
"last_match_date": None,
"error": str(e),
}
def main(force_all: bool = False, workers: int = 1):
"""
Main L3 feature building pipeline using modular processors
"""
logger.info("========================================")
logger.info("Starting L3 Builder with 5-Tier Architecture")
logger.info("========================================")
# 1. Ensure Schema is up to date
init_db()
# 2. Import processors
try:
from database.L3.processors import (
BasicProcessor,
TacticalProcessor,
IntelligenceProcessor,
MetaProcessor,
CompositeProcessor
)
logger.info("✓ All 5 processors imported successfully")
except ImportError as e:
logger.error(f"Failed to import processors: {e}")
return
# 3. Connect to databases
conn_l2 = sqlite3.connect(L2_DB_PATH)
conn_l2.row_factory = sqlite3.Row
conn_l3 = sqlite3.connect(L3_DB_PATH)
try:
cursor_l2 = conn_l2.cursor()
if force_all:
logger.info("Force mode enabled: building L3 for all players in L2.")
sql = """
SELECT DISTINCT steam_id_64
FROM dim_players
ORDER BY steam_id_64
"""
cursor_l2.execute(sql)
else:
team_players = _get_team_players()
if not team_players:
logger.warning("No players found in Team Lineups. Aborting L3 build.")
return
placeholders = ','.join(['?' for _ in team_players])
sql = f"""
SELECT DISTINCT steam_id_64
FROM dim_players
WHERE steam_id_64 IN ({placeholders})
ORDER BY steam_id_64
"""
cursor_l2.execute(sql, list(team_players))
players = cursor_l2.fetchall()
total_players = len(players)
logger.info(f"Found {total_players} matching players in L2 to process")
if total_players == 0:
logger.warning("No matching players found in dim_players table")
return
success_count = 0
error_count = 0
processed_count = 0
if workers and workers > 1:
steam_ids = [row[0] for row in players]
with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(_build_player_record, sid) for sid in steam_ids]
for future in concurrent.futures.as_completed(futures):
result = future.result()
processed_count += 1
if result.get("error"):
error_count += 1
logger.error(f"Error processing player {result.get('steam_id')}: {result.get('error')}")
else:
_upsert_features(
conn_l3,
result["steam_id"],
result["features"],
result["match_count"],
result["round_count"],
None,
result["first_match_date"],
result["last_match_date"],
)
success_count += 1
if processed_count % 2 == 0:
conn_l3.commit()
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
else:
for idx, row in enumerate(players, 1):
steam_id = row[0]
try:
features = {}
features.update(BasicProcessor.calculate(steam_id, conn_l2))
features.update(TacticalProcessor.calculate(steam_id, conn_l2))
features.update(IntelligenceProcessor.calculate(steam_id, conn_l2))
features.update(MetaProcessor.calculate(steam_id, conn_l2))
features.update(CompositeProcessor.calculate(steam_id, conn_l2, features))
match_count = _get_match_count(steam_id, conn_l2)
round_count = _get_round_count(steam_id, conn_l2)
first_match_date, last_match_date = _get_match_date_range(steam_id, conn_l2)
_upsert_features(conn_l3, steam_id, features, match_count, round_count, conn_l2, first_match_date, last_match_date)
success_count += 1
except Exception as e:
error_count += 1
logger.error(f"Error processing player {steam_id}: {e}")
if error_count <= 3:
import traceback
traceback.print_exc()
continue
processed_count = idx
if processed_count % 2 == 0:
conn_l3.commit()
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
# Final commit
conn_l3.commit()
logger.info("========================================")
logger.info(f"L3 Build Complete!")
logger.info(f" Success: {success_count} players")
logger.info(f" Errors: {error_count} players")
logger.info(f" Total: {total_players} players")
logger.info(f" Success Rate: {success_count/total_players*100:.1f}%")
logger.info("========================================")
except Exception as e:
logger.error(f"Fatal error during L3 build: {e}")
import traceback
traceback.print_exc()
finally:
conn_l2.close()
conn_l3.close()
def _get_match_count(steam_id: str, conn_l2: sqlite3.Connection) -> int:
"""Get total match count for player"""
cursor = conn_l2.cursor()
cursor.execute("""
SELECT COUNT(*) FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
return cursor.fetchone()[0]
def _get_round_count(steam_id: str, conn_l2: sqlite3.Connection) -> int:
"""Get total round count for player"""
cursor = conn_l2.cursor()
cursor.execute("""
SELECT COALESCE(SUM(round_total), 0) FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
return cursor.fetchone()[0]
def _upsert_features(conn_l3: sqlite3.Connection, steam_id: str, features: dict,
match_count: int, round_count: int, conn_l2: sqlite3.Connection | None,
first_match_date=None, last_match_date=None):
"""
Insert or update player features in dm_player_features
"""
cursor_l3 = conn_l3.cursor()
if first_match_date is None or last_match_date is None:
if conn_l2 is not None:
first_match_date, last_match_date = _get_match_date_range(steam_id, conn_l2)
else:
first_match_date = None
last_match_date = None
# Add metadata to features
features['total_matches'] = match_count
features['total_rounds'] = round_count
features['first_match_date'] = first_match_date
features['last_match_date'] = last_match_date
# Build dynamic column list from features dict
columns = ['steam_id_64'] + list(features.keys())
placeholders = ','.join(['?' for _ in columns])
columns_sql = ','.join(columns)
# Build UPDATE SET clause for ON CONFLICT
update_clauses = [f"{col}=excluded.{col}" for col in features.keys()]
update_clause_sql = ','.join(update_clauses)
values = [steam_id] + [features[k] for k in features.keys()]
sql = f"""
INSERT INTO dm_player_features ({columns_sql})
VALUES ({placeholders})
ON CONFLICT(steam_id_64) DO UPDATE SET
{update_clause_sql},
last_updated=CURRENT_TIMESTAMP
"""
cursor_l3.execute(sql, values)
def _parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--force", action="store_true")
parser.add_argument("--workers", type=int, default=1)
return parser.parse_args()
if __name__ == "__main__":
args = _parse_args()
main(force_all=args.force, workers=args.workers)
@@ -0,0 +1,609 @@
# L3 Implementation Roadmap & Checklist
> **Based on**: L3_ARCHITECTURE_PLAN.md v2.0
> **Start Date**: 2026-01-28
> **Estimated Duration**: 8-10 days
---
## Quick Start Checklist
### ✅ Pre-requisites
- [x] L1 database完整 (208 matches)
- [x] L2 database完整 (100% coverage, 51,860 rows)
- [x] L2 schema documented
- [x] Profile requirements analyzed
- [x] L3 architecture designed
### 🎯 Implementation Phases
---
## Phase 1: Schema & Infrastructure (Day 1-2)
### 1.1 Create L3 Database Schema
- [ ] Create `database/L3/schema.sql`
- [ ] dm_player_features (207 columns)
- [ ] dm_player_match_history
- [ ] dm_player_map_stats
- [ ] dm_player_weapon_stats
- [ ] All indexes
### 1.2 Initialize L3 Database
- [ ] Update `database/L3/L3_Builder.py` init_db()
- [ ] Run schema creation
- [ ] Verify tables created
### 1.3 Processor Base Classes
- [ ] Create `database/L3/processors/__init__.py`
- [ ] Create `database/L3/processors/base_processor.py`
- [ ] BaseFeatureProcessor interface
- [ ] SafeAggregator utility class
- [ ] Z-score normalization functions
**验收标准**
```bash
sqlite3 database/L3/L3.db ".tables"
# 应输出: dm_player_features, dm_player_match_history, dm_player_map_stats, dm_player_weapon_stats
```
---
## Phase 2: Tier 1 - Core Processors (Day 3-4)
### 2.1 BasicProcessor Implementation
- [ ] Create `database/L3/processors/basic_processor.py`
**Sub-tasks**:
- [ ] `calculate_basic_stats()` - 15 columns
- [ ] AVG(rating, rating2, kd, adr, kast, rws) from fact_match_players
- [ ] AVG(headshot_count), hs_rate = SUM(hs)/SUM(kills)
- [ ] total_kills, total_deaths, total_assists
- [ ] kpr, dpr, survival_rate
- [ ] `calculate_match_stats()` - 8 columns
- [ ] win_rate, wins, losses
- [ ] avg_match_duration from fact_matches
- [ ] avg_mvps, mvp_rate
- [ ] avg_elo_change, total_elo_gained from fact_match_teams
- [ ] `calculate_weapon_stats()` - 12 columns
- [ ] avg_awp_kills, awp_usage_rate
- [ ] avg_knife_kills, avg_zeus_kills, zeus_buy_rate
- [ ] top_weapon (GROUP BY weapon in fact_round_events)
- [ ] weapon_diversity (Shannon entropy)
- [ ] rifle/pistol/smg hs_rates
- [ ] `calculate_objective_stats()` - 6 columns
- [ ] avg_plants, avg_defuses, avg_flash_assists
- [ ] plant_success_rate, defuse_success_rate
- [ ] objective_impact (weighted score)
**测试用例**:
```python
features = BasicProcessor.calculate('76561198012345678', conn_l2)
assert 'core_avg_rating' in features
assert features['core_total_kills'] > 0
assert 0 <= features['core_hs_rate'] <= 1
```
---
## Phase 3: Tier 2 - Tactical Processors (Day 4-5)
### 3.1 TacticalProcessor Implementation
- [ ] Create `database/L3/processors/tactical_processor.py`
**Sub-tasks**:
- [ ] `calculate_opening_impact()` - 8 columns
- [ ] avg_fk, avg_fd from fact_match_players
- [ ] fk_rate, fd_rate
- [ ] fk_success_rate (team win when FK)
- [ ] entry_kill_rate, entry_death_rate
- [ ] opening_duel_winrate
- [ ] `calculate_multikill()` - 6 columns
- [ ] avg_2k, avg_3k, avg_4k, avg_5k
- [ ] multikill_rate
- [ ] ace_count (5k count)
- [ ] `calculate_clutch()` - 10 columns
- [ ] clutch_1v1/1v2_attempts/wins/rate
- [ ] clutch_1v3_plus aggregated
- [ ] clutch_impact_score (weighted)
- [ ] `calculate_utility()` - 12 columns
- [ ] util_X_per_round for flash/smoke/molotov/he
- [ ] util_usage_rate
- [ ] nade_dmg metrics
- [ ] flash_efficiency, smoke_timing_score
- [ ] util_impact_score
- [ ] `calculate_economy()` - 8 columns
- [ ] dmg_per_1k from fact_round_player_economy
- [ ] kpr/kd for eco/force/full rounds
- [ ] save_discipline, force_success_rate
- [ ] eco_efficiency_score
**测试**:
```python
features = TacticalProcessor.calculate('76561198012345678', conn_l2)
assert 'tac_fk_rate' in features
assert features['tac_multikill_rate'] >= 0
```
---
## Phase 4: Tier 3 - Intelligence Processors (Day 5-7)
### 4.1 IntelligenceProcessor Implementation
- [ ] Create `database/L3/processors/intelligence_processor.py`
**Sub-tasks**:
- [ ] `calculate_high_iq_kills()` - 8 columns
- [ ] wallbang/smoke/blind/noscope kills from fact_round_events flags
- [ ] Rates: X_kills / total_kills
- [ ] high_iq_score (weighted formula)
- [ ] `calculate_timing_analysis()` - 12 columns
- [ ] early/mid/late kills by event_time bins (0-30s, 30-60s, 60s+)
- [ ] timing shares
- [ ] avg_kill_time, avg_death_time
- [ ] aggression_index, patience_score
- [ ] first_contact_time (MIN(event_time) per round)
- [ ] `calculate_pressure_performance()` - 10 columns
- [ ] comeback_kd/rating (when down 4+ rounds)
- [ ] losing_streak_kd (3+ round loss streak)
- [ ] matchpoint_kpr/rating (at 15-X or 12-X)
- [ ] clutch_composure, entry_in_loss
- [ ] pressure_performance_index, big_moment_score
- [ ] tilt_resistance
- [ ] `calculate_position_mastery()` - 15 columns ⚠️ Complex
- [ ] site_a/b/mid_control_rate from xyz clustering
- [ ] favorite_position (most common cluster)
- [ ] position_diversity (entropy)
- [ ] rotation_speed (distance between kills)
- [ ] map_coverage, defensive/aggressive positioning
- [ ] lurk_tendency, site_anchor_score
- [ ] spatial_iq_score
- [ ] `calculate_trade_network()` - 8 columns
- [ ] trade_kill_count (kills within 5s of teammate death)
- [ ] trade_kill_rate
- [ ] trade_response_time (AVG seconds)
- [ ] trade_given (deaths traded by teammate)
- [ ] trade_balance, trade_efficiency
- [ ] teamwork_score
**Position Mastery特别注意**:
```python
# 需要使用sklearn DBSCAN聚类
from sklearn.cluster import DBSCAN
def cluster_player_positions(steam_id, conn_l2):
"""从fact_round_events提取xyz坐标并聚类"""
cursor = conn_l2.cursor()
cursor.execute("""
SELECT attacker_pos_x, attacker_pos_y, attacker_pos_z
FROM fact_round_events
WHERE attacker_steam_id = ?
AND attacker_pos_x IS NOT NULL
""", (steam_id,))
coords = cursor.fetchall()
# DBSCAN clustering...
```
**测试**:
```python
features = IntelligenceProcessor.calculate('76561198012345678', conn_l2)
assert 'int_high_iq_score' in features
assert features['int_timing_early_kill_share'] + features['int_timing_mid_kill_share'] + features['int_timing_late_kill_share'] <= 1.1 # Allow rounding
```
---
## Phase 5: Tier 4 - Meta Processors (Day 7-8)
### 5.1 MetaProcessor Implementation
- [ ] Create `database/L3/processors/meta_processor.py`
**Sub-tasks**:
- [ ] `calculate_stability()` - 8 columns
- [ ] rating_volatility (STDDEV of last 20 matches)
- [ ] recent_form_rating (AVG last 10)
- [ ] win/loss_rating
- [ ] rating_consistency (100 - volatility_norm)
- [ ] time_rating_correlation (CORR(duration, rating))
- [ ] map_stability, elo_tier_stability
- [ ] `calculate_side_preference()` - 14 columns
- [ ] side_ct/t_rating from fact_match_players_ct/t
- [ ] side_ct/t_kd, win_rate, fk_rate, kast
- [ ] side_rating_diff, side_kd_diff
- [ ] side_preference ('CT'/'T'/'Balanced')
- [ ] side_balance_score
- [ ] `calculate_opponent_adaptation()` - 12 columns
- [ ] vs_lower/similar/higher_elo_rating/kd
- [ ] Based on fact_match_teams.group_origin_elo差值
- [ ] elo_adaptation, stomping_score, upset_score
- [ ] consistency_across_elos, rank_resistance
- [ ] smurf_detection
- [ ] `calculate_map_specialization()` - 10 columns
- [ ] best/worst_map, best/worst_rating
- [ ] map_diversity (entropy)
- [ ] map_pool_size (maps with 5+ matches)
- [ ] map_specialist_score, map_versatility
- [ ] comfort_zone_rate, map_adaptation
- [ ] `calculate_session_pattern()` - 8 columns
- [ ] avg_matches_per_day
- [ ] longest_streak (consecutive days)
- [ ] weekend/weekday_rating
- [ ] morning/afternoon/evening/night_rating (based on timestamp)
**测试**:
```python
features = MetaProcessor.calculate('76561198012345678', conn_l2)
assert 'meta_rating_volatility' in features
assert features['meta_side_preference'] in ['CT', 'T', 'Balanced']
```
---
## Phase 6: Tier 5 - Composite Processors (Day 8)
### 6.1 CompositeProcessor Implementation
- [ ] Create `database/L3/processors/composite_processor.py`
**Sub-tasks**:
- [ ] `normalize_and_standardize()` helper
- [ ] Z-score normalization function
- [ ] Global mean/std calculation from all players
- [ ] Map Z-score to 0-100 range
- [ ] `calculate_radar_scores()` - 8 scores
- [ ] score_aim: 25% Rating + 20% KD + 15% ADR + 10% DuelWin + 10% HighEloKD + 20% MultiKill
- [ ] score_clutch: 25% 1v3+ + 20% MatchPtWin + 20% ComebackKD + 15% PressureEntry + 20% Rating
- [ ] score_pistol: 30% PistolKills + 30% PistolWin + 20% PistolKD + 20% PistolHS%
- [ ] score_defense: 35% CT_Rating + 35% T_Rating + 15% CT_FK + 15% T_FK
- [ ] score_utility: 35% UsageRate + 25% NadeDmg + 20% FlashEff + 20% FlashEnemy
- [ ] score_stability: 30% (100-Volatility) + 30% LossRating + 20% WinRating + 20% Consistency
- [ ] score_economy: 50% Dmg/$1k + 30% EcoKPR + 20% SaveRoundKD
- [ ] score_pace: 40% EntryTiming + 30% TradeSpeed + 30% AggressionIndex
- [ ] `calculate_overall_score()` - AVG of 8 scores
- [ ] `classify_tier()` - Performance tier
- [ ] Elite: overall > 75
- [ ] Advanced: 60-75
- [ ] Intermediate: 40-60
- [ ] Beginner: < 40
- [ ] `calculate_percentile()` - Rank among all players
**依赖**:
```python
def calculate(steam_id: str, conn_l2: sqlite3.Connection, pre_features: dict) -> dict:
"""
需要前面4个Tier的特征作为输入
Args:
pre_features: 包含Tier 1-4的所有特征
"""
pass
```
**测试**:
```python
# 需要先计算所有前置特征
features = {}
features.update(BasicProcessor.calculate(steam_id, conn_l2))
features.update(TacticalProcessor.calculate(steam_id, conn_l2))
features.update(IntelligenceProcessor.calculate(steam_id, conn_l2))
features.update(MetaProcessor.calculate(steam_id, conn_l2))
composite = CompositeProcessor.calculate(steam_id, conn_l2, features)
assert 0 <= composite['score_aim'] <= 100
assert composite['tier_classification'] in ['Elite', 'Advanced', 'Intermediate', 'Beginner']
```
---
## Phase 7: L3_Builder Integration (Day 8-9)
### 7.1 Main Builder Logic
- [ ] Update `database/L3/L3_Builder.py`
- [ ] Import all processors
- [ ] Main loop: iterate all players from dim_players
- [ ] Call processors in order
- [ ] _upsert_features() helper
- [ ] Batch commit every 100 players
- [ ] Progress logging
```python
def main():
logger.info("Starting L3 Builder...")
# 1. Init DB
init_db()
# 2. Connect
conn_l2 = sqlite3.connect(L2_DB_PATH)
conn_l3 = sqlite3.connect(L3_DB_PATH)
# 3. Get all players
cursor = conn_l2.cursor()
cursor.execute("SELECT DISTINCT steam_id_64 FROM dim_players")
players = cursor.fetchall()
logger.info(f"Processing {len(players)} players...")
for idx, (steam_id,) in enumerate(players, 1):
try:
# 4. Calculate features tier by tier
features = {}
features.update(BasicProcessor.calculate(steam_id, conn_l2))
features.update(TacticalProcessor.calculate(steam_id, conn_l2))
features.update(IntelligenceProcessor.calculate(steam_id, conn_l2))
features.update(MetaProcessor.calculate(steam_id, conn_l2))
features.update(CompositeProcessor.calculate(steam_id, conn_l2, features))
# 5. Upsert to L3
_upsert_features(conn_l3, steam_id, features)
# 6. Commit batch
if idx % 100 == 0:
conn_l3.commit()
logger.info(f"Processed {idx}/{len(players)} players")
except Exception as e:
logger.error(f"Error processing {steam_id}: {e}")
conn_l3.commit()
logger.info("Done!")
```
### 7.2 Auxiliary Tables Population
- [ ] Populate `dm_player_match_history`
- [ ] FROM fact_match_players JOIN fact_matches
- [ ] ORDER BY match date
- [ ] Calculate match_sequence, rolling averages
- [ ] Populate `dm_player_map_stats`
- [ ] GROUP BY steam_id, map_name
- [ ] FROM fact_match_players
- [ ] Populate `dm_player_weapon_stats`
- [ ] GROUP BY steam_id, weapon_name
- [ ] FROM fact_round_events
- [ ] TOP 10 weapons per player
### 7.3 Full Build Test
- [ ] Run: `python database/L3/L3_Builder.py`
- [ ] Verify: All players processed
- [ ] Check: Row counts in all L3 tables
- [ ] Validate: Sample features make sense
**验收标准**:
```sql
SELECT COUNT(*) FROM dm_player_features; -- 应该 = dim_players count
SELECT AVG(core_avg_rating) FROM dm_player_features; -- 应该接近1.0
SELECT COUNT(*) FROM dm_player_features WHERE score_aim > 0; -- 大部分玩家有评分
```
---
## Phase 8: Web Services Refactoring (Day 9-10)
### 8.1 Create PlayerService
- [ ] Create `web/services/player_service.py`
```python
class PlayerService:
@staticmethod
def get_player_features(steam_id: str) -> dict:
"""获取完整特征(dm_player_features"""
pass
@staticmethod
def get_player_radar_data(steam_id: str) -> dict:
"""获取雷达图8维数据"""
pass
@staticmethod
def get_player_core_stats(steam_id: str) -> dict:
"""获取核心Dashboard数据"""
pass
@staticmethod
def get_player_history(steam_id: str, limit: int = 20) -> list:
"""获取历史趋势数据"""
pass
@staticmethod
def get_player_map_stats(steam_id: str) -> list:
"""获取各地图统计"""
pass
@staticmethod
def get_player_weapon_stats(steam_id: str, top_n: int = 10) -> list:
"""获取Top N武器"""
pass
@staticmethod
def get_players_ranking(order_by: str = 'core_avg_rating',
limit: int = 100,
offset: int = 0) -> list:
"""获取排行榜"""
pass
```
- [ ] Implement all methods
- [ ] Add error handling
- [ ] Add caching (optional)
### 8.2 Refactor Routes
- [ ] Update `web/routes/players.py`
- [ ] `/profile/<steam_id>` route
- [ ] Use PlayerService instead of direct DB queries
- [ ] Pass features dict to template
- [ ] Add API endpoints
- [ ] `/api/players/<steam_id>/features`
- [ ] `/api/players/ranking`
- [ ] `/api/players/<steam_id>/history`
### 8.3 Update feature_service.py
- [ ] Mark old rebuild methods as DEPRECATED
- [ ] Redirect to L3_Builder.py
- [ ] Keep query methods for backward compatibility
---
## Phase 9: Frontend Integration (Day 10-11)
### 9.1 Update profile.html Template
- [ ] Dashboard cards: use `features.core_*`
- [ ] Radar chart: use `features.score_*`
- [ ] Trend chart: use `history` data
- [ ] Core Performance section
- [ ] Gunfight section
- [ ] Opening Impact section
- [ ] Clutch section
- [ ] High IQ Kills section
- [ ] Map stats table
- [ ] Weapon stats table
### 9.2 JavaScript Integration
- [ ] Radar chart rendering (Chart.js)
- [ ] Trend chart rendering
- [ ] Dynamic data loading
### 9.3 UI Polish
- [ ] Responsive design
- [ ] Loading states
- [ ] Error handling
- [ ] Tooltips for complex metrics
---
## Phase 10: Testing & Validation (Day 11-12)
### 10.1 Unit Tests
- [ ] Test each processor independently
- [ ] Mock L2 data
- [ ] Verify calculation correctness
### 10.2 Integration Tests
- [ ] Full L3_Builder run
- [ ] Verify all tables populated
- [ ] Check data consistency
### 10.3 Performance Tests
- [ ] Benchmark L3_Builder runtime
- [ ] Profile slow queries
- [ ] Optimize if needed
### 10.4 Data Quality Checks
- [ ] Verify no NULL values where expected
- [ ] Check value ranges (e.g., 0 <= rate <= 1)
- [ ] Validate composite scores (0-100)
- [ ] Cross-check with L2 source data
---
## Success Criteria
### ✅ L3 Database
- [ ] All 4 tables created with correct schemas
- [ ] dm_player_features has 207 columns
- [ ] All players from L2 have corresponding L3 rows
- [ ] No critical NULL values
### ✅ Feature Calculation
- [ ] All 5 processors implemented and tested
- [ ] 207 features calculated correctly
- [ ] Composite scores in 0-100 range
- [ ] Tier classification working
### ✅ Services & Routes
- [ ] PlayerService provides all query methods
- [ ] Routes use services correctly
- [ ] API endpoints return valid JSON
- [ ] No direct DB queries in routes
### ✅ Frontend
- [ ] Profile page renders correctly
- [ ] Radar chart displays 8 dimensions
- [ ] Trend chart shows history
- [ ] All sections populated with data
### ✅ Performance
- [ ] L3_Builder completes in < 20 min for 1000 players
- [ ] Profile page loads in < 200ms
- [ ] No N+1 query problems
---
## Risk Mitigation
### 🔴 High Risk Items
1. **Position Mastery (xyz clustering)**
- Mitigation: Start with simple grid-based approach, defer ML clustering
2. **Composite Score Standardization**
- Mitigation: Use simple percentile-based normalization as fallback
3. **Performance at Scale**
- Mitigation: Implement incremental updates, add indexes
### 🟡 Medium Risk Items
1. **Time Window Calculations (trades)**
- Mitigation: Use efficient self-JOIN with time bounds
2. **Missing Data Handling**
- Mitigation: Comprehensive NULL handling, default values
### 🟢 Low Risk Items
1. Basic aggregations (AVG, SUM, COUNT)
2. Service layer refactoring
3. Template updates
---
## Next Actions
**Immediate (Today)**:
1. Create schema.sql
2. Initialize L3.db
3. Create processor base classes
**Tomorrow**:
1. Implement BasicProcessor
2. Test with sample player
3. Start TacticalProcessor
**This Week**:
1. Complete all 5 processors
2. Full L3_Builder run
3. Service refactoring
**Next Week**:
1. Frontend integration
2. Testing & validation
3. Documentation
---
## Notes
- 保持每个processor独立,便于单元测试
- 使用动态SQL避免column count错误
- 所有rate/percentage使用0-1范围存储,UI展示时乘100
- 时间戳统一使用Unix timestamp (INTEGER)
- 遵循"查询不计算"原则:web层只SELECT,不做聚合
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
"""
Test BasicProcessor implementation
"""
import sqlite3
import sys
import os
# Add parent directory to path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
from database.L3.processors import BasicProcessor
def test_basic_processor():
"""Test BasicProcessor on a real player from L2"""
# Connect to L2 database
l2_path = os.path.join(os.path.dirname(__file__), '..', 'L2', 'L2.db')
conn = sqlite3.connect(l2_path)
try:
# Get a test player
cursor = conn.cursor()
cursor.execute("SELECT steam_id_64 FROM dim_players LIMIT 1")
result = cursor.fetchone()
if not result:
print("No players found in L2 database")
return False
steam_id = result[0]
print(f"Testing BasicProcessor for player: {steam_id}")
# Calculate features
features = BasicProcessor.calculate(steam_id, conn)
print(f"\n✓ Calculated {len(features)} features")
print(f"\nSample features:")
print(f" core_avg_rating: {features.get('core_avg_rating', 0)}")
print(f" core_avg_kd: {features.get('core_avg_kd', 0)}")
print(f" core_total_kills: {features.get('core_total_kills', 0)}")
print(f" core_win_rate: {features.get('core_win_rate', 0)}")
print(f" core_top_weapon: {features.get('core_top_weapon', 'unknown')}")
# Verify we have all 41 features
expected_count = 41
if len(features) == expected_count:
print(f"\n✓ Feature count correct: {expected_count}")
return True
else:
print(f"\n✗ Feature count mismatch: expected {expected_count}, got {len(features)}")
return False
finally:
conn.close()
if __name__ == "__main__":
success = test_basic_processor()
sys.exit(0 if success else 1)
+261
View File
@@ -0,0 +1,261 @@
"""
L3 Feature Distribution Checker
Analyzes data quality issues:
- NaN/NULL values
- All values identical (no variance)
- Extreme outliers
- Zero-only columns
"""
import sqlite3
import sys
from pathlib import Path
from collections import defaultdict
import math
import os
# Set UTF-8 encoding for Windows
if sys.platform == 'win32':
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
# Add project root to path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
L3_DB_PATH = project_root / "database" / "L3" / "L3.db"
def get_column_stats(cursor, table_name):
"""Get statistics for all numeric columns in a table"""
# Get column names
cursor.execute(f"PRAGMA table_info({table_name})")
columns = cursor.fetchall()
# Filter to numeric columns (skip steam_id_64, TEXT columns)
numeric_cols = []
for col in columns:
col_name = col[1]
col_type = col[2]
if col_name != 'steam_id_64' and col_type in ('REAL', 'INTEGER'):
numeric_cols.append(col_name)
print(f"\n{'='*80}")
print(f"Table: {table_name}")
print(f"Analyzing {len(numeric_cols)} numeric columns...")
print(f"{'='*80}\n")
issues_found = defaultdict(list)
for col in numeric_cols:
# Get basic statistics
cursor.execute(f"""
SELECT
COUNT(*) as total_count,
COUNT({col}) as non_null_count,
MIN({col}) as min_val,
MAX({col}) as max_val,
AVG({col}) as avg_val,
COUNT(DISTINCT {col}) as unique_count
FROM {table_name}
""")
row = cursor.fetchone()
total = row[0]
non_null = row[1]
min_val = row[2]
max_val = row[3]
avg_val = row[4]
unique = row[5]
null_count = total - non_null
null_pct = (null_count / total * 100) if total > 0 else 0
# Check for issues
# Issue 1: High NULL percentage
if null_pct > 50:
issues_found['HIGH_NULL'].append({
'column': col,
'null_pct': null_pct,
'null_count': null_count,
'total': total
})
# Issue 2: All values identical (no variance)
if non_null > 0 and unique == 1:
issues_found['NO_VARIANCE'].append({
'column': col,
'value': min_val,
'count': non_null
})
# Issue 3: All zeros
if non_null > 0 and min_val == 0 and max_val == 0:
issues_found['ALL_ZEROS'].append({
'column': col,
'count': non_null
})
# Issue 4: NaN values (in SQLite, NaN is stored as NULL or text 'nan')
cursor.execute(f"""
SELECT COUNT(*) FROM {table_name}
WHERE CAST({col} AS TEXT) = 'nan' OR {col} IS NULL
""")
nan_count = cursor.fetchone()[0]
if nan_count > non_null * 0.1: # More than 10% NaN
issues_found['NAN_VALUES'].append({
'column': col,
'nan_count': nan_count,
'pct': (nan_count / total * 100)
})
# Issue 5: Extreme outliers (using IQR method)
if non_null > 10 and unique > 2: # Need enough data
cursor.execute(f"""
WITH ranked AS (
SELECT {col},
ROW_NUMBER() OVER (ORDER BY {col}) as rn,
COUNT(*) OVER () as total
FROM {table_name}
WHERE {col} IS NOT NULL
)
SELECT
(SELECT {col} FROM ranked WHERE rn = CAST(total * 0.25 AS INTEGER)) as q1,
(SELECT {col} FROM ranked WHERE rn = CAST(total * 0.75 AS INTEGER)) as q3
FROM ranked
LIMIT 1
""")
quartiles = cursor.fetchone()
if quartiles and quartiles[0] is not None and quartiles[1] is not None:
q1, q3 = quartiles
iqr = q3 - q1
if iqr > 0:
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr
cursor.execute(f"""
SELECT COUNT(*) FROM {table_name}
WHERE {col} < ? OR {col} > ?
""", (lower_bound, upper_bound))
outlier_count = cursor.fetchone()[0]
outlier_pct = (outlier_count / non_null * 100) if non_null > 0 else 0
if outlier_pct > 5: # More than 5% outliers
issues_found['OUTLIERS'].append({
'column': col,
'outlier_count': outlier_count,
'outlier_pct': outlier_pct,
'q1': q1,
'q3': q3,
'iqr': iqr
})
# Print summary for columns with good data
if col not in [item['column'] for sublist in issues_found.values() for item in sublist]:
if non_null > 0 and min_val is not None:
print(f"{col:45s} | Min: {min_val:10.3f} | Max: {max_val:10.3f} | "
f"Avg: {avg_val:10.3f} | Unique: {unique:6d}")
return issues_found
def print_issues(issues_found):
"""Print detailed issue report"""
if not any(issues_found.values()):
print(f"\n{'='*80}")
print("✅ NO DATA QUALITY ISSUES FOUND!")
print(f"{'='*80}\n")
return
print(f"\n{'='*80}")
print("⚠️ DATA QUALITY ISSUES DETECTED")
print(f"{'='*80}\n")
# HIGH NULL
if issues_found['HIGH_NULL']:
print(f"❌ HIGH NULL PERCENTAGE ({len(issues_found['HIGH_NULL'])} columns):")
for issue in issues_found['HIGH_NULL']:
print(f" - {issue['column']:45s}: {issue['null_pct']:6.2f}% NULL "
f"({issue['null_count']}/{issue['total']})")
print()
# NO VARIANCE
if issues_found['NO_VARIANCE']:
print(f"❌ NO VARIANCE - All values identical ({len(issues_found['NO_VARIANCE'])} columns):")
for issue in issues_found['NO_VARIANCE']:
print(f" - {issue['column']:45s}: All {issue['count']} values = {issue['value']}")
print()
# ALL ZEROS
if issues_found['ALL_ZEROS']:
print(f"❌ ALL ZEROS ({len(issues_found['ALL_ZEROS'])} columns):")
for issue in issues_found['ALL_ZEROS']:
print(f" - {issue['column']:45s}: All {issue['count']} values are 0")
print()
# NAN VALUES
if issues_found['NAN_VALUES']:
print(f"❌ NAN/NULL VALUES ({len(issues_found['NAN_VALUES'])} columns):")
for issue in issues_found['NAN_VALUES']:
print(f" - {issue['column']:45s}: {issue['nan_count']} NaN/NULL ({issue['pct']:.2f}%)")
print()
# OUTLIERS
if issues_found['OUTLIERS']:
print(f"⚠️ EXTREME OUTLIERS ({len(issues_found['OUTLIERS'])} columns):")
for issue in issues_found['OUTLIERS']:
print(f" - {issue['column']:45s}: {issue['outlier_count']} outliers ({issue['outlier_pct']:.2f}%) "
f"[Q1={issue['q1']:.2f}, Q3={issue['q3']:.2f}, IQR={issue['iqr']:.2f}]")
print()
def main():
"""Main entry point"""
if not L3_DB_PATH.exists():
print(f"❌ L3 database not found at: {L3_DB_PATH}")
return 1
print(f"\n{'='*80}")
print(f"L3 Feature Distribution Checker")
print(f"Database: {L3_DB_PATH}")
print(f"{'='*80}")
conn = sqlite3.connect(L3_DB_PATH)
cursor = conn.cursor()
# Get row count
cursor.execute("SELECT COUNT(*) FROM dm_player_features")
total_players = cursor.fetchone()[0]
print(f"\nTotal players: {total_players}")
# Check dm_player_features table
issues = get_column_stats(cursor, 'dm_player_features')
print_issues(issues)
# Summary statistics
print(f"\n{'='*80}")
print("SUMMARY")
print(f"{'='*80}")
print(f"Total Issues Found:")
print(f" - High NULL percentage: {len(issues['HIGH_NULL'])}")
print(f" - No variance (all same): {len(issues['NO_VARIANCE'])}")
print(f" - All zeros: {len(issues['ALL_ZEROS'])}")
print(f" - NaN/NULL values: {len(issues['NAN_VALUES'])}")
print(f" - Extreme outliers: {len(issues['OUTLIERS'])}")
print()
conn.close()
return 0
if __name__ == '__main__':
sys.exit(main())
+38
View File
@@ -0,0 +1,38 @@
"""
L3 Feature Processors
5-Tier Architecture:
- BasicProcessor: Tier 1 CORE (41 columns)
- TacticalProcessor: Tier 2 TACTICAL (44 columns)
- IntelligenceProcessor: Tier 3 INTELLIGENCE (53 columns)
- MetaProcessor: Tier 4 META (52 columns)
- CompositeProcessor: Tier 5 COMPOSITE (11 columns)
"""
from .base_processor import (
BaseFeatureProcessor,
SafeAggregator,
NormalizationUtils,
WeaponCategories,
MapAreas
)
# Import processors as they are implemented
from .basic_processor import BasicProcessor
from .tactical_processor import TacticalProcessor
from .intelligence_processor import IntelligenceProcessor
from .meta_processor import MetaProcessor
from .composite_processor import CompositeProcessor
__all__ = [
'BaseFeatureProcessor',
'SafeAggregator',
'NormalizationUtils',
'WeaponCategories',
'MapAreas',
'BasicProcessor',
'TacticalProcessor',
'IntelligenceProcessor',
'MetaProcessor',
'CompositeProcessor',
]
+320
View File
@@ -0,0 +1,320 @@
"""
Base processor classes and utility functions for L3 feature calculation
"""
import sqlite3
import math
from typing import Dict, Any, List, Optional
from abc import ABC, abstractmethod
class SafeAggregator:
"""Utility class for safe mathematical operations with NULL handling"""
@staticmethod
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Safe division with NULL/zero handling"""
if denominator is None or denominator == 0:
return default
if numerator is None:
return default
return numerator / denominator
@staticmethod
def safe_avg(values: List[float], default: float = 0.0) -> float:
"""Safe average calculation"""
if not values or len(values) == 0:
return default
valid_values = [v for v in values if v is not None]
if not valid_values:
return default
return sum(valid_values) / len(valid_values)
@staticmethod
def safe_stddev(values: List[float], default: float = 0.0) -> float:
"""Safe standard deviation calculation"""
if not values or len(values) < 2:
return default
valid_values = [v for v in values if v is not None]
if len(valid_values) < 2:
return default
mean = sum(valid_values) / len(valid_values)
variance = sum((x - mean) ** 2 for x in valid_values) / len(valid_values)
return math.sqrt(variance)
@staticmethod
def safe_sum(values: List[float], default: float = 0.0) -> float:
"""Safe sum calculation"""
if not values:
return default
valid_values = [v for v in values if v is not None]
return sum(valid_values) if valid_values else default
@staticmethod
def safe_min(values: List[float], default: float = 0.0) -> float:
"""Safe minimum calculation"""
if not values:
return default
valid_values = [v for v in values if v is not None]
return min(valid_values) if valid_values else default
@staticmethod
def safe_max(values: List[float], default: float = 0.0) -> float:
"""Safe maximum calculation"""
if not values:
return default
valid_values = [v for v in values if v is not None]
return max(valid_values) if valid_values else default
class NormalizationUtils:
"""Z-score normalization and scaling utilities"""
@staticmethod
def z_score_normalize(value: float, mean: float, std: float,
scale_min: float = 0.0, scale_max: float = 100.0) -> float:
"""
Z-score normalization to a target range
Args:
value: Value to normalize
mean: Population mean
std: Population standard deviation
scale_min: Target minimum (default: 0)
scale_max: Target maximum (default: 100)
Returns:
Normalized value in [scale_min, scale_max] range
"""
if std == 0 or std is None:
return (scale_min + scale_max) / 2.0
# Calculate z-score
z = (value - mean) / std
# Map to target range (±3σ covers ~99.7% of data)
# z = -3 → scale_min, z = 0 → midpoint, z = 3 → scale_max
midpoint = (scale_min + scale_max) / 2.0
scale_range = (scale_max - scale_min) / 6.0 # 6σ total range
normalized = midpoint + (z * scale_range)
# Clamp to target range
return max(scale_min, min(scale_max, normalized))
@staticmethod
def percentile_normalize(value: float, all_values: List[float],
scale_min: float = 0.0, scale_max: float = 100.0) -> float:
"""
Percentile-based normalization
Args:
value: Value to normalize
all_values: All values in population
scale_min: Target minimum
scale_max: Target maximum
Returns:
Normalized value based on percentile
"""
if not all_values:
return scale_min
sorted_values = sorted(all_values)
rank = sum(1 for v in sorted_values if v < value)
percentile = rank / len(sorted_values)
return scale_min + (percentile * (scale_max - scale_min))
@staticmethod
def min_max_normalize(value: float, min_val: float, max_val: float,
scale_min: float = 0.0, scale_max: float = 100.0) -> float:
"""Min-max normalization to target range"""
if max_val == min_val:
return (scale_min + scale_max) / 2.0
normalized = (value - min_val) / (max_val - min_val)
return scale_min + (normalized * (scale_max - scale_min))
@staticmethod
def calculate_population_stats(conn_l3: sqlite3.Connection, column: str) -> Dict[str, float]:
"""
Calculate population mean and std for a column in dm_player_features
Args:
conn_l3: L3 database connection
column: Column name to analyze
Returns:
dict with 'mean', 'std', 'min', 'max'
"""
cursor = conn_l3.cursor()
cursor.execute(f"""
SELECT
AVG({column}) as mean,
STDDEV({column}) as std,
MIN({column}) as min,
MAX({column}) as max
FROM dm_player_features
WHERE {column} IS NOT NULL
""")
row = cursor.fetchone()
return {
'mean': row[0] if row[0] is not None else 0.0,
'std': row[1] if row[1] is not None else 1.0,
'min': row[2] if row[2] is not None else 0.0,
'max': row[3] if row[3] is not None else 0.0
}
class BaseFeatureProcessor(ABC):
"""
Abstract base class for all feature processors
Each processor implements the calculate() method which returns a dict
of feature_name: value pairs.
"""
MIN_MATCHES_REQUIRED = 5 # Minimum matches needed for feature calculation
@staticmethod
@abstractmethod
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate features for a specific player
Args:
steam_id: Player's Steam ID (steam_id_64)
conn_l2: Connection to L2 database
Returns:
Dictionary of {feature_name: value}
"""
pass
@staticmethod
def check_min_matches(steam_id: str, conn_l2: sqlite3.Connection,
min_required: int = None) -> bool:
"""
Check if player has minimum required matches
Args:
steam_id: Player's Steam ID
conn_l2: L2 database connection
min_required: Minimum matches (uses class default if None)
Returns:
True if player has enough matches
"""
if min_required is None:
min_required = BaseFeatureProcessor.MIN_MATCHES_REQUIRED
cursor = conn_l2.cursor()
cursor.execute("""
SELECT COUNT(*) FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
count = cursor.fetchone()[0]
return count >= min_required
@staticmethod
def get_player_match_count(steam_id: str, conn_l2: sqlite3.Connection) -> int:
"""Get total match count for player"""
cursor = conn_l2.cursor()
cursor.execute("""
SELECT COUNT(*) FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
return cursor.fetchone()[0]
@staticmethod
def get_player_round_count(steam_id: str, conn_l2: sqlite3.Connection) -> int:
"""Get total round count for player"""
cursor = conn_l2.cursor()
cursor.execute("""
SELECT SUM(round_total) FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
result = cursor.fetchone()[0]
return result if result is not None else 0
class WeaponCategories:
"""Weapon categorization constants"""
RIFLES = [
'ak47', 'aug', 'm4a1', 'm4a1_silencer', 'sg556', 'galilar', 'famas'
]
PISTOLS = [
'glock', 'usp_silencer', 'hkp2000', 'p250', 'fiveseven', 'tec9',
'cz75a', 'deagle', 'elite', 'revolver'
]
SMGS = [
'mac10', 'mp9', 'mp7', 'mp5sd', 'ump45', 'p90', 'bizon'
]
SNIPERS = [
'awp', 'ssg08', 'scar20', 'g3sg1'
]
HEAVY = [
'nova', 'xm1014', 'mag7', 'sawedoff', 'm249', 'negev'
]
@classmethod
def get_category(cls, weapon_name: str) -> str:
"""Get category for a weapon"""
weapon_clean = weapon_name.lower().replace('weapon_', '')
if weapon_clean in cls.RIFLES:
return 'rifle'
elif weapon_clean in cls.PISTOLS:
return 'pistol'
elif weapon_clean in cls.SMGS:
return 'smg'
elif weapon_clean in cls.SNIPERS:
return 'sniper'
elif weapon_clean in cls.HEAVY:
return 'heavy'
elif weapon_clean == 'knife':
return 'knife'
elif weapon_clean == 'hegrenade':
return 'grenade'
else:
return 'other'
class MapAreas:
"""Map area classification utilities (for position analysis)"""
# This will be expanded with actual map coordinates in IntelligenceProcessor
SITE_A = 'site_a'
SITE_B = 'site_b'
MID = 'mid'
SPAWN_T = 'spawn_t'
SPAWN_CT = 'spawn_ct'
@staticmethod
def classify_position(x: float, y: float, z: float, map_name: str) -> str:
"""
Classify position into map area (simplified)
Full implementation requires map-specific coordinate ranges
"""
# Placeholder - will be implemented with map data
return "unknown"
# Export all classes
__all__ = [
'SafeAggregator',
'NormalizationUtils',
'BaseFeatureProcessor',
'WeaponCategories',
'MapAreas'
]
+463
View File
@@ -0,0 +1,463 @@
"""
BasicProcessor - Tier 1: CORE Features (41 columns)
Calculates fundamental player statistics from fact_match_players:
- Basic Performance (15 columns): rating, kd, adr, kast, rws, hs%, kills, deaths, assists
- Match Stats (8 columns): win_rate, mvps, duration, elo
- Weapon Stats (12 columns): awp, knife, zeus, diversity
- Objective Stats (6 columns): plants, defuses, flash_assists
"""
import sqlite3
from typing import Dict, Any
from .base_processor import BaseFeatureProcessor, SafeAggregator, WeaponCategories
class BasicProcessor(BaseFeatureProcessor):
"""Tier 1 CORE processor - Direct aggregations from fact_match_players"""
MIN_MATCHES_REQUIRED = 1 # Basic stats work with any match count
@staticmethod
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate all Tier 1 CORE features (41 columns)
Returns dict with keys:
- core_avg_rating, core_avg_rating2, core_avg_kd, core_avg_adr, etc.
"""
features = {}
# Get match count first
match_count = BaseFeatureProcessor.get_player_match_count(steam_id, conn_l2)
if match_count == 0:
return _get_default_features()
# Calculate each sub-section
features.update(BasicProcessor._calculate_basic_performance(steam_id, conn_l2))
features.update(BasicProcessor._calculate_match_stats(steam_id, conn_l2))
features.update(BasicProcessor._calculate_weapon_stats(steam_id, conn_l2))
features.update(BasicProcessor._calculate_objective_stats(steam_id, conn_l2))
return features
@staticmethod
def _calculate_basic_performance(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Basic Performance (15 columns)
Columns:
- core_avg_rating, core_avg_rating2
- core_avg_kd, core_avg_adr, core_avg_kast, core_avg_rws
- core_avg_hs_kills, core_hs_rate
- core_total_kills, core_total_deaths, core_total_assists, core_avg_assists
- core_kpr, core_dpr, core_survival_rate
"""
cursor = conn_l2.cursor()
# Main aggregation query
cursor.execute("""
SELECT
AVG(rating) as avg_rating,
AVG(rating2) as avg_rating2,
AVG(CAST(kills AS REAL) / NULLIF(deaths, 0)) as avg_kd,
AVG(adr) as avg_adr,
AVG(kast) as avg_kast,
AVG(rws) as avg_rws,
AVG(headshot_count) as avg_hs_kills,
SUM(kills) as total_kills,
SUM(deaths) as total_deaths,
SUM(headshot_count) as total_hs,
SUM(assists) as total_assists,
AVG(assists) as avg_assists,
SUM(round_total) as total_rounds
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
if not row:
return {}
total_kills = row[7] if row[7] else 0
total_deaths = row[8] if row[8] else 1
total_hs = row[9] if row[9] else 0
total_rounds = row[12] if row[12] else 1
return {
'core_avg_rating': round(row[0], 3) if row[0] else 0.0,
'core_avg_rating2': round(row[1], 3) if row[1] else 0.0,
'core_avg_kd': round(row[2], 3) if row[2] else 0.0,
'core_avg_adr': round(row[3], 2) if row[3] else 0.0,
'core_avg_kast': round(row[4], 3) if row[4] else 0.0,
'core_avg_rws': round(row[5], 2) if row[5] else 0.0,
'core_avg_hs_kills': round(row[6], 2) if row[6] else 0.0,
'core_hs_rate': round(total_hs / total_kills, 3) if total_kills > 0 else 0.0,
'core_total_kills': total_kills,
'core_total_deaths': total_deaths,
'core_total_assists': row[10] if row[10] else 0,
'core_avg_assists': round(row[11], 2) if row[11] else 0.0,
'core_kpr': round(total_kills / total_rounds, 3) if total_rounds > 0 else 0.0,
'core_dpr': round(total_deaths / total_rounds, 3) if total_rounds > 0 else 0.0,
'core_survival_rate': round((total_rounds - total_deaths) / total_rounds, 3) if total_rounds > 0 else 0.0,
}
@staticmethod
def _calculate_flash_assists(steam_id: str, conn_l2: sqlite3.Connection) -> int:
"""
Calculate flash assists from fact_match_players (Total - Damage Assists)
Returns total flash assist count (Estimated)
"""
cursor = conn_l2.cursor()
# NOTE: Flash Assist Logic
# Source 'flash_assists' is often 0.
# User Logic: Flash Assists = Total Assists - Damage Assists (assisted_kill)
# We take MAX(0, diff) to avoid negative numbers if assisted_kill definition varies.
cursor.execute("""
SELECT SUM(MAX(0, assists - assisted_kill))
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
res = cursor.fetchone()
if res and res[0] is not None:
return res[0]
return 0
@staticmethod
def _calculate_match_stats(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Match Stats (8 columns)
Columns:
- core_win_rate, core_wins, core_losses
- core_avg_match_duration
- core_avg_mvps, core_mvp_rate
- core_avg_elo_change, core_total_elo_gained
"""
cursor = conn_l2.cursor()
# Win/loss stats
cursor.execute("""
SELECT
COUNT(*) as total_matches,
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as wins,
SUM(CASE WHEN is_win = 0 THEN 1 ELSE 0 END) as losses,
AVG(mvp_count) as avg_mvps,
SUM(mvp_count) as total_mvps
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
total_matches = row[0] if row[0] else 0
wins = row[1] if row[1] else 0
losses = row[2] if row[2] else 0
avg_mvps = row[3] if row[3] else 0.0
total_mvps = row[4] if row[4] else 0
# Match duration (from fact_matches)
cursor.execute("""
SELECT AVG(m.duration) as avg_duration
FROM fact_matches m
JOIN fact_match_players p ON m.match_id = p.match_id
WHERE p.steam_id_64 = ?
""", (steam_id,))
duration_row = cursor.fetchone()
avg_duration = duration_row[0] if duration_row and duration_row[0] else 0
# ELO stats (from elo_change column)
cursor.execute("""
SELECT
AVG(elo_change) as avg_elo_change,
SUM(elo_change) as total_elo_gained
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
elo_row = cursor.fetchone()
avg_elo_change = elo_row[0] if elo_row and elo_row[0] else 0.0
total_elo_gained = elo_row[1] if elo_row and elo_row[1] else 0.0
return {
'core_win_rate': round(wins / total_matches, 3) if total_matches > 0 else 0.0,
'core_wins': wins,
'core_losses': losses,
'core_avg_match_duration': int(avg_duration),
'core_avg_mvps': round(avg_mvps, 2),
'core_mvp_rate': round(total_mvps / total_matches, 2) if total_matches > 0 else 0.0,
'core_avg_elo_change': round(avg_elo_change, 2),
'core_total_elo_gained': round(total_elo_gained, 2),
}
@staticmethod
def _calculate_weapon_stats(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Weapon Stats (12 columns)
Columns:
- core_avg_awp_kills, core_awp_usage_rate
- core_avg_knife_kills, core_avg_zeus_kills, core_zeus_buy_rate
- core_top_weapon, core_top_weapon_kills, core_top_weapon_hs_rate
- core_weapon_diversity
- core_rifle_hs_rate, core_pistol_hs_rate
- core_smg_kills_total
"""
cursor = conn_l2.cursor()
# AWP/Knife/Zeus stats from fact_round_events
cursor.execute("""
SELECT
weapon,
COUNT(*) as kill_count
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IN ('AWP', 'Knife', 'Zeus', 'knife', 'awp', 'zeus')
GROUP BY weapon
""", (steam_id,))
awp_kills = 0
knife_kills = 0
zeus_kills = 0
for weapon, kills in cursor.fetchall():
weapon_lower = weapon.lower() if weapon else ''
if weapon_lower == 'awp':
awp_kills += kills
elif weapon_lower == 'knife':
knife_kills += kills
elif weapon_lower == 'zeus':
zeus_kills += kills
# Get total matches count for rates
cursor.execute("""
SELECT COUNT(DISTINCT match_id)
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
total_matches = cursor.fetchone()[0] or 1
avg_awp = awp_kills / total_matches
avg_knife = knife_kills / total_matches
avg_zeus = zeus_kills / total_matches
# Flash assists from fact_round_events
flash_assists = BasicProcessor._calculate_flash_assists(steam_id, conn_l2)
avg_flash_assists = flash_assists / total_matches
# Top weapon from fact_round_events
cursor.execute("""
SELECT
weapon,
COUNT(*) as kill_count,
SUM(CASE WHEN is_headshot = 1 THEN 1 ELSE 0 END) as hs_count
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IS NOT NULL
AND weapon != 'unknown'
GROUP BY weapon
ORDER BY kill_count DESC
LIMIT 1
""", (steam_id,))
weapon_row = cursor.fetchone()
top_weapon = weapon_row[0] if weapon_row else "unknown"
top_weapon_kills = weapon_row[1] if weapon_row else 0
top_weapon_hs = weapon_row[2] if weapon_row else 0
top_weapon_hs_rate = top_weapon_hs / top_weapon_kills if top_weapon_kills > 0 else 0.0
# Weapon diversity (number of distinct weapons with 10+ kills)
cursor.execute("""
SELECT COUNT(DISTINCT weapon) as weapon_count
FROM (
SELECT weapon, COUNT(*) as kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IS NOT NULL
GROUP BY weapon
HAVING kills >= 10
)
""", (steam_id,))
diversity_row = cursor.fetchone()
weapon_diversity = diversity_row[0] if diversity_row else 0
# Rifle/Pistol/SMG stats
cursor.execute("""
SELECT
weapon,
COUNT(*) as kills,
SUM(CASE WHEN is_headshot = 1 THEN 1 ELSE 0 END) as headshot_kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IS NOT NULL
GROUP BY weapon
""", (steam_id,))
rifle_kills = 0
rifle_hs = 0
pistol_kills = 0
pistol_hs = 0
smg_kills = 0
awp_usage_count = 0
for weapon, kills, hs in cursor.fetchall():
category = WeaponCategories.get_category(weapon)
if category == 'rifle':
rifle_kills += kills
rifle_hs += hs
elif category == 'pistol':
pistol_kills += kills
pistol_hs += hs
elif category == 'smg':
smg_kills += kills
elif weapon.lower() == 'awp':
awp_usage_count += kills
total_rounds = BaseFeatureProcessor.get_player_round_count(steam_id, conn_l2)
return {
'core_avg_awp_kills': round(avg_awp, 2),
'core_awp_usage_rate': round(awp_usage_count / total_rounds, 3) if total_rounds > 0 else 0.0,
'core_avg_knife_kills': round(avg_knife, 3),
'core_avg_zeus_kills': round(avg_zeus, 3),
'core_zeus_buy_rate': round(avg_zeus / total_matches, 3) if total_matches > 0 else 0.0,
'core_avg_flash_assists': round(avg_flash_assists, 2),
'core_top_weapon': top_weapon,
'core_top_weapon_kills': top_weapon_kills,
'core_top_weapon_hs_rate': round(top_weapon_hs_rate, 3),
'core_weapon_diversity': weapon_diversity,
'core_rifle_hs_rate': round(rifle_hs / rifle_kills, 3) if rifle_kills > 0 else 0.0,
'core_pistol_hs_rate': round(pistol_hs / pistol_kills, 3) if pistol_kills > 0 else 0.0,
'core_smg_kills_total': smg_kills,
}
@staticmethod
def _calculate_objective_stats(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Objective Stats (6 columns)
Columns:
- core_avg_plants, core_avg_defuses, core_avg_flash_assists
- core_plant_success_rate, core_defuse_success_rate
- core_objective_impact
"""
cursor = conn_l2.cursor()
# Get data from main table
# Updated to use calculated flash assists formula
# Calculate flash assists manually first (since column is 0)
flash_assists_total = BasicProcessor._calculate_flash_assists(steam_id, conn_l2)
match_count = BaseFeatureProcessor.get_player_match_count(steam_id, conn_l2)
avg_flash_assists = flash_assists_total / match_count if match_count > 0 else 0.0
cursor.execute("""
SELECT
AVG(planted_bomb) as avg_plants,
AVG(defused_bomb) as avg_defuses,
SUM(planted_bomb) as total_plants,
SUM(defused_bomb) as total_defuses
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
if not row:
return {}
avg_plants = row[0] if row[0] else 0.0
avg_defuses = row[1] if row[1] else 0.0
# avg_flash_assists computed above
total_plants = row[2] if row[2] else 0
total_defuses = row[3] if row[3] else 0
# Get T side rounds
cursor.execute("""
SELECT COALESCE(SUM(round_total), 0)
FROM fact_match_players_t
WHERE steam_id_64 = ?
""", (steam_id,))
t_rounds = cursor.fetchone()[0] or 1
# Get CT side rounds
cursor.execute("""
SELECT COALESCE(SUM(round_total), 0)
FROM fact_match_players_ct
WHERE steam_id_64 = ?
""", (steam_id,))
ct_rounds = cursor.fetchone()[0] or 1
# Plant success rate: plants per T round
plant_rate = total_plants / t_rounds if t_rounds > 0 else 0.0
# Defuse success rate: approximate as defuses per CT round (simplified)
defuse_rate = total_defuses / ct_rounds if ct_rounds > 0 else 0.0
# Objective impact score: weighted combination
objective_impact = (total_plants * 2.0 + total_defuses * 3.0 + avg_flash_assists * 0.5)
return {
'core_avg_plants': round(avg_plants, 2),
'core_avg_defuses': round(avg_defuses, 2),
'core_avg_flash_assists': round(avg_flash_assists, 2),
'core_plant_success_rate': round(plant_rate, 3),
'core_defuse_success_rate': round(defuse_rate, 3),
'core_objective_impact': round(objective_impact, 2),
}
def _get_default_features() -> Dict[str, Any]:
"""Return default zero values for all 41 CORE features"""
return {
# Basic Performance (15)
'core_avg_rating': 0.0,
'core_avg_rating2': 0.0,
'core_avg_kd': 0.0,
'core_avg_adr': 0.0,
'core_avg_kast': 0.0,
'core_avg_rws': 0.0,
'core_avg_hs_kills': 0.0,
'core_hs_rate': 0.0,
'core_total_kills': 0,
'core_total_deaths': 0,
'core_total_assists': 0,
'core_avg_assists': 0.0,
'core_kpr': 0.0,
'core_dpr': 0.0,
'core_survival_rate': 0.0,
# Match Stats (8)
'core_win_rate': 0.0,
'core_wins': 0,
'core_losses': 0,
'core_avg_match_duration': 0,
'core_avg_mvps': 0.0,
'core_mvp_rate': 0.0,
'core_avg_elo_change': 0.0,
'core_total_elo_gained': 0.0,
# Weapon Stats (12)
'core_avg_awp_kills': 0.0,
'core_awp_usage_rate': 0.0,
'core_avg_knife_kills': 0.0,
'core_avg_zeus_kills': 0.0,
'core_zeus_buy_rate': 0.0,
'core_top_weapon': 'unknown',
'core_top_weapon_kills': 0,
'core_top_weapon_hs_rate': 0.0,
'core_weapon_diversity': 0,
'core_rifle_hs_rate': 0.0,
'core_pistol_hs_rate': 0.0,
'core_smg_kills_total': 0,
# Objective Stats (6)
'core_avg_plants': 0.0,
'core_avg_defuses': 0.0,
'core_avg_flash_assists': 0.0,
'core_plant_success_rate': 0.0,
'core_defuse_success_rate': 0.0,
'core_objective_impact': 0.0,
}
@@ -0,0 +1,420 @@
"""
CompositeProcessor - Tier 5: COMPOSITE Features (11 columns)
Weighted composite scores based on Tier 1-4 features:
- 8 Radar Scores (0-100): AIM, CLUTCH, PISTOL, DEFENSE, UTILITY, STABILITY, ECONOMY, PACE
- Overall Score (0-100): Weighted sum of 8 dimensions
- Tier Classification: Elite/Advanced/Intermediate/Beginner
- Tier Percentile: Ranking among all players
"""
import sqlite3
from typing import Dict, Any
from .base_processor import BaseFeatureProcessor, NormalizationUtils, SafeAggregator
class CompositeProcessor(BaseFeatureProcessor):
"""Tier 5 COMPOSITE processor - Weighted scores from all previous tiers"""
MIN_MATCHES_REQUIRED = 20 # Need substantial data for reliable composite scores
@staticmethod
def calculate(steam_id: str, conn_l2: sqlite3.Connection,
pre_features: Dict[str, Any]) -> Dict[str, Any]:
"""
Calculate all Tier 5 COMPOSITE features (11 columns)
Args:
steam_id: Player's Steam ID
conn_l2: L2 database connection
pre_features: Dictionary containing all Tier 1-4 features
Returns dict with keys starting with 'score_' and 'tier_'
"""
features = {}
# Check minimum matches
if not BaseFeatureProcessor.check_min_matches(steam_id, conn_l2,
CompositeProcessor.MIN_MATCHES_REQUIRED):
return _get_default_composite_features()
# Calculate 8 radar dimension scores
features['score_aim'] = CompositeProcessor._calculate_aim_score(pre_features)
features['score_clutch'] = CompositeProcessor._calculate_clutch_score(pre_features)
features['score_pistol'] = CompositeProcessor._calculate_pistol_score(pre_features)
features['score_defense'] = CompositeProcessor._calculate_defense_score(pre_features)
features['score_utility'] = CompositeProcessor._calculate_utility_score(pre_features)
features['score_stability'] = CompositeProcessor._calculate_stability_score(pre_features)
features['score_economy'] = CompositeProcessor._calculate_economy_score(pre_features)
features['score_pace'] = CompositeProcessor._calculate_pace_score(pre_features)
# Calculate overall score (Weighted sum of 8 dimensions)
# Weights: AIM 20%, CLUTCH 12%, PISTOL 10%, DEFENSE 13%, UTILITY 20%, STABILITY 8%, ECONOMY 12%, PACE 5%
features['score_overall'] = (
features['score_aim'] * 0.12 +
features['score_clutch'] * 0.18 +
features['score_pistol'] * 0.18 +
features['score_defense'] * 0.20 +
features['score_utility'] * 0.10 +
features['score_stability'] * 0.07 +
features['score_economy'] * 0.08 +
features['score_pace'] * 0.07
)
features['score_overall'] = round(features['score_overall'], 2)
# Classify tier based on overall score
features['tier_classification'] = CompositeProcessor._classify_tier(features['score_overall'])
# Percentile rank (placeholder - requires all players)
features['tier_percentile'] = min(features['score_overall'], 100.0)
return features
@staticmethod
def _calculate_aim_score(features: Dict[str, Any]) -> float:
"""
AIM Score (0-100) | 20%
"""
# Extract features
rating = features.get('core_avg_rating', 0.0)
kd = features.get('core_avg_kd', 0.0)
adr = features.get('core_avg_adr', 0.0)
hs_rate = features.get('core_hs_rate', 0.0)
multikill_rate = features.get('tac_multikill_rate', 0.0)
avg_hs = features.get('core_avg_hs_kills', 0.0)
weapon_div = features.get('core_weapon_diversity', 0.0)
rifle_hs_rate = features.get('core_rifle_hs_rate', 0.0)
# Normalize (Variable / Baseline * 100)
rating_score = min((rating / 1.15) * 100, 100)
kd_score = min((kd / 1.30) * 100, 100)
adr_score = min((adr / 90) * 100, 100)
hs_score = min((hs_rate / 0.55) * 100, 100)
mk_score = min((multikill_rate / 0.22) * 100, 100)
avg_hs_score = min((avg_hs / 8.5) * 100, 100)
weapon_div_score = min((weapon_div / 20) * 100, 100)
rifle_hs_score = min((rifle_hs_rate / 0.50) * 100, 100)
# Weighted Sum
aim_score = (
rating_score * 0.15 +
kd_score * 0.15 +
adr_score * 0.10 +
hs_score * 0.15 +
mk_score * 0.10 +
avg_hs_score * 0.15 +
weapon_div_score * 0.10 +
rifle_hs_score * 0.10
)
return round(min(max(aim_score, 0), 100), 2)
@staticmethod
def _calculate_clutch_score(features: Dict[str, Any]) -> float:
"""
CLUTCH Score (0-100) | 12%
"""
# Extract features
# Clutch Score Calculation: (1v1*100 + 1v2*200 + 1v3+*500) / 8
c1v1 = features.get('tac_clutch_1v1_wins', 0)
c1v2 = features.get('tac_clutch_1v2_wins', 0)
c1v3p = features.get('tac_clutch_1v3_plus_wins', 0)
# Note: tac_clutch_1v3_plus_wins includes 1v3, 1v4, 1v5
raw_clutch_score = (c1v1 * 100 + c1v2 * 200 + c1v3p * 500) / 8.0
comeback_kd = features.get('int_pressure_comeback_kd', 0.0)
matchpoint_kpr = features.get('int_pressure_matchpoint_kpr', 0.0)
rating = features.get('core_avg_rating', 0.0)
# 1v3+ Win Rate
attempts_1v3p = features.get('tac_clutch_1v3_plus_attempts', 0)
win_1v3p = features.get('tac_clutch_1v3_plus_wins', 0)
win_rate_1v3p = win_1v3p / attempts_1v3p if attempts_1v3p > 0 else 0.0
clutch_impact = features.get('tac_clutch_impact_score', 0.0)
# Normalize
clutch_score_val = min((raw_clutch_score / 200) * 100, 100)
comeback_score = min((comeback_kd / 1.55) * 100, 100)
matchpoint_score = min((matchpoint_kpr / 0.85) * 100, 100)
rating_score = min((rating / 1.15) * 100, 100)
win_rate_1v3p_score = min((win_rate_1v3p / 0.10) * 100, 100)
clutch_impact_score = min((clutch_impact / 200) * 100, 100)
# Weighted Sum
final_clutch_score = (
clutch_score_val * 0.20 +
comeback_score * 0.25 +
matchpoint_score * 0.15 +
rating_score * 0.10 +
win_rate_1v3p_score * 0.15 +
clutch_impact_score * 0.15
)
return round(min(max(final_clutch_score, 0), 100), 2)
@staticmethod
def _calculate_pistol_score(features: Dict[str, Any]) -> float:
"""
PISTOL Score (0-100) | 10%
"""
# Extract features
fk_rate = features.get('tac_fk_rate', 0.0) # Using general FK rate as per original logic, though user said "手枪局首杀率".
# If "手枪局首杀率" means FK rate in pistol rounds specifically, we don't have that in pre-calculated features.
# Assuming general FK rate or tac_fk_rate is acceptable proxy or that user meant tac_fk_rate.
# Given "tac_fk_rate" was used in previous Pistol score, I'll stick with it.
pistol_hs_rate = features.get('core_pistol_hs_rate', 0.0)
entry_win_rate = features.get('tac_opening_duel_winrate', 0.0)
rating = features.get('core_avg_rating', 0.0)
smg_kills = features.get('core_smg_kills_total', 0)
avg_fk = features.get('tac_avg_fk', 0.0)
# Normalize
fk_score = min((fk_rate / 0.58) * 100, 100) # 58%
pistol_hs_score = min((pistol_hs_rate / 0.75) * 100, 100) # 75%
entry_win_score = min((entry_win_rate / 0.47) * 100, 100) # 47%
rating_score = min((rating / 1.15) * 100, 100)
smg_score = min((smg_kills / 270) * 100, 100)
avg_fk_score = min((avg_fk / 3.0) * 100, 100)
# Weighted Sum
pistol_score = (
fk_score * 0.20 +
pistol_hs_score * 0.25 +
entry_win_score * 0.15 +
rating_score * 0.10 +
smg_score * 0.15 +
avg_fk_score * 0.15
)
return round(min(max(pistol_score, 0), 100), 2)
@staticmethod
def _calculate_defense_score(features: Dict[str, Any]) -> float:
"""
DEFENSE Score (0-100) | 13%
"""
# Extract features
ct_rating = features.get('meta_side_ct_rating', 0.0)
t_rating = features.get('meta_side_t_rating', 0.0)
ct_kd = features.get('meta_side_ct_kd', 0.0)
t_kd = features.get('meta_side_t_kd', 0.0)
ct_kast = features.get('meta_side_ct_kast', 0.0)
t_kast = features.get('meta_side_t_kast', 0.0)
# Normalize
ct_rating_score = min((ct_rating / 1.15) * 100, 100)
t_rating_score = min((t_rating / 1.20) * 100, 100)
ct_kd_score = min((ct_kd / 1.40) * 100, 100)
t_kd_score = min((t_kd / 1.45) * 100, 100)
ct_kast_score = min((ct_kast / 0.70) * 100, 100)
t_kast_score = min((t_kast / 0.72) * 100, 100)
# Weighted Sum
defense_score = (
ct_rating_score * 0.20 +
t_rating_score * 0.20 +
ct_kd_score * 0.15 +
t_kd_score * 0.15 +
ct_kast_score * 0.15 +
t_kast_score * 0.15
)
return round(min(max(defense_score, 0), 100), 2)
@staticmethod
def _calculate_utility_score(features: Dict[str, Any]) -> float:
"""
UTILITY Score (0-100) | 20%
"""
# Extract features
util_usage = features.get('tac_util_usage_rate', 0.0)
util_dmg = features.get('tac_util_nade_dmg_per_round', 0.0)
flash_eff = features.get('tac_util_flash_efficiency', 0.0)
util_impact = features.get('tac_util_impact_score', 0.0)
blind = features.get('tac_util_flash_enemies_per_round', 0.0) # 致盲数 (Enemies Blinded per Round)
flash_rnd = features.get('tac_util_flash_per_round', 0.0)
flash_ast = features.get('core_avg_flash_assists', 0.0)
# Normalize
usage_score = min((util_usage / 2.0) * 100, 100)
dmg_score = min((util_dmg / 4.0) * 100, 100)
flash_eff_score = min((flash_eff / 1.35) * 100, 100) # 135%
impact_score = min((util_impact / 22) * 100, 100)
blind_score = min((blind / 1.0) * 100, 100)
flash_rnd_score = min((flash_rnd / 0.85) * 100, 100)
flash_ast_score = min((flash_ast / 2.15) * 100, 100)
# Weighted Sum
utility_score = (
usage_score * 0.15 +
dmg_score * 0.05 +
flash_eff_score * 0.20 +
impact_score * 0.20 +
blind_score * 0.15 +
flash_rnd_score * 0.15 +
flash_ast_score * 0.10
)
return round(min(max(utility_score, 0), 100), 2)
@staticmethod
def _calculate_stability_score(features: Dict[str, Any]) -> float:
"""
STABILITY Score (0-100) | 8%
"""
# Extract features
volatility = features.get('meta_rating_volatility', 0.0)
loss_rating = features.get('meta_loss_rating', 0.0)
consistency = features.get('meta_rating_consistency', 0.0)
tilt_resilience = features.get('int_pressure_tilt_resistance', 0.0)
map_stable = features.get('meta_map_stability', 0.0)
elo_stable = features.get('meta_elo_tier_stability', 0.0)
recent_form = features.get('meta_recent_form_rating', 0.0)
# Normalize
# Volatility: Reverse score. 100 - (Vol * 220)
vol_score = max(0, 100 - (volatility * 220))
loss_score = min((loss_rating / 1.00) * 100, 100)
cons_score = min((consistency / 70) * 100, 100)
tilt_score = min((tilt_resilience / 0.80) * 100, 100)
map_score = min((map_stable / 0.25) * 100, 100)
elo_score = min((elo_stable / 0.48) * 100, 100)
recent_score = min((recent_form / 1.15) * 100, 100)
# Weighted Sum
stability_score = (
vol_score * 0.20 +
loss_score * 0.20 +
cons_score * 0.15 +
tilt_score * 0.15 +
map_score * 0.10 +
elo_score * 0.10 +
recent_score * 0.10
)
return round(min(max(stability_score, 0), 100), 2)
@staticmethod
def _calculate_economy_score(features: Dict[str, Any]) -> float:
"""
ECONOMY Score (0-100) | 12%
"""
# Extract features
dmg_1k = features.get('tac_eco_dmg_per_1k', 0.0)
eco_kpr = features.get('tac_eco_kpr_eco_rounds', 0.0)
eco_kd = features.get('tac_eco_kd_eco_rounds', 0.0)
eco_score = features.get('tac_eco_efficiency_score', 0.0)
full_kpr = features.get('tac_eco_kpr_full_rounds', 0.0)
force_win = features.get('tac_eco_force_success_rate', 0.0)
# Normalize
dmg_score = min((dmg_1k / 19) * 100, 100)
eco_kpr_score = min((eco_kpr / 0.85) * 100, 100)
eco_kd_score = min((eco_kd / 1.30) * 100, 100)
eco_eff_score = min((eco_score / 0.80) * 100, 100)
full_kpr_score = min((full_kpr / 0.90) * 100, 100)
force_win_score = min((force_win / 0.50) * 100, 100)
# Weighted Sum
economy_score = (
dmg_score * 0.25 +
eco_kpr_score * 0.20 +
eco_kd_score * 0.15 +
eco_eff_score * 0.15 +
full_kpr_score * 0.15 +
force_win_score * 0.10
)
return round(min(max(economy_score, 0), 100), 2)
@staticmethod
def _calculate_pace_score(features: Dict[str, Any]) -> float:
"""
PACE Score (0-100) | 5%
"""
# Extract features
early_kill_pct = features.get('int_timing_early_kill_share', 0.0)
aggression = features.get('int_timing_aggression_index', 0.0)
trade_speed = features.get('int_trade_response_time', 0.0)
trade_kill = features.get('int_trade_kill_count', 0)
teamwork = features.get('int_teamwork_score', 0.0)
first_contact = features.get('int_timing_first_contact_time', 0.0)
# Normalize
early_score = min((early_kill_pct / 0.44) * 100, 100)
aggression_score = min((aggression / 1.20) * 100, 100)
# Trade Speed: Reverse score. (2.0 / Trade Speed) * 100
# Avoid division by zero
if trade_speed > 0.01:
trade_speed_score = min((2.0 / trade_speed) * 100, 100)
else:
trade_speed_score = 100 # Instant trade
trade_kill_score = min((trade_kill / 650) * 100, 100)
teamwork_score = min((teamwork / 29) * 100, 100)
# First Contact: Reverse score. (30 / 1st Contact) * 100
if first_contact > 0.01:
first_contact_score = min((30 / first_contact) * 100, 100)
else:
first_contact_score = 0 # If 0, probably no data, safe to say 0? Or 100?
# 0 first contact time means instant damage.
# But "30 / Contact" means smaller contact time gives higher score.
# If contact time is 0, score explodes.
# Realistically first contact time is > 0.
# I will clamp it.
first_contact_score = 100 # Assume very fast
# Weighted Sum
pace_score = (
early_score * 0.25 +
aggression_score * 0.20 +
trade_speed_score * 0.20 +
trade_kill_score * 0.15 +
teamwork_score * 0.10 +
first_contact_score * 0.10
)
return round(min(max(pace_score, 0), 100), 2)
@staticmethod
def _classify_tier(overall_score: float) -> str:
"""
Classify player tier based on overall score
Tiers:
- Elite: 75+
- Advanced: 60-75
- Intermediate: 40-60
- Beginner: <40
"""
if overall_score >= 75:
return 'Elite'
elif overall_score >= 60:
return 'Advanced'
elif overall_score >= 40:
return 'Intermediate'
else:
return 'Beginner'
def _get_default_composite_features() -> Dict[str, Any]:
"""Return default zero values for all 11 COMPOSITE features"""
return {
'score_aim': 0.0,
'score_clutch': 0.0,
'score_pistol': 0.0,
'score_defense': 0.0,
'score_utility': 0.0,
'score_stability': 0.0,
'score_economy': 0.0,
'score_pace': 0.0,
'score_overall': 0.0,
'tier_classification': 'Beginner',
'tier_percentile': 0.0,
}
@@ -0,0 +1,732 @@
"""
IntelligenceProcessor - Tier 3: INTELLIGENCE Features (53 columns)
Advanced analytics on fact_round_events with complex calculations:
- High IQ Kills (9 columns): wallbang, smoke, blind, noscope + IQ score
- Timing Analysis (12 columns): early/mid/late kill distribution, aggression
- Pressure Performance (10 columns): comeback, losing streak, matchpoint
- Position Mastery (14 columns): site control, lurk tendency, spatial IQ
- Trade Network (8 columns): trade kills/response time, teamwork
"""
import sqlite3
from typing import Dict, Any, List, Tuple
from .base_processor import BaseFeatureProcessor, SafeAggregator
class IntelligenceProcessor(BaseFeatureProcessor):
"""Tier 3 INTELLIGENCE processor - Complex event-level analytics"""
MIN_MATCHES_REQUIRED = 10 # Need substantial data for reliable patterns
@staticmethod
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate all Tier 3 INTELLIGENCE features (53 columns)
Returns dict with keys starting with 'int_'
"""
features = {}
# Check minimum matches
if not BaseFeatureProcessor.check_min_matches(steam_id, conn_l2,
IntelligenceProcessor.MIN_MATCHES_REQUIRED):
return _get_default_intelligence_features()
# Calculate each intelligence dimension
features.update(IntelligenceProcessor._calculate_high_iq_kills(steam_id, conn_l2))
features.update(IntelligenceProcessor._calculate_timing_analysis(steam_id, conn_l2))
features.update(IntelligenceProcessor._calculate_pressure_performance(steam_id, conn_l2))
features.update(IntelligenceProcessor._calculate_position_mastery(steam_id, conn_l2))
features.update(IntelligenceProcessor._calculate_trade_network(steam_id, conn_l2))
return features
@staticmethod
def _calculate_high_iq_kills(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate High IQ Kills (9 columns)
Columns:
- int_wallbang_kills, int_wallbang_rate
- int_smoke_kills, int_smoke_kill_rate
- int_blind_kills, int_blind_kill_rate
- int_noscope_kills, int_noscope_rate
- int_high_iq_score
"""
cursor = conn_l2.cursor()
# Get total kills for rate calculations
cursor.execute("""
SELECT COUNT(*) as total_kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND event_type = 'kill'
""", (steam_id,))
total_kills = cursor.fetchone()[0]
total_kills = total_kills if total_kills else 1
# Wallbang kills
cursor.execute("""
SELECT COUNT(*) as wallbang_kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND is_wallbang = 1
""", (steam_id,))
wallbang_kills = cursor.fetchone()[0]
wallbang_kills = wallbang_kills if wallbang_kills else 0
# Smoke kills
cursor.execute("""
SELECT COUNT(*) as smoke_kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND is_through_smoke = 1
""", (steam_id,))
smoke_kills = cursor.fetchone()[0]
smoke_kills = smoke_kills if smoke_kills else 0
# Blind kills
cursor.execute("""
SELECT COUNT(*) as blind_kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND is_blind = 1
""", (steam_id,))
blind_kills = cursor.fetchone()[0]
blind_kills = blind_kills if blind_kills else 0
# Noscope kills (AWP only)
cursor.execute("""
SELECT COUNT(*) as noscope_kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND is_noscope = 1
""", (steam_id,))
noscope_kills = cursor.fetchone()[0]
noscope_kills = noscope_kills if noscope_kills else 0
# Calculate rates
wallbang_rate = SafeAggregator.safe_divide(wallbang_kills, total_kills)
smoke_rate = SafeAggregator.safe_divide(smoke_kills, total_kills)
blind_rate = SafeAggregator.safe_divide(blind_kills, total_kills)
noscope_rate = SafeAggregator.safe_divide(noscope_kills, total_kills)
# High IQ score: weighted combination
iq_score = (
wallbang_kills * 3.0 +
smoke_kills * 2.0 +
blind_kills * 1.5 +
noscope_kills * 2.0
)
return {
'int_wallbang_kills': wallbang_kills,
'int_wallbang_rate': round(wallbang_rate, 4),
'int_smoke_kills': smoke_kills,
'int_smoke_kill_rate': round(smoke_rate, 4),
'int_blind_kills': blind_kills,
'int_blind_kill_rate': round(blind_rate, 4),
'int_noscope_kills': noscope_kills,
'int_noscope_rate': round(noscope_rate, 4),
'int_high_iq_score': round(iq_score, 2),
}
@staticmethod
def _calculate_timing_analysis(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Timing Analysis (12 columns)
Time bins: Early (0-30s), Mid (30-60s), Late (60s+)
Columns:
- int_timing_early_kills, int_timing_mid_kills, int_timing_late_kills
- int_timing_early_kill_share, int_timing_mid_kill_share, int_timing_late_kill_share
- int_timing_avg_kill_time
- int_timing_early_deaths, int_timing_early_death_rate
- int_timing_aggression_index
- int_timing_patience_score
- int_timing_first_contact_time
"""
cursor = conn_l2.cursor()
# Kill distribution by time bins
cursor.execute("""
SELECT
COUNT(CASE WHEN event_time <= 30 THEN 1 END) as early_kills,
COUNT(CASE WHEN event_time > 30 AND event_time <= 60 THEN 1 END) as mid_kills,
COUNT(CASE WHEN event_time > 60 THEN 1 END) as late_kills,
COUNT(*) as total_kills,
AVG(event_time) as avg_kill_time
FROM fact_round_events
WHERE attacker_steam_id = ?
AND event_type = 'kill'
""", (steam_id,))
row = cursor.fetchone()
early_kills = row[0] if row[0] else 0
mid_kills = row[1] if row[1] else 0
late_kills = row[2] if row[2] else 0
total_kills = row[3] if row[3] else 1
avg_kill_time = row[4] if row[4] else 0.0
# Calculate shares
early_share = SafeAggregator.safe_divide(early_kills, total_kills)
mid_share = SafeAggregator.safe_divide(mid_kills, total_kills)
late_share = SafeAggregator.safe_divide(late_kills, total_kills)
# Death distribution (for aggression index)
cursor.execute("""
SELECT
COUNT(CASE WHEN event_time <= 30 THEN 1 END) as early_deaths,
COUNT(*) as total_deaths
FROM fact_round_events
WHERE victim_steam_id = ?
AND event_type = 'kill'
""", (steam_id,))
death_row = cursor.fetchone()
early_deaths = death_row[0] if death_row[0] else 0
total_deaths = death_row[1] if death_row[1] else 1
early_death_rate = SafeAggregator.safe_divide(early_deaths, total_deaths)
# Aggression index: early kills / early deaths
aggression_index = SafeAggregator.safe_divide(early_kills, max(early_deaths, 1))
# Patience score: late kill share
patience_score = late_share
# First contact time: average time of first event per round
cursor.execute("""
SELECT AVG(min_time) as avg_first_contact
FROM (
SELECT match_id, round_num, MIN(event_time) as min_time
FROM fact_round_events
WHERE attacker_steam_id = ? OR victim_steam_id = ?
GROUP BY match_id, round_num
)
""", (steam_id, steam_id))
first_contact = cursor.fetchone()[0]
first_contact_time = first_contact if first_contact else 0.0
return {
'int_timing_early_kills': early_kills,
'int_timing_mid_kills': mid_kills,
'int_timing_late_kills': late_kills,
'int_timing_early_kill_share': round(early_share, 3),
'int_timing_mid_kill_share': round(mid_share, 3),
'int_timing_late_kill_share': round(late_share, 3),
'int_timing_avg_kill_time': round(avg_kill_time, 2),
'int_timing_early_deaths': early_deaths,
'int_timing_early_death_rate': round(early_death_rate, 3),
'int_timing_aggression_index': round(aggression_index, 3),
'int_timing_patience_score': round(patience_score, 3),
'int_timing_first_contact_time': round(first_contact_time, 2),
}
@staticmethod
def _calculate_pressure_performance(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Pressure Performance (10 columns)
"""
cursor = conn_l2.cursor()
# 1. Comeback Performance (Whole Match Stats for Comeback Games)
# Definition: Won match where team faced >= 5 round deficit
# Get all winning matches
cursor.execute("""
SELECT match_id, rating, kills, deaths
FROM fact_match_players
WHERE steam_id_64 = ? AND is_win = 1
""", (steam_id,))
win_matches = cursor.fetchall()
comeback_ratings = []
comeback_kds = []
for match_id, rating, kills, deaths in win_matches:
# Check for deficit
# Need round scores
cursor.execute("""
SELECT round_num, ct_score, t_score, winner_side
FROM fact_rounds
WHERE match_id = ?
ORDER BY round_num
""", (match_id,))
rounds = cursor.fetchall()
if not rounds: continue
# Determine starting side or side per round?
# We need player's side per round to know if they are trailing.
# Simplified: Use fact_round_player_economy to get side per round
cursor.execute("""
SELECT round_num, side
FROM fact_round_player_economy
WHERE match_id = ? AND steam_id_64 = ?
""", (match_id, steam_id))
side_map = {r[0]: r[1] for r in cursor.fetchall()}
max_deficit = 0
for r_num, ct_s, t_s, win_side in rounds:
side = side_map.get(r_num)
if not side: continue
my_score = ct_s if side == 'CT' else t_s
opp_score = t_s if side == 'CT' else ct_s
diff = opp_score - my_score
if diff > max_deficit:
max_deficit = diff
if max_deficit >= 5:
# This is a comeback match
if rating: comeback_ratings.append(rating)
kd = kills / max(deaths, 1)
comeback_kds.append(kd)
avg_comeback_rating = SafeAggregator.safe_avg(comeback_ratings)
avg_comeback_kd = SafeAggregator.safe_avg(comeback_kds)
# 2. Matchpoint Performance (KPR only)
# Definition: Rounds where ANY team is at match point (12 or 15)
cursor.execute("""
SELECT DISTINCT match_id FROM fact_match_players WHERE steam_id_64 = ?
""", (steam_id,))
all_match_ids = [r[0] for r in cursor.fetchall()]
mp_kills = 0
mp_rounds = 0
for match_id in all_match_ids:
# Get rounds and sides
cursor.execute("""
SELECT round_num, ct_score, t_score
FROM fact_rounds
WHERE match_id = ?
""", (match_id,))
rounds = cursor.fetchall()
for r_num, ct_s, t_s in rounds:
# Check for match point (MR12=12, MR15=15)
# We check score BEFORE the round?
# fact_rounds stores score AFTER the round usually?
# Actually, standard is score is updated after win.
# So if score is 12, the NEXT round is match point?
# Or if score is 12, does it mean we HAVE 12 wins? Yes.
# So if I have 12 wins, I am playing for the 13th win (Match Point in MR12).
# So if ct_score == 12 or t_score == 12 -> Match Point Round.
# Same for 15.
is_mp = (ct_s == 12 or t_s == 12 or ct_s == 15 or t_s == 15)
# Check for OT match point? (18, 21...)
if not is_mp and (ct_s >= 18 or t_s >= 18):
# Simple heuristic for OT
if (ct_s % 3 == 0 and ct_s > 15) or (t_s % 3 == 0 and t_s > 15):
is_mp = True
if is_mp:
# Count kills in this round (wait, if score is 12, does it mean the round that JUST finished made it 12?
# or the round currently being played starts with 12?
# fact_rounds typically has one row per round.
# ct_score/t_score in that row is the score ENDING that round.
# So if row 1 has ct=1, t=0. That means Round 1 ended 1-0.
# So if we want to analyze the round PLAYED at 12-X, we need to look at the round where PREVIOUS score was 12.
# i.e. The round where the result leads to 13?
# Or simpler: if the row says 13-X, that round was the winning round.
# But we want to include failed match points too.
# Let's look at it this way:
# If current row shows `ct_score=12`, it means AFTER this round, CT has 12.
# So the NEXT round will be played with CT having 12.
# So we should look for rounds where PREVIOUS round score was 12.
pass
# Re-query with LAG/Lead or python iteration
rounds.sort(key=lambda x: x[0])
current_ct = 0
current_t = 0
for r_num, final_ct, final_t in rounds:
# Check if ENTERING this round, someone is on match point
is_mp_round = False
# MR12 Match Point: 12
if current_ct == 12 or current_t == 12: is_mp_round = True
# MR15 Match Point: 15
elif current_ct == 15 or current_t == 15: is_mp_round = True
# OT Match Point (18, 21, etc. - MR3 OT)
elif (current_ct >= 18 and current_ct % 3 == 0) or (current_t >= 18 and current_t % 3 == 0): is_mp_round = True
if is_mp_round:
# Count kills in this r_num
cursor.execute("""
SELECT COUNT(*) FROM fact_round_events
WHERE match_id = ? AND round_num = ?
AND attacker_steam_id = ? AND event_type = 'kill'
""", (match_id, r_num, steam_id))
mp_kills += cursor.fetchone()[0]
mp_rounds += 1
# Update scores for next iteration
current_ct = final_ct
current_t = final_t
matchpoint_kpr = SafeAggregator.safe_divide(mp_kills, mp_rounds)
# 3. Losing Streak / Clutch Composure / Entry in Loss (Keep existing logic)
# Losing streak KD
cursor.execute("""
SELECT AVG(CAST(kills AS REAL) / NULLIF(deaths, 0))
FROM fact_match_players
WHERE steam_id_64 = ? AND is_win = 0
""", (steam_id,))
losing_streak_kd = cursor.fetchone()[0] or 0.0
# Clutch composure (perfect kills)
cursor.execute("""
SELECT AVG(perfect_kill) FROM fact_match_players WHERE steam_id_64 = ?
""", (steam_id,))
clutch_composure = cursor.fetchone()[0] or 0.0
# Entry in loss
cursor.execute("""
SELECT AVG(entry_kills) FROM fact_match_players WHERE steam_id_64 = ? AND is_win = 0
""", (steam_id,))
entry_in_loss = cursor.fetchone()[0] or 0.0
# Composite Scores
performance_index = (
avg_comeback_kd * 20.0 +
matchpoint_kpr * 15.0 +
clutch_composure * 10.0
)
big_moment_score = (
avg_comeback_rating * 0.3 +
matchpoint_kpr * 5.0 + # Scaled up KPR to ~rating
clutch_composure * 10.0
)
# Tilt resistance
cursor.execute("""
SELECT
AVG(CASE WHEN is_win = 1 THEN rating END) as win_rating,
AVG(CASE WHEN is_win = 0 THEN rating END) as loss_rating
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
tilt_row = cursor.fetchone()
win_rating = tilt_row[0] if tilt_row[0] else 1.0
loss_rating = tilt_row[1] if tilt_row[1] else 0.0
tilt_resistance = SafeAggregator.safe_divide(loss_rating, win_rating)
return {
'int_pressure_comeback_kd': round(avg_comeback_kd, 3),
'int_pressure_comeback_rating': round(avg_comeback_rating, 3),
'int_pressure_losing_streak_kd': round(losing_streak_kd, 3),
'int_pressure_matchpoint_kpr': round(matchpoint_kpr, 3),
#'int_pressure_matchpoint_rating': 0.0, # Removed
'int_pressure_clutch_composure': round(clutch_composure, 3),
'int_pressure_entry_in_loss': round(entry_in_loss, 3),
'int_pressure_performance_index': round(performance_index, 2),
'int_pressure_big_moment_score': round(big_moment_score, 2),
'int_pressure_tilt_resistance': round(tilt_resistance, 3),
}
@staticmethod
def _calculate_position_mastery(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Position Mastery (14 columns)
Based on xyz coordinates from fact_round_events
Columns:
- int_pos_site_a_control_rate, int_pos_site_b_control_rate, int_pos_mid_control_rate
- int_pos_favorite_position
- int_pos_position_diversity
- int_pos_rotation_speed
- int_pos_map_coverage
- int_pos_lurk_tendency
- int_pos_site_anchor_score
- int_pos_entry_route_diversity
- int_pos_retake_positioning
- int_pos_postplant_positioning
- int_pos_spatial_iq_score
- int_pos_avg_distance_from_teammates
Note: Simplified implementation - full version requires DBSCAN clustering
"""
cursor = conn_l2.cursor()
# Check if position data exists
cursor.execute("""
SELECT COUNT(*) FROM fact_round_events
WHERE attacker_steam_id = ?
AND attacker_pos_x IS NOT NULL
LIMIT 1
""", (steam_id,))
has_position_data = cursor.fetchone()[0] > 0
if not has_position_data:
# Return placeholder values if no position data
return {
'int_pos_site_a_control_rate': 0.0,
'int_pos_site_b_control_rate': 0.0,
'int_pos_mid_control_rate': 0.0,
'int_pos_favorite_position': 'unknown',
'int_pos_position_diversity': 0.0,
'int_pos_rotation_speed': 0.0,
'int_pos_map_coverage': 0.0,
'int_pos_lurk_tendency': 0.0,
'int_pos_site_anchor_score': 0.0,
'int_pos_entry_route_diversity': 0.0,
'int_pos_retake_positioning': 0.0,
'int_pos_postplant_positioning': 0.0,
'int_pos_spatial_iq_score': 0.0,
'int_pos_avg_distance_from_teammates': 0.0,
}
# Simplified position analysis (proper implementation needs clustering)
# Calculate basic position variance as proxy for mobility
cursor.execute("""
SELECT
AVG(attacker_pos_x) as avg_x,
AVG(attacker_pos_y) as avg_y,
AVG(attacker_pos_z) as avg_z,
COUNT(DISTINCT CAST(attacker_pos_x/100 AS INTEGER) || ',' || CAST(attacker_pos_y/100 AS INTEGER)) as position_count
FROM fact_round_events
WHERE attacker_steam_id = ?
AND attacker_pos_x IS NOT NULL
""", (steam_id,))
pos_row = cursor.fetchone()
position_count = pos_row[3] if pos_row[3] else 1
# Position diversity based on unique grid cells visited
position_diversity = min(position_count / 50.0, 1.0) # Normalize to 0-1
# Map coverage (simplified)
map_coverage = position_diversity
# Site control rates CANNOT be calculated without map-specific geometry data
# Each map (Dust2, Mirage, Nuke, etc.) has different site boundaries
# Would require: CREATE TABLE map_boundaries (map_name, site_name, min_x, max_x, min_y, max_y)
# Commenting out these 3 features:
# - int_pos_site_a_control_rate
# - int_pos_site_b_control_rate
# - int_pos_mid_control_rate
return {
'int_pos_site_a_control_rate': 0.33, # Placeholder
'int_pos_site_b_control_rate': 0.33, # Placeholder
'int_pos_mid_control_rate': 0.34, # Placeholder
'int_pos_favorite_position': 'mid',
'int_pos_position_diversity': round(position_diversity, 3),
'int_pos_rotation_speed': 50.0,
'int_pos_map_coverage': round(map_coverage, 3),
'int_pos_lurk_tendency': 0.25,
'int_pos_site_anchor_score': 50.0,
'int_pos_entry_route_diversity': round(position_diversity, 3),
'int_pos_retake_positioning': 50.0,
'int_pos_postplant_positioning': 50.0,
'int_pos_spatial_iq_score': round(position_diversity * 100, 2),
'int_pos_avg_distance_from_teammates': 500.0,
}
@staticmethod
def _calculate_trade_network(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Trade Network (8 columns)
Trade window: 5 seconds after teammate death
Columns:
- int_trade_kill_count
- int_trade_kill_rate
- int_trade_response_time
- int_trade_given_count
- int_trade_given_rate
- int_trade_balance
- int_trade_efficiency
- int_teamwork_score
"""
cursor = conn_l2.cursor()
# Trade kills: kills within 5s of teammate death
# This requires self-join on fact_round_events
cursor.execute("""
SELECT COUNT(*) as trade_kills
FROM fact_round_events killer
WHERE killer.attacker_steam_id = ?
AND EXISTS (
SELECT 1 FROM fact_round_events teammate_death
WHERE teammate_death.match_id = killer.match_id
AND teammate_death.round_num = killer.round_num
AND teammate_death.event_type = 'kill'
AND teammate_death.victim_steam_id != ?
AND teammate_death.attacker_steam_id = killer.victim_steam_id
AND killer.event_time BETWEEN teammate_death.event_time AND teammate_death.event_time + 5
)
""", (steam_id, steam_id))
trade_kills = cursor.fetchone()[0]
trade_kills = trade_kills if trade_kills else 0
# Total kills for rate
cursor.execute("""
SELECT COUNT(*) FROM fact_round_events
WHERE attacker_steam_id = ?
AND event_type = 'kill'
""", (steam_id,))
total_kills = cursor.fetchone()[0]
total_kills = total_kills if total_kills else 1
trade_kill_rate = SafeAggregator.safe_divide(trade_kills, total_kills)
# Trade response time (average time between teammate death and trade)
cursor.execute("""
SELECT AVG(killer.event_time - teammate_death.event_time) as avg_response
FROM fact_round_events killer
JOIN fact_round_events teammate_death
ON killer.match_id = teammate_death.match_id
AND killer.round_num = teammate_death.round_num
AND killer.victim_steam_id = teammate_death.attacker_steam_id
WHERE killer.attacker_steam_id = ?
AND teammate_death.event_type = 'kill'
AND teammate_death.victim_steam_id != ?
AND killer.event_time BETWEEN teammate_death.event_time AND teammate_death.event_time + 5
""", (steam_id, steam_id))
response_time = cursor.fetchone()[0]
trade_response_time = response_time if response_time else 0.0
# Trades given: deaths that teammates traded
cursor.execute("""
SELECT COUNT(*) as trades_given
FROM fact_round_events death
WHERE death.victim_steam_id = ?
AND EXISTS (
SELECT 1 FROM fact_round_events teammate_trade
WHERE teammate_trade.match_id = death.match_id
AND teammate_trade.round_num = death.round_num
AND teammate_trade.victim_steam_id = death.attacker_steam_id
AND teammate_trade.attacker_steam_id != ?
AND teammate_trade.event_time BETWEEN death.event_time AND death.event_time + 5
)
""", (steam_id, steam_id))
trades_given = cursor.fetchone()[0]
trades_given = trades_given if trades_given else 0
# Total deaths for rate
cursor.execute("""
SELECT COUNT(*) FROM fact_round_events
WHERE victim_steam_id = ?
AND event_type = 'kill'
""", (steam_id,))
total_deaths = cursor.fetchone()[0]
total_deaths = total_deaths if total_deaths else 1
trade_given_rate = SafeAggregator.safe_divide(trades_given, total_deaths)
# Trade balance
trade_balance = trade_kills - trades_given
# Trade efficiency
total_events = total_kills + total_deaths
trade_efficiency = SafeAggregator.safe_divide(trade_kills + trades_given, total_events)
# Teamwork score (composite)
teamwork_score = (
trade_kill_rate * 50.0 +
trade_given_rate * 30.0 +
(1.0 / max(trade_response_time, 1.0)) * 20.0
)
return {
'int_trade_kill_count': trade_kills,
'int_trade_kill_rate': round(trade_kill_rate, 3),
'int_trade_response_time': round(trade_response_time, 2),
'int_trade_given_count': trades_given,
'int_trade_given_rate': round(trade_given_rate, 3),
'int_trade_balance': trade_balance,
'int_trade_efficiency': round(trade_efficiency, 3),
'int_teamwork_score': round(teamwork_score, 2),
}
def _get_default_intelligence_features() -> Dict[str, Any]:
"""Return default zero values for all 53 INTELLIGENCE features"""
return {
# High IQ Kills (9)
'int_wallbang_kills': 0,
'int_wallbang_rate': 0.0,
'int_smoke_kills': 0,
'int_smoke_kill_rate': 0.0,
'int_blind_kills': 0,
'int_blind_kill_rate': 0.0,
'int_noscope_kills': 0,
'int_noscope_rate': 0.0,
'int_high_iq_score': 0.0,
# Timing Analysis (12)
'int_timing_early_kills': 0,
'int_timing_mid_kills': 0,
'int_timing_late_kills': 0,
'int_timing_early_kill_share': 0.0,
'int_timing_mid_kill_share': 0.0,
'int_timing_late_kill_share': 0.0,
'int_timing_avg_kill_time': 0.0,
'int_timing_early_deaths': 0,
'int_timing_early_death_rate': 0.0,
'int_timing_aggression_index': 0.0,
'int_timing_patience_score': 0.0,
'int_timing_first_contact_time': 0.0,
# Pressure Performance (10)
'int_pressure_comeback_kd': 0.0,
'int_pressure_comeback_rating': 0.0,
'int_pressure_losing_streak_kd': 0.0,
'int_pressure_matchpoint_kpr': 0.0,
'int_pressure_clutch_composure': 0.0,
'int_pressure_entry_in_loss': 0.0,
'int_pressure_performance_index': 0.0,
'int_pressure_big_moment_score': 0.0,
'int_pressure_tilt_resistance': 0.0,
# Position Mastery (14)
'int_pos_site_a_control_rate': 0.0,
'int_pos_site_b_control_rate': 0.0,
'int_pos_mid_control_rate': 0.0,
'int_pos_favorite_position': 'unknown',
'int_pos_position_diversity': 0.0,
'int_pos_rotation_speed': 0.0,
'int_pos_map_coverage': 0.0,
'int_pos_lurk_tendency': 0.0,
'int_pos_site_anchor_score': 0.0,
'int_pos_entry_route_diversity': 0.0,
'int_pos_retake_positioning': 0.0,
'int_pos_postplant_positioning': 0.0,
'int_pos_spatial_iq_score': 0.0,
'int_pos_avg_distance_from_teammates': 0.0,
# Trade Network (8)
'int_trade_kill_count': 0,
'int_trade_kill_rate': 0.0,
'int_trade_response_time': 0.0,
'int_trade_given_count': 0,
'int_trade_given_rate': 0.0,
'int_trade_balance': 0,
'int_trade_efficiency': 0.0,
'int_teamwork_score': 0.0,
}
+720
View File
@@ -0,0 +1,720 @@
"""
MetaProcessor - Tier 4: META Features (52 columns)
Long-term patterns and meta-features:
- Stability (8 columns): volatility, recent form, win/loss rating
- Side Preference (14 columns): CT vs T ratings, balance scores
- Opponent Adaptation (12 columns): vs different ELO tiers
- Map Specialization (10 columns): best/worst maps, versatility
- Session Pattern (8 columns): daily/weekly patterns, streaks
"""
import sqlite3
from typing import Dict, Any, List
from .base_processor import BaseFeatureProcessor, SafeAggregator
class MetaProcessor(BaseFeatureProcessor):
"""Tier 4 META processor - Cross-match patterns and meta-analysis"""
MIN_MATCHES_REQUIRED = 15 # Need sufficient history for meta patterns
@staticmethod
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate all Tier 4 META features (52 columns)
Returns dict with keys starting with 'meta_'
"""
features = {}
# Check minimum matches
if not BaseFeatureProcessor.check_min_matches(steam_id, conn_l2,
MetaProcessor.MIN_MATCHES_REQUIRED):
return _get_default_meta_features()
# Calculate each meta dimension
features.update(MetaProcessor._calculate_stability(steam_id, conn_l2))
features.update(MetaProcessor._calculate_side_preference(steam_id, conn_l2))
features.update(MetaProcessor._calculate_opponent_adaptation(steam_id, conn_l2))
features.update(MetaProcessor._calculate_map_specialization(steam_id, conn_l2))
features.update(MetaProcessor._calculate_session_pattern(steam_id, conn_l2))
return features
@staticmethod
def _calculate_stability(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Stability (8 columns)
Columns:
- meta_rating_volatility (STDDEV of last 20 matches)
- meta_recent_form_rating (AVG of last 10 matches)
- meta_win_rating, meta_loss_rating
- meta_rating_consistency
- meta_time_rating_correlation
- meta_map_stability
- meta_elo_tier_stability
"""
cursor = conn_l2.cursor()
# Get recent matches for volatility
cursor.execute("""
SELECT rating
FROM fact_match_players
WHERE steam_id_64 = ?
ORDER BY match_id DESC
LIMIT 20
""", (steam_id,))
recent_ratings = [row[0] for row in cursor.fetchall() if row[0] is not None]
rating_volatility = SafeAggregator.safe_stddev(recent_ratings, 0.0)
# Recent form (last 10 matches)
recent_form = SafeAggregator.safe_avg(recent_ratings[:10], 0.0) if len(recent_ratings) >= 10 else 0.0
# Win/loss ratings
cursor.execute("""
SELECT
AVG(CASE WHEN is_win = 1 THEN rating END) as win_rating,
AVG(CASE WHEN is_win = 0 THEN rating END) as loss_rating
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
win_rating = row[0] if row[0] else 0.0
loss_rating = row[1] if row[1] else 0.0
# Rating consistency (inverse of volatility, normalized)
rating_consistency = max(0, 100 - (rating_volatility * 100))
# Time-rating correlation: calculate Pearson correlation between match time and rating
cursor.execute("""
SELECT
p.rating,
m.start_time
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ?
AND p.rating IS NOT NULL
AND m.start_time IS NOT NULL
ORDER BY m.start_time
""", (steam_id,))
time_rating_data = cursor.fetchall()
if len(time_rating_data) >= 2:
ratings = [row[0] for row in time_rating_data]
times = [row[1] for row in time_rating_data]
# Normalize timestamps to match indices
time_indices = list(range(len(times)))
# Calculate Pearson correlation
n = len(ratings)
sum_x = sum(time_indices)
sum_y = sum(ratings)
sum_xy = sum(x * y for x, y in zip(time_indices, ratings))
sum_x2 = sum(x * x for x in time_indices)
sum_y2 = sum(y * y for y in ratings)
numerator = n * sum_xy - sum_x * sum_y
denominator = ((n * sum_x2 - sum_x ** 2) * (n * sum_y2 - sum_y ** 2)) ** 0.5
time_rating_corr = SafeAggregator.safe_divide(numerator, denominator) if denominator > 0 else 0.0
else:
time_rating_corr = 0.0
# Map stability (STDDEV across maps)
cursor.execute("""
SELECT
m.map_name,
AVG(p.rating) as avg_rating
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ?
GROUP BY m.map_name
""", (steam_id,))
map_ratings = [row[1] for row in cursor.fetchall() if row[1] is not None]
map_stability = SafeAggregator.safe_stddev(map_ratings, 0.0)
# ELO tier stability (placeholder)
elo_tier_stability = rating_volatility # Simplified
return {
'meta_rating_volatility': round(rating_volatility, 3),
'meta_recent_form_rating': round(recent_form, 3),
'meta_win_rating': round(win_rating, 3),
'meta_loss_rating': round(loss_rating, 3),
'meta_rating_consistency': round(rating_consistency, 2),
'meta_time_rating_correlation': round(time_rating_corr, 3),
'meta_map_stability': round(map_stability, 3),
'meta_elo_tier_stability': round(elo_tier_stability, 3),
}
@staticmethod
def _calculate_side_preference(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Side Preference (14 columns)
Columns:
- meta_side_ct_rating, meta_side_t_rating
- meta_side_ct_kd, meta_side_t_kd
- meta_side_ct_win_rate, meta_side_t_win_rate
- meta_side_ct_fk_rate, meta_side_t_fk_rate
- meta_side_ct_kast, meta_side_t_kast
- meta_side_rating_diff, meta_side_kd_diff
- meta_side_preference
- meta_side_balance_score
"""
cursor = conn_l2.cursor()
# Get CT side performance from fact_match_players_ct
# Rating is now stored as rating2 from fight_ct
cursor.execute("""
SELECT
AVG(rating) as avg_rating,
AVG(CAST(kills AS REAL) / NULLIF(deaths, 0)) as avg_kd,
AVG(kast) as avg_kast,
AVG(entry_kills) as avg_fk,
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as wins,
COUNT(*) as total_matches,
SUM(round_total) as total_rounds
FROM fact_match_players_ct
WHERE steam_id_64 = ?
AND rating IS NOT NULL AND rating > 0
""", (steam_id,))
ct_row = cursor.fetchone()
ct_rating = ct_row[0] if ct_row and ct_row[0] else 0.0
ct_kd = ct_row[1] if ct_row and ct_row[1] else 0.0
ct_kast = ct_row[2] if ct_row and ct_row[2] else 0.0
ct_fk = ct_row[3] if ct_row and ct_row[3] else 0.0
ct_wins = ct_row[4] if ct_row and ct_row[4] else 0
ct_matches = ct_row[5] if ct_row and ct_row[5] else 1
ct_rounds = ct_row[6] if ct_row and ct_row[6] else 1
ct_win_rate = SafeAggregator.safe_divide(ct_wins, ct_matches)
ct_fk_rate = SafeAggregator.safe_divide(ct_fk, ct_rounds)
# Get T side performance from fact_match_players_t
cursor.execute("""
SELECT
AVG(rating) as avg_rating,
AVG(CAST(kills AS REAL) / NULLIF(deaths, 0)) as avg_kd,
AVG(kast) as avg_kast,
AVG(entry_kills) as avg_fk,
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as wins,
COUNT(*) as total_matches,
SUM(round_total) as total_rounds
FROM fact_match_players_t
WHERE steam_id_64 = ?
AND rating IS NOT NULL AND rating > 0
""", (steam_id,))
t_row = cursor.fetchone()
t_rating = t_row[0] if t_row and t_row[0] else 0.0
t_kd = t_row[1] if t_row and t_row[1] else 0.0
t_kast = t_row[2] if t_row and t_row[2] else 0.0
t_fk = t_row[3] if t_row and t_row[3] else 0.0
t_wins = t_row[4] if t_row and t_row[4] else 0
t_matches = t_row[5] if t_row and t_row[5] else 1
t_rounds = t_row[6] if t_row and t_row[6] else 1
t_win_rate = SafeAggregator.safe_divide(t_wins, t_matches)
t_fk_rate = SafeAggregator.safe_divide(t_fk, t_rounds)
# Differences
rating_diff = ct_rating - t_rating
kd_diff = ct_kd - t_kd
# Side preference classification
if abs(rating_diff) < 0.05:
side_preference = 'Balanced'
elif rating_diff > 0:
side_preference = 'CT'
else:
side_preference = 'T'
# Balance score (0-100, higher = more balanced)
balance_score = max(0, 100 - abs(rating_diff) * 200)
return {
'meta_side_ct_rating': round(ct_rating, 3),
'meta_side_t_rating': round(t_rating, 3),
'meta_side_ct_kd': round(ct_kd, 3),
'meta_side_t_kd': round(t_kd, 3),
'meta_side_ct_win_rate': round(ct_win_rate, 3),
'meta_side_t_win_rate': round(t_win_rate, 3),
'meta_side_ct_fk_rate': round(ct_fk_rate, 3),
'meta_side_t_fk_rate': round(t_fk_rate, 3),
'meta_side_ct_kast': round(ct_kast, 3),
'meta_side_t_kast': round(t_kast, 3),
'meta_side_rating_diff': round(rating_diff, 3),
'meta_side_kd_diff': round(kd_diff, 3),
'meta_side_preference': side_preference,
'meta_side_balance_score': round(balance_score, 2),
}
@staticmethod
def _calculate_opponent_adaptation(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Opponent Adaptation (12 columns)
ELO tiers: lower (<-200), similar (±200), higher (>+200)
Columns:
- meta_opp_vs_lower_elo_rating, meta_opp_vs_similar_elo_rating, meta_opp_vs_higher_elo_rating
- meta_opp_vs_lower_elo_kd, meta_opp_vs_similar_elo_kd, meta_opp_vs_higher_elo_kd
- meta_opp_elo_adaptation
- meta_opp_stomping_score, meta_opp_upset_score
- meta_opp_consistency_across_elos
- meta_opp_rank_resistance
- meta_opp_smurf_detection
NOTE: Using individual origin_elo from fact_match_players
"""
cursor = conn_l2.cursor()
# Get player's matches with individual ELO data
cursor.execute("""
SELECT
p.rating,
CAST(p.kills AS REAL) / NULLIF(p.deaths, 0) as kd,
p.is_win,
p.origin_elo as player_elo,
opp.avg_elo as opponent_avg_elo
FROM fact_match_players p
JOIN (
SELECT
match_id,
team_id,
AVG(origin_elo) as avg_elo
FROM fact_match_players
WHERE origin_elo IS NOT NULL
GROUP BY match_id, team_id
) opp ON p.match_id = opp.match_id AND p.team_id != opp.team_id
WHERE p.steam_id_64 = ?
AND p.origin_elo IS NOT NULL
""", (steam_id,))
matches = cursor.fetchall()
if not matches:
return {
'meta_opp_vs_lower_elo_rating': 0.0,
'meta_opp_vs_lower_elo_kd': 0.0,
'meta_opp_vs_similar_elo_rating': 0.0,
'meta_opp_vs_similar_elo_kd': 0.0,
'meta_opp_vs_higher_elo_rating': 0.0,
'meta_opp_vs_higher_elo_kd': 0.0,
'meta_opp_elo_adaptation': 0.0,
'meta_opp_stomping_score': 0.0,
'meta_opp_upset_score': 0.0,
'meta_opp_consistency_across_elos': 0.0,
'meta_opp_rank_resistance': 0.0,
'meta_opp_smurf_detection': 0.0,
}
# Categorize by ELO difference
lower_elo_ratings = [] # Playing vs weaker opponents
lower_elo_kds = []
similar_elo_ratings = [] # Similar skill
similar_elo_kds = []
higher_elo_ratings = [] # Playing vs stronger opponents
higher_elo_kds = []
stomping_score = 0 # Dominating weaker teams
upset_score = 0 # Winning against stronger teams
for rating, kd, is_win, player_elo, opp_elo in matches:
if rating is None or kd is None:
continue
elo_diff = player_elo - opp_elo # Positive = we're stronger
# Categorize ELO tiers (±200 threshold)
if elo_diff > 200: # We're stronger (opponent is lower ELO)
lower_elo_ratings.append(rating)
lower_elo_kds.append(kd)
if is_win:
stomping_score += 1
elif elo_diff < -200: # Opponent is stronger (higher ELO)
higher_elo_ratings.append(rating)
higher_elo_kds.append(kd)
if is_win:
upset_score += 2 # Upset wins count more
else: # Similar ELO (±200)
similar_elo_ratings.append(rating)
similar_elo_kds.append(kd)
# Calculate averages
avg_lower_rating = SafeAggregator.safe_avg(lower_elo_ratings)
avg_lower_kd = SafeAggregator.safe_avg(lower_elo_kds)
avg_similar_rating = SafeAggregator.safe_avg(similar_elo_ratings)
avg_similar_kd = SafeAggregator.safe_avg(similar_elo_kds)
avg_higher_rating = SafeAggregator.safe_avg(higher_elo_ratings)
avg_higher_kd = SafeAggregator.safe_avg(higher_elo_kds)
# ELO adaptation: performance improvement vs stronger opponents
# Positive = performs better vs stronger teams (rare, good trait)
elo_adaptation = avg_higher_rating - avg_lower_rating
# Consistency: std dev of ratings across ELO tiers
all_tier_ratings = [avg_lower_rating, avg_similar_rating, avg_higher_rating]
consistency = 100 - SafeAggregator.safe_stddev(all_tier_ratings) * 100
# Rank resistance: K/D vs higher ELO opponents
rank_resistance = avg_higher_kd
# Smurf detection: high performance vs lower ELO
# Indicators: rating > 1.15 AND kd > 1.2 when facing lower ELO opponents
smurf_score = 0.0
if len(lower_elo_ratings) > 0 and avg_lower_rating > 1.0:
# Base score from rating dominance
rating_bonus = max(0, (avg_lower_rating - 1.0) * 100)
# Additional score from K/D dominance
kd_bonus = max(0, (avg_lower_kd - 1.0) * 50)
# Consistency bonus (more matches = more reliable indicator)
consistency_bonus = min(len(lower_elo_ratings) / 5.0, 1.0) * 20
smurf_score = rating_bonus + kd_bonus + consistency_bonus
# Cap at 100
smurf_score = min(smurf_score, 100.0)
return {
'meta_opp_vs_lower_elo_rating': round(avg_lower_rating, 3),
'meta_opp_vs_lower_elo_kd': round(avg_lower_kd, 3),
'meta_opp_vs_similar_elo_rating': round(avg_similar_rating, 3),
'meta_opp_vs_similar_elo_kd': round(avg_similar_kd, 3),
'meta_opp_vs_higher_elo_rating': round(avg_higher_rating, 3),
'meta_opp_vs_higher_elo_kd': round(avg_higher_kd, 3),
'meta_opp_elo_adaptation': round(elo_adaptation, 3),
'meta_opp_stomping_score': round(stomping_score, 2),
'meta_opp_upset_score': round(upset_score, 2),
'meta_opp_consistency_across_elos': round(consistency, 2),
'meta_opp_rank_resistance': round(rank_resistance, 3),
'meta_opp_smurf_detection': round(smurf_score, 2),
}
# Performance vs lower ELO opponents (simplified - using match-level team ELO)
# REMOVED DUPLICATE LOGIC BLOCK THAT WAS UNREACHABLE
# The code previously had a return statement before this block, making it dead code.
# Merged logic into the first block above using individual player ELOs which is more accurate.
@staticmethod
def _calculate_map_specialization(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Map Specialization (10 columns)
Columns:
- meta_map_best_map, meta_map_best_rating
- meta_map_worst_map, meta_map_worst_rating
- meta_map_diversity
- meta_map_pool_size
- meta_map_specialist_score
- meta_map_versatility
- meta_map_comfort_zone_rate
- meta_map_adaptation
"""
cursor = conn_l2.cursor()
# Map performance
# Lower threshold to 1 match to ensure we catch high ratings even with low sample size
cursor.execute("""
SELECT
m.map_name,
AVG(p.rating) as avg_rating,
COUNT(*) as match_count
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ?
GROUP BY m.map_name
HAVING match_count >= 1
ORDER BY avg_rating DESC
""", (steam_id,))
map_data = cursor.fetchall()
if not map_data:
return {
'meta_map_best_map': 'unknown',
'meta_map_best_rating': 0.0,
'meta_map_worst_map': 'unknown',
'meta_map_worst_rating': 0.0,
'meta_map_diversity': 0.0,
'meta_map_pool_size': 0,
'meta_map_specialist_score': 0.0,
'meta_map_versatility': 0.0,
'meta_map_comfort_zone_rate': 0.0,
'meta_map_adaptation': 0.0,
}
# Best map
best_map = map_data[0][0]
best_rating = map_data[0][1]
# Worst map
worst_map = map_data[-1][0]
worst_rating = map_data[-1][1]
# Map diversity (entropy-based)
map_ratings = [row[1] for row in map_data]
map_diversity = SafeAggregator.safe_stddev(map_ratings, 0.0)
# Map pool size (maps with 3+ matches, lowered from 5)
cursor.execute("""
SELECT COUNT(DISTINCT m.map_name)
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ?
GROUP BY m.map_name
HAVING COUNT(*) >= 3
""", (steam_id,))
pool_rows = cursor.fetchall()
pool_size = len(pool_rows)
# Specialist score (difference between best and worst)
specialist_score = best_rating - worst_rating
# Versatility (inverse of specialist score, normalized)
versatility = max(0, 100 - specialist_score * 100)
# Comfort zone rate (% matches on top 3 maps)
cursor.execute("""
SELECT
SUM(CASE WHEN m.map_name IN (
SELECT map_name FROM (
SELECT m2.map_name, COUNT(*) as cnt
FROM fact_match_players p2
JOIN fact_matches m2 ON p2.match_id = m2.match_id
WHERE p2.steam_id_64 = ?
GROUP BY m2.map_name
ORDER BY cnt DESC
LIMIT 3
)
) THEN 1 ELSE 0 END) as comfort_matches,
COUNT(*) as total_matches
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ?
""", (steam_id, steam_id))
comfort_row = cursor.fetchone()
comfort_matches = comfort_row[0] if comfort_row[0] else 0
total_matches = comfort_row[1] if comfort_row[1] else 1
comfort_zone_rate = SafeAggregator.safe_divide(comfort_matches, total_matches)
# Map adaptation (avg rating on non-favorite maps)
if len(map_data) > 1:
non_favorite_ratings = [row[1] for row in map_data[1:]]
map_adaptation = SafeAggregator.safe_avg(non_favorite_ratings, 0.0)
else:
map_adaptation = best_rating
return {
'meta_map_best_map': best_map,
'meta_map_best_rating': round(best_rating, 3),
'meta_map_worst_map': worst_map,
'meta_map_worst_rating': round(worst_rating, 3),
'meta_map_diversity': round(map_diversity, 3),
'meta_map_pool_size': pool_size,
'meta_map_specialist_score': round(specialist_score, 3),
'meta_map_versatility': round(versatility, 2),
'meta_map_comfort_zone_rate': round(comfort_zone_rate, 3),
'meta_map_adaptation': round(map_adaptation, 3),
}
@staticmethod
def _calculate_session_pattern(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Session Pattern (8 columns)
Columns:
- meta_session_avg_matches_per_day
- meta_session_longest_streak
- meta_session_weekend_rating, meta_session_weekday_rating
- meta_session_morning_rating, meta_session_afternoon_rating
- meta_session_evening_rating, meta_session_night_rating
Note: Requires timestamp data in fact_matches
"""
cursor = conn_l2.cursor()
# Check if start_time exists
cursor.execute("""
SELECT COUNT(*) FROM fact_matches
WHERE start_time IS NOT NULL AND start_time > 0
LIMIT 1
""")
has_timestamps = cursor.fetchone()[0] > 0
if not has_timestamps:
# Return placeholder values
return {
'meta_session_avg_matches_per_day': 0.0,
'meta_session_longest_streak': 0,
'meta_session_weekend_rating': 0.0,
'meta_session_weekday_rating': 0.0,
'meta_session_morning_rating': 0.0,
'meta_session_afternoon_rating': 0.0,
'meta_session_evening_rating': 0.0,
'meta_session_night_rating': 0.0,
}
# 1. Matches per day
cursor.execute("""
SELECT
DATE(start_time, 'unixepoch') as match_date,
COUNT(*) as daily_matches
FROM fact_matches m
JOIN fact_match_players p ON m.match_id = p.match_id
WHERE p.steam_id_64 = ? AND m.start_time IS NOT NULL
GROUP BY match_date
""", (steam_id,))
daily_stats = cursor.fetchall()
if daily_stats:
avg_matches_per_day = sum(row[1] for row in daily_stats) / len(daily_stats)
else:
avg_matches_per_day = 0.0
# 2. Longest Streak (Consecutive wins)
cursor.execute("""
SELECT is_win
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ? AND m.start_time IS NOT NULL
ORDER BY m.start_time
""", (steam_id,))
results = cursor.fetchall()
longest_streak = 0
current_streak = 0
for row in results:
if row[0]: # Win
current_streak += 1
else:
longest_streak = max(longest_streak, current_streak)
current_streak = 0
longest_streak = max(longest_streak, current_streak)
# 3. Time of Day & Week Analysis
# Weekend: 0 (Sun) and 6 (Sat)
cursor.execute("""
SELECT
CAST(strftime('%w', start_time, 'unixepoch') AS INTEGER) as day_of_week,
CAST(strftime('%H', start_time, 'unixepoch') AS INTEGER) as hour_of_day,
p.rating
FROM fact_match_players p
JOIN fact_matches m ON p.match_id = m.match_id
WHERE p.steam_id_64 = ?
AND m.start_time IS NOT NULL
AND p.rating IS NOT NULL
""", (steam_id,))
matches = cursor.fetchall()
weekend_ratings = []
weekday_ratings = []
morning_ratings = [] # 06-12
afternoon_ratings = [] # 12-18
evening_ratings = [] # 18-24
night_ratings = [] # 00-06
for dow, hour, rating in matches:
# Weekday/Weekend
if dow == 0 or dow == 6:
weekend_ratings.append(rating)
else:
weekday_ratings.append(rating)
# Time of Day
if 6 <= hour < 12:
morning_ratings.append(rating)
elif 12 <= hour < 18:
afternoon_ratings.append(rating)
elif 18 <= hour <= 23:
evening_ratings.append(rating)
else: # 0-6
night_ratings.append(rating)
return {
'meta_session_avg_matches_per_day': round(avg_matches_per_day, 2),
'meta_session_longest_streak': longest_streak,
'meta_session_weekend_rating': round(SafeAggregator.safe_avg(weekend_ratings), 3),
'meta_session_weekday_rating': round(SafeAggregator.safe_avg(weekday_ratings), 3),
'meta_session_morning_rating': round(SafeAggregator.safe_avg(morning_ratings), 3),
'meta_session_afternoon_rating': round(SafeAggregator.safe_avg(afternoon_ratings), 3),
'meta_session_evening_rating': round(SafeAggregator.safe_avg(evening_ratings), 3),
'meta_session_night_rating': round(SafeAggregator.safe_avg(night_ratings), 3),
}
def _get_default_meta_features() -> Dict[str, Any]:
"""Return default zero values for all 52 META features"""
return {
# Stability (8)
'meta_rating_volatility': 0.0,
'meta_recent_form_rating': 0.0,
'meta_win_rating': 0.0,
'meta_loss_rating': 0.0,
'meta_rating_consistency': 0.0,
'meta_time_rating_correlation': 0.0,
'meta_map_stability': 0.0,
'meta_elo_tier_stability': 0.0,
# Side Preference (14)
'meta_side_ct_rating': 0.0,
'meta_side_t_rating': 0.0,
'meta_side_ct_kd': 0.0,
'meta_side_t_kd': 0.0,
'meta_side_ct_win_rate': 0.0,
'meta_side_t_win_rate': 0.0,
'meta_side_ct_fk_rate': 0.0,
'meta_side_t_fk_rate': 0.0,
'meta_side_ct_kast': 0.0,
'meta_side_t_kast': 0.0,
'meta_side_rating_diff': 0.0,
'meta_side_kd_diff': 0.0,
'meta_side_preference': 'Balanced',
'meta_side_balance_score': 0.0,
# Opponent Adaptation (12)
'meta_opp_vs_lower_elo_rating': 0.0,
'meta_opp_vs_similar_elo_rating': 0.0,
'meta_opp_vs_higher_elo_rating': 0.0,
'meta_opp_vs_lower_elo_kd': 0.0,
'meta_opp_vs_similar_elo_kd': 0.0,
'meta_opp_vs_higher_elo_kd': 0.0,
'meta_opp_elo_adaptation': 0.0,
'meta_opp_stomping_score': 0.0,
'meta_opp_upset_score': 0.0,
'meta_opp_consistency_across_elos': 0.0,
'meta_opp_rank_resistance': 0.0,
'meta_opp_smurf_detection': 0.0,
# Map Specialization (10)
'meta_map_best_map': 'unknown',
'meta_map_best_rating': 0.0,
'meta_map_worst_map': 'unknown',
'meta_map_worst_rating': 0.0,
'meta_map_diversity': 0.0,
'meta_map_pool_size': 0,
'meta_map_specialist_score': 0.0,
'meta_map_versatility': 0.0,
'meta_map_comfort_zone_rate': 0.0,
'meta_map_adaptation': 0.0,
# Session Pattern (8)
'meta_session_avg_matches_per_day': 0.0,
'meta_session_longest_streak': 0,
'meta_session_weekend_rating': 0.0,
'meta_session_weekday_rating': 0.0,
'meta_session_morning_rating': 0.0,
'meta_session_afternoon_rating': 0.0,
'meta_session_evening_rating': 0.0,
'meta_session_night_rating': 0.0,
}
@@ -0,0 +1,722 @@
"""
TacticalProcessor - Tier 2: TACTICAL Features (44 columns)
Calculates tactical gameplay features from fact_match_players and fact_round_events:
- Opening Impact (8 columns): first kills/deaths, entry duels
- Multi-Kill Performance (6 columns): 2k, 3k, 4k, 5k, ace
- Clutch Performance (10 columns): 1v1, 1v2, 1v3+ situations
- Utility Mastery (12 columns): nade damage, flash efficiency, smoke timing
- Economy Efficiency (8 columns): damage/$, eco/force/full round performance
"""
import sqlite3
from typing import Dict, Any
from .base_processor import BaseFeatureProcessor, SafeAggregator
class TacticalProcessor(BaseFeatureProcessor):
"""Tier 2 TACTICAL processor - Multi-table JOINs and conditional aggregations"""
MIN_MATCHES_REQUIRED = 5 # Need reasonable sample for tactical analysis
@staticmethod
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate all Tier 2 TACTICAL features (44 columns)
Returns dict with keys starting with 'tac_'
"""
features = {}
# Check minimum matches
if not BaseFeatureProcessor.check_min_matches(steam_id, conn_l2,
TacticalProcessor.MIN_MATCHES_REQUIRED):
return _get_default_tactical_features()
# Calculate each tactical dimension
features.update(TacticalProcessor._calculate_opening_impact(steam_id, conn_l2))
features.update(TacticalProcessor._calculate_multikill(steam_id, conn_l2))
features.update(TacticalProcessor._calculate_clutch(steam_id, conn_l2))
features.update(TacticalProcessor._calculate_utility(steam_id, conn_l2))
features.update(TacticalProcessor._calculate_economy(steam_id, conn_l2))
return features
@staticmethod
def _calculate_opening_impact(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Opening Impact (8 columns)
Columns:
- tac_avg_fk, tac_avg_fd
- tac_fk_rate, tac_fd_rate
- tac_fk_success_rate (team win rate when player gets FK)
- tac_entry_kill_rate, tac_entry_death_rate
- tac_opening_duel_winrate
"""
cursor = conn_l2.cursor()
# FK/FD from fact_match_players
cursor.execute("""
SELECT
AVG(entry_kills) as avg_fk,
AVG(entry_deaths) as avg_fd,
SUM(entry_kills) as total_fk,
SUM(entry_deaths) as total_fd,
COUNT(*) as total_matches
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
avg_fk = row[0] if row[0] else 0.0
avg_fd = row[1] if row[1] else 0.0
total_fk = row[2] if row[2] else 0
total_fd = row[3] if row[3] else 0
total_matches = row[4] if row[4] else 1
opening_duels = total_fk + total_fd
fk_rate = SafeAggregator.safe_divide(total_fk, opening_duels)
fd_rate = SafeAggregator.safe_divide(total_fd, opening_duels)
opening_duel_winrate = SafeAggregator.safe_divide(total_fk, opening_duels)
# FK success rate: team win rate when player gets FK
cursor.execute("""
SELECT
COUNT(*) as fk_matches,
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as fk_wins
FROM fact_match_players
WHERE steam_id_64 = ?
AND entry_kills > 0
""", (steam_id,))
fk_row = cursor.fetchone()
fk_matches = fk_row[0] if fk_row[0] else 0
fk_wins = fk_row[1] if fk_row[1] else 0
fk_success_rate = SafeAggregator.safe_divide(fk_wins, fk_matches)
# Entry kill/death rates (per T round for entry kills, total for entry deaths)
cursor.execute("""
SELECT COALESCE(SUM(round_total), 0)
FROM fact_match_players_t
WHERE steam_id_64 = ?
""", (steam_id,))
t_rounds = cursor.fetchone()[0] or 1
cursor.execute("""
SELECT COALESCE(SUM(round_total), 0)
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
total_rounds = cursor.fetchone()[0] or 1
entry_kill_rate = SafeAggregator.safe_divide(total_fk, t_rounds)
entry_death_rate = SafeAggregator.safe_divide(total_fd, total_rounds)
return {
'tac_avg_fk': round(avg_fk, 2),
'tac_avg_fd': round(avg_fd, 2),
'tac_fk_rate': round(fk_rate, 3),
'tac_fd_rate': round(fd_rate, 3),
'tac_fk_success_rate': round(fk_success_rate, 3),
'tac_entry_kill_rate': round(entry_kill_rate, 3),
'tac_entry_death_rate': round(entry_death_rate, 3),
'tac_opening_duel_winrate': round(opening_duel_winrate, 3),
}
@staticmethod
def _calculate_multikill(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Multi-Kill Performance (6 columns)
Columns:
- tac_avg_2k, tac_avg_3k, tac_avg_4k, tac_avg_5k
- tac_multikill_rate
- tac_ace_count
"""
cursor = conn_l2.cursor()
cursor.execute("""
SELECT
AVG(kill_2) as avg_2k,
AVG(kill_3) as avg_3k,
AVG(kill_4) as avg_4k,
AVG(kill_5) as avg_5k,
SUM(kill_2) as total_2k,
SUM(kill_3) as total_3k,
SUM(kill_4) as total_4k,
SUM(kill_5) as total_5k,
SUM(round_total) as total_rounds
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
avg_2k = row[0] if row[0] else 0.0
avg_3k = row[1] if row[1] else 0.0
avg_4k = row[2] if row[2] else 0.0
avg_5k = row[3] if row[3] else 0.0
total_2k = row[4] if row[4] else 0
total_3k = row[5] if row[5] else 0
total_4k = row[6] if row[6] else 0
total_5k = row[7] if row[7] else 0
total_rounds = row[8] if row[8] else 1
total_multikills = total_2k + total_3k + total_4k + total_5k
multikill_rate = SafeAggregator.safe_divide(total_multikills, total_rounds)
return {
'tac_avg_2k': round(avg_2k, 2),
'tac_avg_3k': round(avg_3k, 2),
'tac_avg_4k': round(avg_4k, 2),
'tac_avg_5k': round(avg_5k, 2),
'tac_multikill_rate': round(multikill_rate, 3),
'tac_ace_count': total_5k,
}
@staticmethod
def _calculate_clutch(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Clutch Performance (10 columns)
Columns:
- tac_clutch_1v1_attempts, tac_clutch_1v1_wins, tac_clutch_1v1_rate
- tac_clutch_1v2_attempts, tac_clutch_1v2_wins, tac_clutch_1v2_rate
- tac_clutch_1v3_plus_attempts, tac_clutch_1v3_plus_wins, tac_clutch_1v3_plus_rate
- tac_clutch_impact_score
Logic:
- Wins: Aggregated directly from fact_match_players (trusting upstream data).
- Attempts: Calculated by replaying rounds with 'Active Player' filtering to remove ghosts.
"""
cursor = conn_l2.cursor()
# Step 1: Get Wins from fact_match_players
cursor.execute("""
SELECT
SUM(clutch_1v1) as c1,
SUM(clutch_1v2) as c2,
SUM(clutch_1v3) as c3,
SUM(clutch_1v4) as c4,
SUM(clutch_1v5) as c5
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
wins_row = cursor.fetchone()
clutch_1v1_wins = wins_row[0] if wins_row and wins_row[0] else 0
clutch_1v2_wins = wins_row[1] if wins_row and wins_row[1] else 0
clutch_1v3_wins = wins_row[2] if wins_row and wins_row[2] else 0
clutch_1v4_wins = wins_row[3] if wins_row and wins_row[3] else 0
clutch_1v5_wins = wins_row[4] if wins_row and wins_row[4] else 0
# Group 1v3+ wins
clutch_1v3_plus_wins = clutch_1v3_wins + clutch_1v4_wins + clutch_1v5_wins
# Step 2: Calculate Attempts
cursor.execute("SELECT DISTINCT match_id FROM fact_match_players WHERE steam_id_64 = ?", (steam_id,))
match_ids = [row[0] for row in cursor.fetchall()]
clutch_1v1_attempts = 0
clutch_1v2_attempts = 0
clutch_1v3_plus_attempts = 0
for match_id in match_ids:
# Get Roster
cursor.execute("SELECT steam_id_64, team_id FROM fact_match_players WHERE match_id = ?", (match_id,))
roster = cursor.fetchall()
my_team_id = None
for pid, tid in roster:
if str(pid) == str(steam_id):
my_team_id = tid
break
if my_team_id is None:
continue
all_teammates = {str(pid) for pid, tid in roster if tid == my_team_id}
all_enemies = {str(pid) for pid, tid in roster if tid != my_team_id}
# Get Events for this match
cursor.execute("""
SELECT round_num, event_type, attacker_steam_id, victim_steam_id, event_time
FROM fact_round_events
WHERE match_id = ?
ORDER BY round_num, event_time
""", (match_id,))
all_events = cursor.fetchall()
# Group events by round
from collections import defaultdict
events_by_round = defaultdict(list)
active_players_by_round = defaultdict(set)
for r_num, e_type, attacker, victim, e_time in all_events:
events_by_round[r_num].append((e_type, attacker, victim))
if attacker: active_players_by_round[r_num].add(str(attacker))
if victim: active_players_by_round[r_num].add(str(victim))
# Iterate rounds
for r_num, round_events in events_by_round.items():
active_players = active_players_by_round[r_num]
# If player not active, skip (probably camping or AFK or not spawned)
if str(steam_id) not in active_players:
continue
# Filter roster to active players only (removes ghosts)
alive_teammates = all_teammates.intersection(active_players)
alive_enemies = all_enemies.intersection(active_players)
# Safety: ensure player is in alive_teammates
alive_teammates.add(str(steam_id))
clutch_detected = False
for e_type, attacker, victim in round_events:
if e_type == 'kill':
vic_str = str(victim)
if vic_str in alive_teammates:
alive_teammates.discard(vic_str)
elif vic_str in alive_enemies:
alive_enemies.discard(vic_str)
# Check clutch condition
if not clutch_detected:
# Teammates dead (len==1 means only me), Enemies alive
if len(alive_teammates) == 1 and str(steam_id) in alive_teammates:
enemies_cnt = len(alive_enemies)
if enemies_cnt > 0:
clutch_detected = True
if enemies_cnt == 1:
clutch_1v1_attempts += 1
elif enemies_cnt == 2:
clutch_1v2_attempts += 1
elif enemies_cnt >= 3:
clutch_1v3_plus_attempts += 1
# Calculate win rates
rate_1v1 = SafeAggregator.safe_divide(clutch_1v1_wins, clutch_1v1_attempts)
rate_1v2 = SafeAggregator.safe_divide(clutch_1v2_wins, clutch_1v2_attempts)
rate_1v3_plus = SafeAggregator.safe_divide(clutch_1v3_plus_wins, clutch_1v3_plus_attempts)
# Clutch impact score: weighted by difficulty
impact_score = (clutch_1v1_wins * 1.0 + clutch_1v2_wins * 3.0 + clutch_1v3_plus_wins * 7.0)
return {
'tac_clutch_1v1_attempts': clutch_1v1_attempts,
'tac_clutch_1v1_wins': clutch_1v1_wins,
'tac_clutch_1v1_rate': round(rate_1v1, 3),
'tac_clutch_1v2_attempts': clutch_1v2_attempts,
'tac_clutch_1v2_wins': clutch_1v2_wins,
'tac_clutch_1v2_rate': round(rate_1v2, 3),
'tac_clutch_1v3_plus_attempts': clutch_1v3_plus_attempts,
'tac_clutch_1v3_plus_wins': clutch_1v3_plus_wins,
'tac_clutch_1v3_plus_rate': round(rate_1v3_plus, 3),
'tac_clutch_impact_score': round(impact_score, 2)
}
@staticmethod
def _calculate_utility(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Utility Mastery (12 columns)
Columns:
- tac_util_flash_per_round, tac_util_smoke_per_round
- tac_util_molotov_per_round, tac_util_he_per_round
- tac_util_usage_rate
- tac_util_nade_dmg_per_round, tac_util_nade_dmg_per_nade
- tac_util_flash_time_per_round, tac_util_flash_enemies_per_round
- tac_util_flash_efficiency
- tac_util_smoke_timing_score
- tac_util_impact_score
Note: Requires fact_round_player_economy for detailed utility stats
"""
cursor = conn_l2.cursor()
# Check if economy table exists (leetify mode)
cursor.execute("""
SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='fact_round_player_economy'
""")
has_economy = cursor.fetchone()[0] > 0
if not has_economy:
# Return zeros if no economy data
return {
'tac_util_flash_per_round': 0.0,
'tac_util_smoke_per_round': 0.0,
'tac_util_molotov_per_round': 0.0,
'tac_util_he_per_round': 0.0,
'tac_util_usage_rate': 0.0,
'tac_util_nade_dmg_per_round': 0.0,
'tac_util_nade_dmg_per_nade': 0.0,
'tac_util_flash_time_per_round': 0.0,
'tac_util_flash_enemies_per_round': 0.0,
'tac_util_flash_efficiency': 0.0,
'tac_util_smoke_timing_score': 0.0,
'tac_util_impact_score': 0.0,
}
# Get total rounds for per-round calculations
total_rounds = BaseFeatureProcessor.get_player_round_count(steam_id, conn_l2)
if total_rounds == 0:
total_rounds = 1
# Utility usage from fact_match_players
cursor.execute("""
SELECT
SUM(util_flash_usage) as total_flash,
SUM(util_smoke_usage) as total_smoke,
SUM(util_molotov_usage) as total_molotov,
SUM(util_he_usage) as total_he,
SUM(flash_enemy) as enemies_flashed,
SUM(damage_total) as total_damage,
SUM(throw_harm_enemy) as nade_damage,
COUNT(*) as matches
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
total_flash = row[0] if row[0] else 0
total_smoke = row[1] if row[1] else 0
total_molotov = row[2] if row[2] else 0
total_he = row[3] if row[3] else 0
enemies_flashed = row[4] if row[4] else 0
total_damage = row[5] if row[5] else 0
nade_damage = row[6] if row[6] else 0
rounds_with_data = row[7] if row[7] else 1
total_nades = total_flash + total_smoke + total_molotov + total_he
flash_per_round = total_flash / total_rounds
smoke_per_round = total_smoke / total_rounds
molotov_per_round = total_molotov / total_rounds
he_per_round = total_he / total_rounds
usage_rate = total_nades / total_rounds
# Nade damage (HE grenade + molotov damage from throw_harm_enemy)
nade_dmg_per_round = SafeAggregator.safe_divide(nade_damage, total_rounds)
nade_dmg_per_nade = SafeAggregator.safe_divide(nade_damage, total_he + total_molotov)
# Flash efficiency (simplified - kills per flash from match data)
# DEPRECATED: Replaced by Enemies Blinded per Flash logic below
# cursor.execute("""
# SELECT SUM(kills) as total_kills
# FROM fact_match_players
# WHERE steam_id_64 = ?
# """, (steam_id,))
#
# total_kills = cursor.fetchone()[0]
# total_kills = total_kills if total_kills else 0
# flash_efficiency = SafeAggregator.safe_divide(total_kills, total_flash)
# Real flash data from fact_match_players
# flash_time in L2 is TOTAL flash time (seconds), not average
# flash_enemy is TOTAL enemies flashed
cursor.execute("""
SELECT
SUM(flash_time) as total_flash_time,
SUM(flash_enemy) as total_enemies_flashed,
SUM(util_flash_usage) as total_flashes_thrown
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
flash_row = cursor.fetchone()
total_flash_time = flash_row[0] if flash_row and flash_row[0] else 0.0
total_enemies_flashed = flash_row[1] if flash_row and flash_row[1] else 0
total_flashes_thrown = flash_row[2] if flash_row and flash_row[2] else 0
flash_time_per_round = total_flash_time / total_rounds if total_rounds > 0 else 0.0
flash_enemies_per_round = total_enemies_flashed / total_rounds if total_rounds > 0 else 0.0
# Flash Efficiency: Enemies Blinded per Flash Thrown (instead of kills per flash)
# 100% means 1 enemy blinded per flash
# 200% means 2 enemies blinded per flash (very good)
flash_efficiency = SafeAggregator.safe_divide(total_enemies_flashed, total_flashes_thrown)
# Smoke timing score CANNOT be calculated without bomb plant event timestamps
# Would require: SELECT event_time FROM fact_round_events WHERE event_type = 'bomb_plant'
# Then correlate with util_smoke_usage timing - currently no timing data for utility usage
# Commenting out: tac_util_smoke_timing_score
smoke_timing_score = 0.0
# Taser Kills Logic (Zeus)
# We want Attempts (shots fired) vs Kills
# User requested to track "Equipped Count" instead of "Attempts" (shots)
# because event logs often miss weapon_fire for taser.
# We check fact_round_player_economy for has_zeus = 1
zeus_equipped_count = 0
if has_economy:
cursor.execute("""
SELECT COUNT(*)
FROM fact_round_player_economy
WHERE steam_id_64 = ? AND has_zeus = 1
""", (steam_id,))
zeus_equipped_count = cursor.fetchone()[0] or 0
# Kills still come from event logs
# Removed tac_util_zeus_kills per user request (data not available)
# cursor.execute("""
# SELECT
# COUNT(CASE WHEN event_type = 'kill' AND weapon = 'taser' THEN 1 END) as kills
# FROM fact_round_events
# WHERE attacker_steam_id = ?
# """, (steam_id,))
# zeus_kills = cursor.fetchone()[0] or 0
# Fallback: if equipped count < kills (shouldn't happen if economy data is good), fix it
# if zeus_equipped_count < zeus_kills:
# zeus_equipped_count = zeus_kills
# Utility impact score (composite)
impact_score = (
nade_dmg_per_round * 0.3 +
flash_efficiency * 2.0 +
usage_rate * 10.0
)
return {
'tac_util_flash_per_round': round(flash_per_round, 2),
'tac_util_smoke_per_round': round(smoke_per_round, 2),
'tac_util_molotov_per_round': round(molotov_per_round, 2),
'tac_util_he_per_round': round(he_per_round, 2),
'tac_util_usage_rate': round(usage_rate, 2),
'tac_util_nade_dmg_per_round': round(nade_dmg_per_round, 2),
'tac_util_nade_dmg_per_nade': round(nade_dmg_per_nade, 2),
'tac_util_flash_time_per_round': round(flash_time_per_round, 2),
'tac_util_flash_enemies_per_round': round(flash_enemies_per_round, 2),
'tac_util_flash_efficiency': round(flash_efficiency, 3),
#'tac_util_smoke_timing_score': round(smoke_timing_score, 2), # Removed per user request
'tac_util_impact_score': round(impact_score, 2),
'tac_util_zeus_equipped_count': zeus_equipped_count,
#'tac_util_zeus_kills': zeus_kills, # Removed
}
@staticmethod
def _calculate_economy(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Economy Efficiency (8 columns)
Columns:
- tac_eco_dmg_per_1k
- tac_eco_kpr_eco_rounds, tac_eco_kd_eco_rounds
- tac_eco_kpr_force_rounds, tac_eco_kpr_full_rounds
- tac_eco_save_discipline
- tac_eco_force_success_rate
- tac_eco_efficiency_score
Note: Requires fact_round_player_economy for equipment values
"""
cursor = conn_l2.cursor()
# Check if economy table exists
cursor.execute("""
SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='fact_round_player_economy'
""")
has_economy = cursor.fetchone()[0] > 0
if not has_economy:
# Return zeros if no economy data
return {
'tac_eco_dmg_per_1k': 0.0,
'tac_eco_kpr_eco_rounds': 0.0,
'tac_eco_kd_eco_rounds': 0.0,
'tac_eco_kpr_force_rounds': 0.0,
'tac_eco_kpr_full_rounds': 0.0,
'tac_eco_save_discipline': 0.0,
'tac_eco_force_success_rate': 0.0,
'tac_eco_efficiency_score': 0.0,
}
# REAL economy-based performance from round-level data
# Join fact_round_player_economy with fact_round_events to get kills/deaths per economy state
# Fallback if no economy table but we want basic DMG/1k approximation from total damage / assumed average buy
# But avg_equip_value is from economy table.
# If no economy table, we can't do this accurately.
# However, user says "Eco Dmg/1k" is 0.00.
# If we have NO economy table, we returned early above.
# If we reached here, we HAVE economy table (or at least check passed).
# Let's check logic.
# Get average equipment value
cursor.execute("""
SELECT AVG(equipment_value)
FROM fact_round_player_economy
WHERE steam_id_64 = ?
AND equipment_value IS NOT NULL
AND equipment_value > 0 -- Filter out zero equipment value rounds? Or include them?
""", (steam_id,))
avg_equip_val_res = cursor.fetchone()
avg_equip_value = avg_equip_val_res[0] if avg_equip_val_res and avg_equip_val_res[0] else 4000.0
# Avoid division by zero if avg_equip_value is somehow 0
if avg_equip_value < 100: avg_equip_value = 4000.0
# Get total damage and calculate dmg per $1000
cursor.execute("""
SELECT SUM(damage_total), SUM(round_total)
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
damage_row = cursor.fetchone()
total_damage = damage_row[0] if damage_row[0] else 0
total_rounds = damage_row[1] if damage_row[1] else 1
avg_dmg_per_round = SafeAggregator.safe_divide(total_damage, total_rounds)
# Formula: (ADR) / (AvgSpend / 1000)
# e.g. 80 ADR / (4000 / 1000) = 80 / 4 = 20 dmg/$1k
dmg_per_1k = SafeAggregator.safe_divide(avg_dmg_per_round, (avg_equip_value / 1000.0))
# ECO rounds: equipment_value < 2000
cursor.execute("""
SELECT
e.match_id,
e.round_num,
e.steam_id_64,
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.attacker_steam_id = e.steam_id_64 THEN 1 END) as kills,
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.victim_steam_id = e.steam_id_64 THEN 1 END) as deaths
FROM fact_round_player_economy e
LEFT JOIN fact_round_events fre ON e.match_id = fre.match_id AND e.round_num = fre.round_num
WHERE e.steam_id_64 = ?
AND e.equipment_value < 2000
GROUP BY e.match_id, e.round_num, e.steam_id_64
""", (steam_id,))
eco_rounds = cursor.fetchall()
eco_kills = sum(row[3] for row in eco_rounds)
eco_deaths = sum(row[4] for row in eco_rounds)
eco_round_count = len(eco_rounds)
kpr_eco = SafeAggregator.safe_divide(eco_kills, eco_round_count)
kd_eco = SafeAggregator.safe_divide(eco_kills, eco_deaths)
# FORCE rounds: 2000 <= equipment_value < 3500
cursor.execute("""
SELECT
e.match_id,
e.round_num,
e.steam_id_64,
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.attacker_steam_id = e.steam_id_64 THEN 1 END) as kills,
fr.winner_side,
e.side
FROM fact_round_player_economy e
LEFT JOIN fact_round_events fre ON e.match_id = fre.match_id AND e.round_num = fre.round_num
LEFT JOIN fact_rounds fr ON e.match_id = fr.match_id AND e.round_num = fr.round_num
WHERE e.steam_id_64 = ?
AND e.equipment_value >= 2000
AND e.equipment_value < 3500
GROUP BY e.match_id, e.round_num, e.steam_id_64, fr.winner_side, e.side
""", (steam_id,))
force_rounds = cursor.fetchall()
force_kills = sum(row[3] for row in force_rounds)
force_round_count = len(force_rounds)
force_wins = sum(1 for row in force_rounds if row[4] == row[5]) # winner_side == player_side
kpr_force = SafeAggregator.safe_divide(force_kills, force_round_count)
force_success = SafeAggregator.safe_divide(force_wins, force_round_count)
# FULL BUY rounds: equipment_value >= 3500
cursor.execute("""
SELECT
e.match_id,
e.round_num,
e.steam_id_64,
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.attacker_steam_id = e.steam_id_64 THEN 1 END) as kills
FROM fact_round_player_economy e
LEFT JOIN fact_round_events fre ON e.match_id = fre.match_id AND e.round_num = fre.round_num
WHERE e.steam_id_64 = ?
AND e.equipment_value >= 3500
GROUP BY e.match_id, e.round_num, e.steam_id_64
""", (steam_id,))
full_rounds = cursor.fetchall()
full_kills = sum(row[3] for row in full_rounds)
full_round_count = len(full_rounds)
kpr_full = SafeAggregator.safe_divide(full_kills, full_round_count)
# Save discipline: ratio of eco rounds to total rounds (lower is better discipline)
save_discipline = 1.0 - SafeAggregator.safe_divide(eco_round_count, total_rounds)
# Efficiency score: weighted KPR across economy states
efficiency_score = (kpr_eco * 1.5 + kpr_force * 1.2 + kpr_full * 1.0) / 3.7
return {
'tac_eco_dmg_per_1k': round(dmg_per_1k, 2),
'tac_eco_kpr_eco_rounds': round(kpr_eco, 3),
'tac_eco_kd_eco_rounds': round(kd_eco, 3),
'tac_eco_kpr_force_rounds': round(kpr_force, 3),
'tac_eco_kpr_full_rounds': round(kpr_full, 3),
'tac_eco_save_discipline': round(save_discipline, 3),
'tac_eco_force_success_rate': round(force_success, 3),
'tac_eco_efficiency_score': round(efficiency_score, 2),
}
def _get_default_tactical_features() -> Dict[str, Any]:
"""Return default zero values for all 44 TACTICAL features"""
return {
# Opening Impact (8)
'tac_avg_fk': 0.0,
'tac_avg_fd': 0.0,
'tac_fk_rate': 0.0,
'tac_fd_rate': 0.0,
'tac_fk_success_rate': 0.0,
'tac_entry_kill_rate': 0.0,
'tac_entry_death_rate': 0.0,
'tac_opening_duel_winrate': 0.0,
# Multi-Kill (6)
'tac_avg_2k': 0.0,
'tac_avg_3k': 0.0,
'tac_avg_4k': 0.0,
'tac_avg_5k': 0.0,
'tac_multikill_rate': 0.0,
'tac_ace_count': 0,
# Clutch Performance (10)
'tac_clutch_1v1_attempts': 0,
'tac_clutch_1v1_wins': 0,
'tac_clutch_1v1_rate': 0.0,
'tac_clutch_1v2_attempts': 0,
'tac_clutch_1v2_wins': 0,
'tac_clutch_1v2_rate': 0.0,
'tac_clutch_1v3_plus_attempts': 0,
'tac_clutch_1v3_plus_wins': 0,
'tac_clutch_1v3_plus_rate': 0.0,
'tac_clutch_impact_score': 0.0,
# Utility Mastery (12)
'tac_util_flash_per_round': 0.0,
'tac_util_smoke_per_round': 0.0,
'tac_util_molotov_per_round': 0.0,
'tac_util_he_per_round': 0.0,
'tac_util_usage_rate': 0.0,
'tac_util_nade_dmg_per_round': 0.0,
'tac_util_nade_dmg_per_nade': 0.0,
'tac_util_flash_time_per_round': 0.0,
'tac_util_flash_enemies_per_round': 0.0,
'tac_util_flash_efficiency': 0.0,
# 'tac_util_smoke_timing_score': 0.0, # Removed
'tac_util_impact_score': 0.0,
'tac_util_zeus_equipped_count': 0,
# 'tac_util_zeus_kills': 0, # Removed
# Economy Efficiency (8)
'tac_eco_dmg_per_1k': 0.0,
'tac_eco_kpr_eco_rounds': 0.0,
'tac_eco_kd_eco_rounds': 0.0,
'tac_eco_kpr_force_rounds': 0.0,
'tac_eco_kpr_full_rounds': 0.0,
'tac_eco_save_discipline': 0.0,
'tac_eco_force_success_rate': 0.0,
'tac_eco_efficiency_score': 0.0,
}
+394
View File
@@ -0,0 +1,394 @@
-- ============================================================================
-- L3 Schema: Player Features Data Mart (Version 2.0)
-- ============================================================================
-- Based on: L3_ARCHITECTURE_PLAN.md
-- Design: 5-Tier Feature Hierarchy (CORE → TACTICAL → INTELLIGENCE → META → COMPOSITE)
-- Granularity: One row per player (Aggregated Profile)
-- Total Columns: 207 features + 6 metadata = 213 columns
-- ============================================================================
-- ============================================================================
-- Main Table: dm_player_features
-- ============================================================================
CREATE TABLE IF NOT EXISTS dm_player_features (
-- ========================================================================
-- Metadata (6 columns)
-- ========================================================================
steam_id_64 TEXT PRIMARY KEY,
total_matches INTEGER NOT NULL DEFAULT 0,
total_rounds INTEGER NOT NULL DEFAULT 0,
first_match_date INTEGER, -- Unix timestamp
last_match_date INTEGER, -- Unix timestamp
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- ========================================================================
-- TIER 1: CORE (41 columns)
-- Direct aggregations from fact_match_players
-- ========================================================================
-- Basic Performance (15 columns)
core_avg_rating REAL DEFAULT 0.0,
core_avg_rating2 REAL DEFAULT 0.0,
core_avg_kd REAL DEFAULT 0.0,
core_avg_adr REAL DEFAULT 0.0,
core_avg_kast REAL DEFAULT 0.0,
core_avg_rws REAL DEFAULT 0.0,
core_avg_hs_kills REAL DEFAULT 0.0,
core_hs_rate REAL DEFAULT 0.0, -- hs/total_kills
core_total_kills INTEGER DEFAULT 0,
core_total_deaths INTEGER DEFAULT 0,
core_total_assists INTEGER DEFAULT 0,
core_avg_assists REAL DEFAULT 0.0,
core_kpr REAL DEFAULT 0.0, -- kills per round
core_dpr REAL DEFAULT 0.0, -- deaths per round
core_survival_rate REAL DEFAULT 0.0,
-- Match Stats (8 columns)
core_win_rate REAL DEFAULT 0.0,
core_wins INTEGER DEFAULT 0,
core_losses INTEGER DEFAULT 0,
core_avg_match_duration INTEGER DEFAULT 0, -- seconds
core_avg_mvps REAL DEFAULT 0.0,
core_mvp_rate REAL DEFAULT 0.0,
core_avg_elo_change REAL DEFAULT 0.0,
core_total_elo_gained REAL DEFAULT 0.0,
-- Weapon Stats (12 columns)
core_avg_awp_kills REAL DEFAULT 0.0,
core_awp_usage_rate REAL DEFAULT 0.0,
core_avg_knife_kills REAL DEFAULT 0.0,
core_avg_zeus_kills REAL DEFAULT 0.0,
core_zeus_buy_rate REAL DEFAULT 0.0,
core_top_weapon TEXT,
core_top_weapon_kills INTEGER DEFAULT 0,
core_top_weapon_hs_rate REAL DEFAULT 0.0,
core_weapon_diversity REAL DEFAULT 0.0,
core_rifle_hs_rate REAL DEFAULT 0.0,
core_pistol_hs_rate REAL DEFAULT 0.0,
core_smg_kills_total INTEGER DEFAULT 0,
-- Objective Stats (6 columns)
core_avg_plants REAL DEFAULT 0.0,
core_avg_defuses REAL DEFAULT 0.0,
core_avg_flash_assists REAL DEFAULT 0.0,
core_plant_success_rate REAL DEFAULT 0.0,
core_defuse_success_rate REAL DEFAULT 0.0,
core_objective_impact REAL DEFAULT 0.0,
-- ========================================================================
-- TIER 2: TACTICAL (44 columns)
-- Multi-table JOINs, conditional aggregations
-- ========================================================================
-- Opening Impact (8 columns)
tac_avg_fk REAL DEFAULT 0.0,
tac_avg_fd REAL DEFAULT 0.0,
tac_fk_rate REAL DEFAULT 0.0,
tac_fd_rate REAL DEFAULT 0.0,
tac_fk_success_rate REAL DEFAULT 0.0,
tac_entry_kill_rate REAL DEFAULT 0.0,
tac_entry_death_rate REAL DEFAULT 0.0,
tac_opening_duel_winrate REAL DEFAULT 0.0,
-- Multi-Kill (6 columns)
tac_avg_2k REAL DEFAULT 0.0,
tac_avg_3k REAL DEFAULT 0.0,
tac_avg_4k REAL DEFAULT 0.0,
tac_avg_5k REAL DEFAULT 0.0,
tac_multikill_rate REAL DEFAULT 0.0,
tac_ace_count INTEGER DEFAULT 0,
-- Clutch Performance (10 columns)
tac_clutch_1v1_attempts INTEGER DEFAULT 0,
tac_clutch_1v1_wins INTEGER DEFAULT 0,
tac_clutch_1v1_rate REAL DEFAULT 0.0,
tac_clutch_1v2_attempts INTEGER DEFAULT 0,
tac_clutch_1v2_wins INTEGER DEFAULT 0,
tac_clutch_1v2_rate REAL DEFAULT 0.0,
tac_clutch_1v3_plus_attempts INTEGER DEFAULT 0,
tac_clutch_1v3_plus_wins INTEGER DEFAULT 0,
tac_clutch_1v3_plus_rate REAL DEFAULT 0.0,
tac_clutch_impact_score REAL DEFAULT 0.0,
-- Utility Mastery (13 columns)
tac_util_flash_per_round REAL DEFAULT 0.0,
tac_util_smoke_per_round REAL DEFAULT 0.0,
tac_util_molotov_per_round REAL DEFAULT 0.0,
tac_util_he_per_round REAL DEFAULT 0.0,
tac_util_usage_rate REAL DEFAULT 0.0,
tac_util_nade_dmg_per_round REAL DEFAULT 0.0,
tac_util_nade_dmg_per_nade REAL DEFAULT 0.0,
tac_util_flash_time_per_round REAL DEFAULT 0.0,
tac_util_flash_enemies_per_round REAL DEFAULT 0.0,
tac_util_flash_efficiency REAL DEFAULT 0.0,
tac_util_impact_score REAL DEFAULT 0.0,
tac_util_zeus_equipped_count INTEGER DEFAULT 0,
-- tac_util_zeus_kills REMOVED
-- Economy Efficiency (8 columns)
tac_eco_dmg_per_1k REAL DEFAULT 0.0,
tac_eco_kpr_eco_rounds REAL DEFAULT 0.0,
tac_eco_kd_eco_rounds REAL DEFAULT 0.0,
tac_eco_kpr_force_rounds REAL DEFAULT 0.0,
tac_eco_kpr_full_rounds REAL DEFAULT 0.0,
tac_eco_save_discipline REAL DEFAULT 0.0,
tac_eco_force_success_rate REAL DEFAULT 0.0,
tac_eco_efficiency_score REAL DEFAULT 0.0,
-- ========================================================================
-- TIER 3: INTELLIGENCE (53 columns)
-- Advanced analytics on fact_round_events
-- ========================================================================
-- High IQ Kills (9 columns)
int_wallbang_kills INTEGER DEFAULT 0,
int_wallbang_rate REAL DEFAULT 0.0,
int_smoke_kills INTEGER DEFAULT 0,
int_smoke_kill_rate REAL DEFAULT 0.0,
int_blind_kills INTEGER DEFAULT 0,
int_blind_kill_rate REAL DEFAULT 0.0,
int_noscope_kills INTEGER DEFAULT 0,
int_noscope_rate REAL DEFAULT 0.0,
int_high_iq_score REAL DEFAULT 0.0,
-- Timing Analysis (12 columns)
int_timing_early_kills INTEGER DEFAULT 0,
int_timing_mid_kills INTEGER DEFAULT 0,
int_timing_late_kills INTEGER DEFAULT 0,
int_timing_early_kill_share REAL DEFAULT 0.0,
int_timing_mid_kill_share REAL DEFAULT 0.0,
int_timing_late_kill_share REAL DEFAULT 0.0,
int_timing_avg_kill_time REAL DEFAULT 0.0,
int_timing_early_deaths INTEGER DEFAULT 0,
int_timing_early_death_rate REAL DEFAULT 0.0,
int_timing_aggression_index REAL DEFAULT 0.0,
int_timing_patience_score REAL DEFAULT 0.0,
int_timing_first_contact_time REAL DEFAULT 0.0,
-- Pressure Performance (9 columns)
int_pressure_comeback_kd REAL DEFAULT 0.0,
int_pressure_comeback_rating REAL DEFAULT 0.0,
int_pressure_losing_streak_kd REAL DEFAULT 0.0,
int_pressure_matchpoint_kpr REAL DEFAULT 0.0,
int_pressure_clutch_composure REAL DEFAULT 0.0,
int_pressure_entry_in_loss REAL DEFAULT 0.0,
int_pressure_performance_index REAL DEFAULT 0.0,
int_pressure_big_moment_score REAL DEFAULT 0.0,
int_pressure_tilt_resistance REAL DEFAULT 0.0,
-- Position Mastery (14 columns)
int_pos_site_a_control_rate REAL DEFAULT 0.0,
int_pos_site_b_control_rate REAL DEFAULT 0.0,
int_pos_mid_control_rate REAL DEFAULT 0.0,
int_pos_favorite_position TEXT,
int_pos_position_diversity REAL DEFAULT 0.0,
int_pos_rotation_speed REAL DEFAULT 0.0,
int_pos_map_coverage REAL DEFAULT 0.0,
int_pos_lurk_tendency REAL DEFAULT 0.0,
int_pos_site_anchor_score REAL DEFAULT 0.0,
int_pos_entry_route_diversity REAL DEFAULT 0.0,
int_pos_retake_positioning REAL DEFAULT 0.0,
int_pos_postplant_positioning REAL DEFAULT 0.0,
int_pos_spatial_iq_score REAL DEFAULT 0.0,
int_pos_avg_distance_from_teammates REAL DEFAULT 0.0,
-- Trade Network (8 columns)
int_trade_kill_count INTEGER DEFAULT 0,
int_trade_kill_rate REAL DEFAULT 0.0,
int_trade_response_time REAL DEFAULT 0.0,
int_trade_given_count INTEGER DEFAULT 0,
int_trade_given_rate REAL DEFAULT 0.0,
int_trade_balance REAL DEFAULT 0.0,
int_trade_efficiency REAL DEFAULT 0.0,
int_teamwork_score REAL DEFAULT 0.0,
-- ========================================================================
-- TIER 4: META (52 columns)
-- Long-term patterns and meta-features
-- ========================================================================
-- Stability (8 columns)
meta_rating_volatility REAL DEFAULT 0.0,
meta_recent_form_rating REAL DEFAULT 0.0,
meta_win_rating REAL DEFAULT 0.0,
meta_loss_rating REAL DEFAULT 0.0,
meta_rating_consistency REAL DEFAULT 0.0,
meta_time_rating_correlation REAL DEFAULT 0.0,
meta_map_stability REAL DEFAULT 0.0,
meta_elo_tier_stability REAL DEFAULT 0.0,
-- Side Preference (14 columns)
meta_side_ct_rating REAL DEFAULT 0.0,
meta_side_t_rating REAL DEFAULT 0.0,
meta_side_ct_kd REAL DEFAULT 0.0,
meta_side_t_kd REAL DEFAULT 0.0,
meta_side_ct_win_rate REAL DEFAULT 0.0,
meta_side_t_win_rate REAL DEFAULT 0.0,
meta_side_ct_fk_rate REAL DEFAULT 0.0,
meta_side_t_fk_rate REAL DEFAULT 0.0,
meta_side_ct_kast REAL DEFAULT 0.0,
meta_side_t_kast REAL DEFAULT 0.0,
meta_side_rating_diff REAL DEFAULT 0.0,
meta_side_kd_diff REAL DEFAULT 0.0,
meta_side_preference TEXT,
meta_side_balance_score REAL DEFAULT 0.0,
-- Opponent Adaptation (12 columns)
meta_opp_vs_lower_elo_rating REAL DEFAULT 0.0,
meta_opp_vs_similar_elo_rating REAL DEFAULT 0.0,
meta_opp_vs_higher_elo_rating REAL DEFAULT 0.0,
meta_opp_vs_lower_elo_kd REAL DEFAULT 0.0,
meta_opp_vs_similar_elo_kd REAL DEFAULT 0.0,
meta_opp_vs_higher_elo_kd REAL DEFAULT 0.0,
meta_opp_elo_adaptation REAL DEFAULT 0.0,
meta_opp_stomping_score REAL DEFAULT 0.0,
meta_opp_upset_score REAL DEFAULT 0.0,
meta_opp_consistency_across_elos REAL DEFAULT 0.0,
meta_opp_rank_resistance REAL DEFAULT 0.0,
meta_opp_smurf_detection REAL DEFAULT 0.0,
-- Map Specialization (10 columns)
meta_map_best_map TEXT,
meta_map_best_rating REAL DEFAULT 0.0,
meta_map_worst_map TEXT,
meta_map_worst_rating REAL DEFAULT 0.0,
meta_map_diversity REAL DEFAULT 0.0,
meta_map_pool_size INTEGER DEFAULT 0,
meta_map_specialist_score REAL DEFAULT 0.0,
meta_map_versatility REAL DEFAULT 0.0,
meta_map_comfort_zone_rate REAL DEFAULT 0.0,
meta_map_adaptation REAL DEFAULT 0.0,
-- Session Pattern (8 columns)
meta_session_avg_matches_per_day REAL DEFAULT 0.0,
meta_session_longest_streak INTEGER DEFAULT 0,
meta_session_weekend_rating REAL DEFAULT 0.0,
meta_session_weekday_rating REAL DEFAULT 0.0,
meta_session_morning_rating REAL DEFAULT 0.0,
meta_session_afternoon_rating REAL DEFAULT 0.0,
meta_session_evening_rating REAL DEFAULT 0.0,
meta_session_night_rating REAL DEFAULT 0.0,
-- ========================================================================
-- TIER 5: COMPOSITE (11 columns)
-- Weighted composite scores (0-100)
-- ========================================================================
score_aim REAL DEFAULT 0.0,
score_clutch REAL DEFAULT 0.0,
score_pistol REAL DEFAULT 0.0,
score_defense REAL DEFAULT 0.0,
score_utility REAL DEFAULT 0.0,
score_stability REAL DEFAULT 0.0,
score_economy REAL DEFAULT 0.0,
score_pace REAL DEFAULT 0.0,
score_overall REAL DEFAULT 0.0,
tier_classification TEXT,
tier_percentile REAL DEFAULT 0.0,
-- Foreign key constraint
FOREIGN KEY (steam_id_64) REFERENCES dim_players(steam_id_64)
);
-- Indexes for query performance
CREATE INDEX IF NOT EXISTS idx_dm_player_features_rating ON dm_player_features(core_avg_rating DESC);
CREATE INDEX IF NOT EXISTS idx_dm_player_features_matches ON dm_player_features(total_matches DESC);
CREATE INDEX IF NOT EXISTS idx_dm_player_features_tier ON dm_player_features(tier_classification);
CREATE INDEX IF NOT EXISTS idx_dm_player_features_updated ON dm_player_features(last_updated DESC);
-- ============================================================================
-- Auxiliary Table: dm_player_match_history
-- ============================================================================
CREATE TABLE IF NOT EXISTS dm_player_match_history (
steam_id_64 TEXT,
match_id TEXT,
match_date INTEGER, -- Unix timestamp
match_sequence INTEGER, -- Player's N-th match
-- Core performance snapshot
rating REAL,
kd_ratio REAL,
adr REAL,
kast REAL,
is_win BOOLEAN,
-- Match context
map_name TEXT,
opponent_avg_elo REAL,
teammate_avg_rating REAL,
-- Cumulative stats
cumulative_rating REAL,
rolling_10_rating REAL,
PRIMARY KEY (steam_id_64, match_id),
FOREIGN KEY (steam_id_64) REFERENCES dm_player_features(steam_id_64) ON DELETE CASCADE,
FOREIGN KEY (match_id) REFERENCES fact_matches(match_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_player_history_player_date ON dm_player_match_history(steam_id_64, match_date DESC);
CREATE INDEX IF NOT EXISTS idx_player_history_match ON dm_player_match_history(match_id);
-- ============================================================================
-- Auxiliary Table: dm_player_map_stats
-- ============================================================================
CREATE TABLE IF NOT EXISTS dm_player_map_stats (
steam_id_64 TEXT,
map_name TEXT,
matches INTEGER DEFAULT 0,
wins INTEGER DEFAULT 0,
win_rate REAL DEFAULT 0.0,
avg_rating REAL DEFAULT 0.0,
avg_kd REAL DEFAULT 0.0,
avg_adr REAL DEFAULT 0.0,
avg_kast REAL DEFAULT 0.0,
best_rating REAL DEFAULT 0.0,
worst_rating REAL DEFAULT 0.0,
PRIMARY KEY (steam_id_64, map_name),
FOREIGN KEY (steam_id_64) REFERENCES dm_player_features(steam_id_64) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_player_map_stats_player ON dm_player_map_stats(steam_id_64);
CREATE INDEX IF NOT EXISTS idx_player_map_stats_map ON dm_player_map_stats(map_name);
-- ============================================================================
-- Auxiliary Table: dm_player_weapon_stats
-- ============================================================================
CREATE TABLE IF NOT EXISTS dm_player_weapon_stats (
steam_id_64 TEXT,
weapon_name TEXT,
total_kills INTEGER DEFAULT 0,
total_headshots INTEGER DEFAULT 0,
hs_rate REAL DEFAULT 0.0,
usage_rounds INTEGER DEFAULT 0,
usage_rate REAL DEFAULT 0.0,
avg_kills_per_round REAL DEFAULT 0.0,
effectiveness_score REAL DEFAULT 0.0,
PRIMARY KEY (steam_id_64, weapon_name),
FOREIGN KEY (steam_id_64) REFERENCES dm_player_features(steam_id_64) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_player_weapon_stats_player ON dm_player_weapon_stats(steam_id_64);
CREATE INDEX IF NOT EXISTS idx_player_weapon_stats_weapon ON dm_player_weapon_stats(weapon_name);
-- ============================================================================
-- Schema Summary
-- ============================================================================
-- dm_player_features: 213 columns (6 metadata + 207 features)
-- - Tier 1 CORE: 41 columns
-- - Tier 2 TACTICAL: 44 columns
-- - Tier 3 INTELLIGENCE: 53 columns
-- - Tier 4 META: 52 columns
-- - Tier 5 COMPOSITE: 11 columns
--
-- dm_player_match_history: Per-match snapshots for trend analysis
-- dm_player_map_stats: Map-level aggregations
-- dm_player_weapon_stats: Weapon usage statistics
-- ============================================================================
Binary file not shown.
+564
View File
@@ -0,0 +1,564 @@
Category,Path,Types,Examples
ats/api/v1/activityInterface/fallActivityInfo,code,int,401
ats/api/v1/activityInterface/fallActivityInfo,message,string,User auth failed
ats/api/v1/activityInterface/fallActivityInfo,data,null,None
ats/api/v1/activityInterface/fallActivityInfo,timeStamp,int,1768931732; 1768931718; 1768931709
ats/api/v1/activityInterface/fallActivityInfo,status,bool,False
ats/api/v1/activityInterface/fallActivityInfo,traceId,string,c3d47b6d9a6bf7099b45af1b3f516370; 96e6a86453435f463f2ff8e0b0d7611b; 2e40738b400d90ea6ece7be0abe2de3c
ats/api/v1/activityInterface/fallActivityInfo,success,bool,False
ats/api/v1/activityInterface/fallActivityInfo,errcode,int,401
crane/http/api/data/match/{match_id},data.has_side_data_and_rating2,bool,True
crane/http/api/data/match/{match_id},data.main.demo_url,string,; https://hz-demo.5eplaycdn.com/pug/20260118/g161-20260118202243599083093_de_dust2.zip; https://hz-demo.5eplaycdn.com/pug/20260118/g161-20260118215640650728700_de_nuke.zip
crane/http/api/data/match/{match_id},data.main.end_time,int,1739528619; 1739526455; 1739625426
crane/http/api/data/match/{match_id},data.main.game_mode,int,6; 24; 103
crane/http/api/data/match/{match_id},data.main.game_name,string,; nspug_c; npug_c
crane/http/api/data/match/{match_id},data.main.group1_all_score,int,10; 9; 4
crane/http/api/data/match/{match_id},data.main.group1_change_elo,int,0
crane/http/api/data/match/{match_id},data.main.group1_fh_role,int,1
crane/http/api/data/match/{match_id},data.main.group1_fh_score,int,6; 2; 7
crane/http/api/data/match/{match_id},data.main.group1_origin_elo,"float, int",1628.1; 1616.55; 1573.79
crane/http/api/data/match/{match_id},data.main.group1_sh_role,int,0
crane/http/api/data/match/{match_id},data.main.group1_sh_score,int,6; 5; 4
crane/http/api/data/match/{match_id},data.main.group1_tid,int,0
crane/http/api/data/match/{match_id},data.main.group1_uids,string,"14869472,14888575,1326932,14869396,14889445; 14869472,14889445,14869396,18337753,1326932; 18337753,14869472,14869396,13889539,1326932"
crane/http/api/data/match/{match_id},data.main.group2_all_score,int,6; 5; 11
crane/http/api/data/match/{match_id},data.main.group2_change_elo,int,0
crane/http/api/data/match/{match_id},data.main.group2_fh_role,int,0
crane/http/api/data/match/{match_id},data.main.group2_fh_score,int,6; 10; 7
crane/http/api/data/match/{match_id},data.main.group2_origin_elo,"float, int",1617.02; 1594.69; 1610.97
crane/http/api/data/match/{match_id},data.main.group2_sh_role,int,1
crane/http/api/data/match/{match_id},data.main.group2_sh_score,int,6; 5; 4
crane/http/api/data/match/{match_id},data.main.group2_tid,int,0
crane/http/api/data/match/{match_id},data.main.group2_uids,string,"7866482,7976557,13918176,7998628,18857497; 12501578,20691317,17181895,19535157,13074509; 14889445,14869472,14888575,1326932,14869396"
crane/http/api/data/match/{match_id},data.main.id,int,232025624; 232016531; 232248045
crane/http/api/data/match/{match_id},data.main.knife_winner,int,0
crane/http/api/data/match/{match_id},data.main.knife_winner_role,int,0
crane/http/api/data/match/{match_id},data.main.location,string,hz; sz; cd
crane/http/api/data/match/{match_id},data.main.location_full,string,sh_pug-low; sz_pug-high; bj_pug-low_volc
crane/http/api/data/match/{match_id},data.main.map,string,de_nuke; de_ancient; de_dust2
crane/http/api/data/match/{match_id},data.main.map_desc,string,阿努比斯; 远古遗迹; 炙热沙城2
crane/http/api/data/match/{match_id},data.main.match_code,string,g161-20250215211846894242128; g161-20250214164955786323546; g161-20250214172202090993964
crane/http/api/data/match/{match_id},data.main.match_mode,int,9
crane/http/api/data/match/{match_id},data.main.match_winner,int,1; 2
crane/http/api/data/match/{match_id},data.main.most_1v2_uid,"<5eid>, int",14869396; 18337753; 16009709
crane/http/api/data/match/{match_id},data.main.most_assist_uid,"<5eid>, int",14869396; 13918176; 15820822
crane/http/api/data/match/{match_id},data.main.most_awp_uid,"<5eid>, int",12501578; 21610332; 18337753
crane/http/api/data/match/{match_id},data.main.most_end_uid,"<5eid>, int",12501578; 14889445; 14565365
crane/http/api/data/match/{match_id},data.main.most_first_kill_uid,"<5eid>, int",18337753; 19535157; 14888575
crane/http/api/data/match/{match_id},data.main.most_headshot_uid,"<5eid>, int",17181895; 1326932; 16009709
crane/http/api/data/match/{match_id},data.main.most_jump_uid,"<5eid>, int",12501578; 17746844; 17783270
crane/http/api/data/match/{match_id},data.main.mvp_uid,"<5eid>, int",19535157; 14888575; 14869472
crane/http/api/data/match/{match_id},data.main.round_total,int,24; 22; 17
crane/http/api/data/match/{match_id},data.main.season,string,2025s2; 2025s3; 2025s4
crane/http/api/data/match/{match_id},data.main.server_ip,string,
crane/http/api/data/match/{match_id},data.main.server_port,string,27015
crane/http/api/data/match/{match_id},data.main.start_time,int,1739523090; 1739625610; 1739623308
crane/http/api/data/match/{match_id},data.main.status,int,1
crane/http/api/data/match/{match_id},data.main.waiver,int,0
crane/http/api/data/match/{match_id},data.main.year,int,2026; 2025
crane/http/api/data/match/{match_id},data.main.cs_type,int,0
crane/http/api/data/match/{match_id},data.main.priority_show_type,int,3; 1; 2
crane/http/api/data/match/{match_id},data.main.pug10m_show_type,int,1; 0
crane/http/api/data/match/{match_id},data.main.credit_match_status,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.adr,string,106.58; 100.22; 62.39
crane/http/api/data/match/{match_id},data.group_N[].fight_any.assist,string,2; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.awp_kill,string,2; 5; 4
crane/http/api/data/match/{match_id},data.group_N[].fight_any.benefit_kill,string,6; 5; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.day,string,20250218; 20250217; 20250214
crane/http/api/data/match/{match_id},data.group_N[].fight_any.death,string,5; 16; 4
crane/http/api/data/match/{match_id},data.group_N[].fight_any.defused_bomb,string,2; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.end_1v1,string,2; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.end_1v2,string,2; 1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.end_1v3,string,2; 1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.end_1v4,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.end_1v5,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.explode_bomb,string,2; 5; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.first_death,string,5; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.first_kill,string,2; 7; 4
crane/http/api/data/match/{match_id},data.group_N[].fight_any.flash_enemy,string,43; 7; 4
crane/http/api/data/match/{match_id},data.group_N[].fight_any.flash_enemy_time,string,7; 4; 15
crane/http/api/data/match/{match_id},data.group_N[].fight_any.flash_team,string,5; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.flash_team_time,string,21; 16; 4
crane/http/api/data/match/{match_id},data.group_N[].fight_any.flash_time,string,6; 21; 7
crane/http/api/data/match/{match_id},data.group_N[].fight_any.game_mode,string,6; 24; 103
crane/http/api/data/match/{match_id},data.group_N[].fight_any.group_id,string,1; 2
crane/http/api/data/match/{match_id},data.group_N[].fight_any.headshot,string,2; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.hold_total,string,0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.id,string,1937230471; 168065372; 168065362
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_highlight,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_most_1v2,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_most_assist,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_most_awp,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_most_end,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_most_first_kill,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_most_headshot,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_most_jump,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_mvp,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_svp,string,; 1
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_tie,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.is_win,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.jump_total,string,64; 33; 17
crane/http/api/data/match/{match_id},data.group_N[].fight_any.kast,string,0.82; 0.7; 0.74
crane/http/api/data/match/{match_id},data.group_N[].fight_any.kill,string,14; 21; 7
crane/http/api/data/match/{match_id},data.group_N[].fight_any.kill_1,string,5; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.kill_2,string,2; 5; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.kill_3,string,2; 5; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.kill_4,string,3; 2; 1
crane/http/api/data/match/{match_id},data.group_N[].fight_any.kill_5,string,2; 1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.map,string,de_nuke; de_ancient; de_dust2
crane/http/api/data/match/{match_id},data.group_N[].fight_any.match_code,string,g161-20250215211846894242128; g161-20250214164955786323546; g161-20250214172202090993964
crane/http/api/data/match/{match_id},data.group_N[].fight_any.match_mode,string,9
crane/http/api/data/match/{match_id},data.group_N[].fight_any.match_team_id,string,2; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.match_time,string,1739625526; 1739623222; 1739522995
crane/http/api/data/match/{match_id},data.group_N[].fight_any.per_headshot,string,0.44; 0.29; 0.21
crane/http/api/data/match/{match_id},data.group_N[].fight_any.planted_bomb,string,2; 5; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.rating,string,0.89; 0.87; 1.21
crane/http/api/data/match/{match_id},data.group_N[].fight_any.many_assists_cnt1,string,6; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.many_assists_cnt2,string,2; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.many_assists_cnt3,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.many_assists_cnt4,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.many_assists_cnt5,string,0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.perfect_kill,string,10; 17; 7
crane/http/api/data/match/{match_id},data.group_N[].fight_any.assisted_kill,string,5; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.rating2,string,1.24; 1.63; 0.87
crane/http/api/data/match/{match_id},data.group_N[].fight_any.rating3,string,2.15; -0.53; 0.00
crane/http/api/data/match/{match_id},data.group_N[].fight_any.revenge_kill,string,2; 7; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.round_total,string,17; 5; 23
crane/http/api/data/match/{match_id},data.group_N[].fight_any.rws,string,8.41; 8.86; 6.02
crane/http/api/data/match/{match_id},data.group_N[].fight_any.season,string,2025s2; 2025s3; 2025s4
crane/http/api/data/match/{match_id},data.group_N[].fight_any.team_kill,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].fight_any.throw_harm,string,120; 119; 70
crane/http/api/data/match/{match_id},data.group_N[].fight_any.throw_harm_enemy,string,10; 147; 3
crane/http/api/data/match/{match_id},data.group_N[].fight_any.uid,"<5eid>, string",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].fight_any.year,string,2026; 2025
crane/http/api/data/match/{match_id},data.group_N[].sts.data_tips_detail,int,-7; 0
crane/http/api/data/match/{match_id},data.group_N[].sts.challenge_status,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].sts.map_reward_status,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].sts.change_rank,int,-423964; -51338; -9561
crane/http/api/data/match/{match_id},data.group_N[].sts.origin_level_id,int,103; 108; 105
crane/http/api/data/match/{match_id},data.group_N[].sts.rank_change_type,int,5; 1; 0
crane/http/api/data/match/{match_id},data.group_N[].sts.star_num,int,0
crane/http/api/data/match/{match_id},data.group_N[].sts.origin_star_num,int,0
crane/http/api/data/match/{match_id},data.group_N[].sts.change_elo,string,-22.97; -36.73; -20.39
crane/http/api/data/match/{match_id},data.group_N[].sts.id,string,1930709265; 1930709271; 1930709266
crane/http/api/data/match/{match_id},data.group_N[].sts.level_id,string,103; 108; 104
crane/http/api/data/match/{match_id},data.group_N[].sts.match_code,string,g161-20250215211846894242128; g161-20250214164955786323546; g161-20250214172202090993964
crane/http/api/data/match/{match_id},data.group_N[].sts.match_flag,string,32; 2; 3
crane/http/api/data/match/{match_id},data.group_N[].sts.match_mode,string,9
crane/http/api/data/match/{match_id},data.group_N[].sts.match_status,string,3; 2; 0
crane/http/api/data/match/{match_id},data.group_N[].sts.origin_elo,string,1214.69; 1490.09; 1777.88
crane/http/api/data/match/{match_id},data.group_N[].sts.origin_match_total,string,269; 145; 63
crane/http/api/data/match/{match_id},data.group_N[].sts.placement,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].sts.punishment,string,1; 0
crane/http/api/data/match/{match_id},data.group_N[].sts.rank,string,3251068; 1410250; 2717215
crane/http/api/data/match/{match_id},data.group_N[].sts.origin_rank,string,2293251; 3241507; 1358912
crane/http/api/data/match/{match_id},data.group_N[].sts.season,string,2025s2; 2025s3; 2025s4
crane/http/api/data/match/{match_id},data.group_N[].sts.special_data,string,"; {""match_data"":[{""is_win"":-1,""match_id"":""g161-20250214164503716847890"",""match_status"":0,""change_elo"":-100.14724769911413},{""is_win"":1,""match_id"":""g161-20250214172202090993964"",""match_status"":0,""change_elo"":160.71161885810778},{""is_win"":0,""match_id"":"""",""match_status"":0,""change_elo"":0},{""is_win"":0,""match_id"":"""",""match_status"":0,""change_elo"":0},{""is_win"":0,""match_id"":"""",""match_status"":0,""change_elo"":0}]}; {""match_data"":[{""is_win"":-1,""match_id"":""g161-20250214164503716847890"",""match_status"":0,""change_elo"":-56.99773123078694},{""is_win"":1,""match_id"":""g161-20250214172202090993964"",""match_status"":0,""change_elo"":120.48283784034022},{""is_win"":0,""match_id"":"""",""match_status"":0,""change_elo"":0},{""is_win"":0,""match_id"":"""",""match_status"":0,""change_elo"":0},{""is_win"":0,""match_id"":"""",""match_status"":0,""change_elo"":0}]}"
crane/http/api/data/match/{match_id},data.group_N[].sts.uid,"<5eid>, string",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].level_info.level_id,int,103; 108; 104
crane/http/api/data/match/{match_id},data.group_N[].level_info.level_name,string,C; E-; B-
crane/http/api/data/match/{match_id},data.group_N[].level_info.level_type,int,2; 1; 0
crane/http/api/data/match/{match_id},data.group_N[].level_info.star_num,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.origin_star_num,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.dragon_flag,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.deduct_data.all_deduct_elo,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.deduct_data.deduct_remain_elo,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.deduct_data.deduct_elo,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.special_data[].is_win,int,1; 0; -1
crane/http/api/data/match/{match_id},data.group_N[].level_info.special_data[].match_id,string,; g161-n-20250103203331443454143; g161-20250214164503716847890
crane/http/api/data/match/{match_id},data.group_N[].level_info.special_data[].match_status,int,2; 0
crane/http/api/data/match/{match_id},data.group_N[].level_info.special_data[].change_elo,"float, int",-100.14724769911413; 120.48283784034022; 160.71161885810778
crane/http/api/data/match/{match_id},data.group_N[].level_info.match_status,string,3; 2; 0
crane/http/api/data/match/{match_id},data.group_N[].level_info.match_flag,string,32; 2; 3
crane/http/api/data/match/{match_id},data.group_N[].level_info.change_elo,string,-22.97; -36.73; -20.39
crane/http/api/data/match/{match_id},data.group_N[].level_info.origin_elo,string,1214.69; 1490.09; 1777.88
crane/http/api/data/match/{match_id},data.group_N[].level_info.rank,string,3251068; 1410250; 2717215
crane/http/api/data/match/{match_id},data.group_N[].level_info.origin_rank,string,; 1444425; 1444424
crane/http/api/data/match/{match_id},data.group_N[].level_info.trigger_promotion,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.special_bo,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.rise_type,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.tie_status,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].level_info.level_elo,int,800; 1700; 1400
crane/http/api/data/match/{match_id},data.group_N[].level_info.max_level,int,19; 30; 0
crane/http/api/data/match/{match_id},data.group_N[].level_info.origin_level_id,int,103; 108; 105
crane/http/api/data/match/{match_id},data.group_N[].level_info.origin_match_total,int,269; 145; 63
crane/http/api/data/match/{match_id},data.group_N[].level_info.star_info.change_small_star_num,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.star_info.origin_small_star_num,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.star_info.change_type,int,0
crane/http/api/data/match/{match_id},data.group_N[].level_info.star_info.now_small_star_num,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.username,"<5eid>, string",Sonka; 午夜伤心忧郁玫瑰; _陆小果
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.uuid,string,e6f87d93-ea92-11ee-9ce2-ec0d9a495494; 857f1c11-49c8-11ef-ac9f-ec0d9a7185e0; 4d9e3561-c373-11ef-848e-506b4bfa3106
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.email,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.area,string,; 86; 852
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.mobile,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.createdAt,int,1711362715; 1688270111; 1676517088
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.updatedAt,int,1767921452; 1768905111; 1767770760
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.domain,"<5eid>, string",123442; 1226wi4xw0ya; 15478597ldiutg
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.nickname,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.avatarUrl,string,disguise/images/cf/b2/cfb285c3d8d1c905b648954e42dc8cb0.jpg; disguise/images/9d/94/9d94029776f802318860f1bbd19c3bca.jpg; prop/images/6f/c0/6fc0c147e94ea8b1432ed072c19b0991.png
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.avatarAuditStatus,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.rgbAvatarUrl,string,; rgb_avatar/20230503/1fc76fccd31807fcb709d5d119522d32.rgb; rgb_avatar/20230803/d8b7ba92df98837791082ea3bcf6292b.rgb
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.photoUrl,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.gender,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.birthday,int,1141315200; 904233600; 1077638400
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.countryId,string,; kr; bm
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.regionId,string,; 620000; 450000
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.cityId,string,; 360400; 451100
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.language,string,simplified-chinese;
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.recommendUrl,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.groupId,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.profile.regSource,int,5; 4; 3
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.status,int,-4; -6; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.expire,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.cancellationStatus,int,2; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.newUser,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.loginBannedTime,int,1687524902; 1733207455; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.anticheatType,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.flagStatus1,string,32; 4224; 24704
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.anticheatStatus,string,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.FlagHonor,string,65548; 93196; 2162700
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.PrivacyPolicyStatus,int,3; 4
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.status.csgoFrozenExptime,int,1766231693; 1767001958; 1760438129
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.platformExp.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.platformExp.level,int,22; 30; 25
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.platformExp.exp,int,12641; 32004; 13776
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.steam.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.steam.steamId,<steamid>,76561198812383596; 76561199812085195; 76561199187871084
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.steam.steamAccount,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.steam.tradeUrl,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.steam.rentSteamId,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.trusted.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.trusted.credit,int,2550; 2990; 2033
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.trusted.creditLevel,int,3; 1; 4
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.trusted.score,int,100000; 97059; 96082
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.trusted.status,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.trusted.creditStatus,int,1; 2
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.certify.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.certify.idType,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.certify.status,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.certify.age,int,20; 22; 25
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.certify.realName,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.certify.auditStatus,int,1
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.certify.gender,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.uid,"<5eid>, int",14026928; 15478597; 21610332
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.type,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.extras,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.status,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.slogan,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.slogan_ext,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.live_url,string,
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.identity.live_type,int,0
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.usernameAuditStatus,int,1
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.Accid,string,263d37a4e1f87bce763e0d1b8ec03982; 07809f60e739d9c47648f4acda66667d; 879462b5de38dce892033adc138dec22
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.teamID,int,99868; 132671; 117796
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.domain,"<5eid>, string",123442; 1226wi4xw0ya; 15478597ldiutg
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_data.trumpetCount,int,2; 23; 1
crane/http/api/data/match/{match_id},data.group_N[].user_info.plus_info.is_plus,int,1; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.plus_info.plus_icon,string,images/act/e9cf57699303d9f6b18e465156fc6291.png; images/act/dae5c4cb98ceb6eeb1700f63c9ed14b7.png; images/act/09bbeb0f83a2f13419a0d75ac93e8a0c.png
crane/http/api/data/match/{match_id},data.group_N[].user_info.plus_info.plus_icon_short,string,images/act/d53f3bd55c836e057af230e2a138e94a.png; images/act/b7e90458420245283d9878a1e92b3a74.png; images/act/49b525ee6f74f423f3c2f0f913289824.png
crane/http/api/data/match/{match_id},data.group_N[].user_info.plus_info.vip_level,int,6; 5; 0
crane/http/api/data/match/{match_id},data.group_N[].user_info.plus_info.plus_grade,int,6; 2; 4
crane/http/api/data/match/{match_id},data.group_N[].user_info.plus_info.growth_score,int,540; 8196; 5458
crane/http/api/data/match/{match_id},data.group_N[].user_info.user_avatar_frame,null,None
crane/http/api/data/match/{match_id},data.group_N[].friend_relation,int,0
crane/http/api/data/match/{match_id},data.level_list[].elo,int,1000; 800; 900
crane/http/api/data/match/{match_id},data.level_list[].remark,string,800-899; 700-799; 900-999
crane/http/api/data/match/{match_id},data.level_list[].level_id,int,2; 5; 4
crane/http/api/data/match/{match_id},data.level_list[].level_name,string,E-; E+; N
crane/http/api/data/match/{match_id},data.level_list[].elo_type,int,9
crane/http/api/data/match/{match_id},data.level_list[].group_id,int,2; 5; 4
crane/http/api/data/match/{match_id},data.level_list[].level_image,string,
crane/http/api/data/match/{match_id},data.level_list[].rise_type,int,0
crane/http/api/data/match/{match_id},data.level_list[].shelves_status,int,1
crane/http/api/data/match/{match_id},data.room_card.id,string,310; 1326; 1309
crane/http/api/data/match/{match_id},data.room_card.category,string,48; 0
crane/http/api/data/match/{match_id},data.room_card.describe,string,; PLUS1专属房间卡片; 灵动小5房间卡片
crane/http/api/data/match/{match_id},data.room_card.name,string,; PLUS1专属房间卡片; 赛博少女
crane/http/api/data/match/{match_id},data.room_card.propTemplateId,string,133841; 134304; 1001
crane/http/api/data/match/{match_id},data.room_card.getWay,string,
crane/http/api/data/match/{match_id},data.room_card.onShelf,int,0
crane/http/api/data/match/{match_id},data.room_card.shelfAt,string,
crane/http/api/data/match/{match_id},data.room_card.getButton,int,0
crane/http/api/data/match/{match_id},data.room_card.getUrl,string,
crane/http/api/data/match/{match_id},data.room_card.attrs.flagAnimation,string,; https://oss-arena.5eplay.com/prop/videos/ba/23/ba2356a47ba93454a2de62c6fb817f82.avif; https://oss-arena.5eplay.com/prop/videos/59/79/59795c76433dfcadad8e6c02627e7d0f.avif
crane/http/api/data/match/{match_id},data.room_card.attrs.flagAnimationTime,string,; 2
crane/http/api/data/match/{match_id},data.room_card.attrs.flagViewUrl,string,https://oss-arena.5eplay.com/prop/images/49/36/49365bf9f2b7fe3ac6a7ded3656e092a.png; https://oss-arena.5eplay.com/prop/images/77/8c/778c698eb83d864e49e8a90bc8837a50.png; https://oss-arena.5eplay.com/prop/images/09/a9/09a93ce3f1476005f926298491188b21.png
crane/http/api/data/match/{match_id},data.room_card.attrs.flagViewVideo,string,; https://oss-arena.5eplay.com/prop/videos/6a/ae/6aaee03bbd40a093e5c00d6babe8e276.avif; https://oss-arena.5eplay.com/prop/videos/11/e8/11e8446dcd0202316605b08ab0b35466.avif
crane/http/api/data/match/{match_id},data.room_card.attrs.flagViewVideoTime,string,; 5; 2
crane/http/api/data/match/{match_id},data.room_card.attrs.getWay,string,升级至PLUS1级获取; 购买DANK1NG联名装扮获得; CS全新版本上线活动获得
crane/http/api/data/match/{match_id},data.room_card.attrs.mallJumpLink,string,
crane/http/api/data/match/{match_id},data.room_card.attrs.matchViewUrlLeft,string,https://oss-arena.5eplay.com/prop/images/13/fd/13fdb6d3b8dfaca3e8cd4987acc45606.png; https://oss-arena.5eplay.com/prop/images/1a/3a/1a3a7725e7bcb19f5a42858160e78bf8.png; https://oss-arena.5eplay.com/prop/images/f9/36/f9366f00cf41b3609a5b52194bf3b309.png
crane/http/api/data/match/{match_id},data.room_card.attrs.matchViewUrlRight,string,https://oss-arena.5eplay.com/prop/images/a9/da/a9da623d19cff27141cf6335507071ff.png; https://oss-arena.5eplay.com/prop/images/fa/45/fa45de3775d1bb75a6456c75ea454147.png; https://oss-arena.5eplay.com/prop/images/0c/f6/0cf657f3461dbd312a1083f546db9e54.png
crane/http/api/data/match/{match_id},data.room_card.attrs.mvpSettleAnimation,string,https://oss-arena.5eplay.com/dress/room_card/9e2ab6983d4ed9a6d23637abd9cd2152.mp4; https://oss-arena.5eplay.com/prop/videos/38/3e/383ec8198005d46da7194252353e7cf4.mp4; https://oss-arena.5eplay.com/prop/videos/14/05/14055e4e7cb184edb5f9849031e97231.mp4
crane/http/api/data/match/{match_id},data.room_card.attrs.mvpSettleColor,string,#9f1dea; #1ab5c6; #c89c68
crane/http/api/data/match/{match_id},data.room_card.attrs.mvpSettleViewAnimation,string,https://oss-arena.5eplay.com/dress/room_card/9e2ab6983d4ed9a6d23637abd9cd2152.mp4; https://oss-arena.5eplay.com/prop/videos/82/52/82526d004e9d0f41f3a3e7367b253003.mp4; https://oss-arena.5eplay.com/prop/videos/d2/bc/d2bc06fcc9e997c1d826537c145ea38e.mp4
crane/http/api/data/match/{match_id},data.room_card.attrs.pcImg,string,https://oss-arena.5eplay.com/prop/images/1a/47/1a47dda552d9501004d9043f637406d5.png; https://oss-arena.5eplay.com/prop/images/a1/e6/a1e6656596228734258d74b727a1aa48.png; https://oss-arena.5eplay.com/prop/images/d5/45/d545c6caf716a99a6725d24e37098078.png
crane/http/api/data/match/{match_id},data.room_card.attrs.sort,int,1; 2
crane/http/api/data/match/{match_id},data.room_card.attrs.templateId,int,2029; 1663; 2050
crane/http/api/data/match/{match_id},data.room_card.attrs.rarityLevel,int,3; 4; 2
crane/http/api/data/match/{match_id},data.room_card.attrs.sourceId,int,3; 11; 4
crane/http/api/data/match/{match_id},data.room_card.displayStatus,int,0
crane/http/api/data/match/{match_id},data.room_card.sysType,int,0
crane/http/api/data/match/{match_id},data.room_card.createdAt,string,
crane/http/api/data/match/{match_id},data.room_card.updatedAt,string,
crane/http/api/data/match/{match_id},data.round_sfui_type[],string,2; 5; 4
crane/http/api/data/match/{match_id},data.user_stats.map_level.map_exp,int,0
crane/http/api/data/match/{match_id},data.user_stats.map_level.add_exp,int,0
crane/http/api/data/match/{match_id},data.user_stats.plat_level.plat_level_exp,int,0
crane/http/api/data/match/{match_id},data.user_stats.plat_level.add_exp,int,0
crane/http/api/data/match/{match_id},data.group_1_team_info.team_id,string,
crane/http/api/data/match/{match_id},data.group_1_team_info.team_name,string,
crane/http/api/data/match/{match_id},data.group_1_team_info.logo_url,string,
crane/http/api/data/match/{match_id},data.group_1_team_info.team_domain,string,
crane/http/api/data/match/{match_id},data.group_1_team_info.team_tag,string,
crane/http/api/data/match/{match_id},data.group_2_team_info.team_id,string,
crane/http/api/data/match/{match_id},data.group_2_team_info.team_name,string,
crane/http/api/data/match/{match_id},data.group_2_team_info.logo_url,string,
crane/http/api/data/match/{match_id},data.group_2_team_info.team_domain,string,
crane/http/api/data/match/{match_id},data.group_2_team_info.team_tag,string,
crane/http/api/data/match/{match_id},data.treat_info.user_id,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.username,string,熊出没之深情熊二; Royc灬Kerat丶
crane/http/api/data/match/{match_id},data.treat_info.user_data.uuid,string,c9caad5c-a9b3-11ef-848e-506b4bfa3106; 83376211-5c36-11ed-9ce2-ec0d9a495494
crane/http/api/data/match/{match_id},data.treat_info.user_data.email,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.area,string,86
crane/http/api/data/match/{match_id},data.treat_info.user_data.mobile,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.createdAt,int,1667562471; 1732377512
crane/http/api/data/match/{match_id},data.treat_info.user_data.updatedAt,int,1768911939; 1768904695
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.domain,string,13048069yf1jto; 1123rqi1bfha
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.nickname,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.avatarUrl,string,prop/images/3d/c4/3dc4259c07c31adb2439f7acbf1e565f.png; disguise/images/0e/84/0e84fdbb1da54953f1985bfb206604a5.png
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.avatarAuditStatus,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.rgbAvatarUrl,string,; rgb_avatar/20221129/f1ba34afe43c4fa38fd7dd129b0dc303.rgb
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.photoUrl,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.gender,int,1; 0
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.birthday,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.countryId,string,; cn
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.regionId,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.cityId,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.language,string,simplified-chinese;
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.recommendUrl,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.groupId,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.profile.regSource,int,4; 0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.status,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.expire,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.cancellationStatus,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.newUser,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.loginBannedTime,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.anticheatType,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.flagStatus1,string,128
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.anticheatStatus,string,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.FlagHonor,string,1178636; 65548
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.PrivacyPolicyStatus,int,4
crane/http/api/data/match/{match_id},data.treat_info.user_data.status.csgoFrozenExptime,int,1767707372; 1765545847
crane/http/api/data/match/{match_id},data.treat_info.user_data.platformExp.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.platformExp.level,int,29
crane/http/api/data/match/{match_id},data.treat_info.user_data.platformExp.exp,int,26803; 26522
crane/http/api/data/match/{match_id},data.treat_info.user_data.steam.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.steam.steamId,<steamid>,76561199192775594; 76561198290113126
crane/http/api/data/match/{match_id},data.treat_info.user_data.steam.steamAccount,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.steam.tradeUrl,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.steam.rentSteamId,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.trusted.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.trusted.credit,int,2200; 5919
crane/http/api/data/match/{match_id},data.treat_info.user_data.trusted.creditLevel,int,4
crane/http/api/data/match/{match_id},data.treat_info.user_data.trusted.score,int,100000
crane/http/api/data/match/{match_id},data.treat_info.user_data.trusted.status,int,1
crane/http/api/data/match/{match_id},data.treat_info.user_data.trusted.creditStatus,int,1
crane/http/api/data/match/{match_id},data.treat_info.user_data.certify.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.certify.idType,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.certify.status,int,1
crane/http/api/data/match/{match_id},data.treat_info.user_data.certify.age,int,23; 42
crane/http/api/data/match/{match_id},data.treat_info.user_data.certify.realName,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.certify.auditStatus,int,1
crane/http/api/data/match/{match_id},data.treat_info.user_data.certify.gender,int,1
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.uid,"<5eid>, int",13048069; 21150835
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.type,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.extras,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.status,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.slogan,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.slogan_ext,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.live_url,string,
crane/http/api/data/match/{match_id},data.treat_info.user_data.identity.live_type,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.usernameAuditStatus,int,1
crane/http/api/data/match/{match_id},data.treat_info.user_data.Accid,string,57cd6b98be64949589a6cecf7d258cd1; d0d986c392c55c5d422fd2c46e4d6318
crane/http/api/data/match/{match_id},data.treat_info.user_data.teamID,int,0
crane/http/api/data/match/{match_id},data.treat_info.user_data.domain,string,13048069yf1jto; 1123rqi1bfha
crane/http/api/data/match/{match_id},data.treat_info.user_data.trumpetCount,int,3; 2442
crane/http/api/data/match/{match_id},data.season_type,int,0
crane/http/api/data/match/{match_id},code,int,0
crane/http/api/data/match/{match_id},message,string,操作成功
crane/http/api/data/match/{match_id},status,bool,True
crane/http/api/data/match/{match_id},timestamp,int,1768931731; 1768931718; 1768931708
crane/http/api/data/match/{match_id},trace_id,string,8ae4feeb19cc4ed3a24a8a00f056d023; 19582ac94190e3baff795cff50c7a6f3; 87794472a94e5e40be8e12bd116dad55
crane/http/api/data/match/{match_id},success,bool,True
crane/http/api/data/match/{match_id},errcode,int,0
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.fd_ct,int,2; 4; 3
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.fd_t,int,2; 4; 3
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.kast,"float, int",0.7; 0.65; 0.48
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.awp_kill,int,2; 5; 4
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.awp_kill_ct,int,5; 4; 3
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.awp_kill_t,int,2; 5; 4
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.damage_stats,int,3; 5; 50
crane/http/api/data/vip_plus_match_data/{match_id},data.<steamid>.damage_receive,int,0
crane/http/api/data/vip_plus_match_data/{match_id},code,int,0
crane/http/api/data/vip_plus_match_data/{match_id},message,string,操作成功
crane/http/api/data/vip_plus_match_data/{match_id},status,bool,True
crane/http/api/data/vip_plus_match_data/{match_id},timestamp,int,1768931714; 1768931732; 1768931710
crane/http/api/data/vip_plus_match_data/{match_id},trace_id,string,cff29d5dcdd6285b80d11bbb4a8a7da0; 6e7c0c0590b0e561c6c4c8d935ebb02c; 97c1377302559a8f5e01aedfeb208751
crane/http/api/data/vip_plus_match_data/{match_id},success,bool,True
crane/http/api/data/vip_plus_match_data/{match_id},errcode,int,0
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].round,int,2; 5; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].t_money_group,int,3; 1; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].ct_money_group,int,3; 1; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].win_reason,int,2; 5; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].bron_equipment.<steamid>[].Money,int,400; 200; 2900
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].bron_equipment.<steamid>[].WeaponName,string,weapon_flashbang; weapon_tec9; weapon_hegrenade
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].bron_equipment.<steamid>[].Weapon,int,22; 33; 37
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].player_t_score.<steamid>,"float, int",-21.459999999999997; -16.640000000000004; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].player_ct_score.<steamid>,"float, int",17.099999999999994; 15.120000000000001; 27.507999999999996
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].player_bron_crash.<steamid>,int,4200; 3900; 800
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].begin_ts,string,2026-01-18T19:57:29+08:00; 2026-01-18T19:59:18+08:00; 2026-01-18T19:55:55+08:00
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].sfui_event.sfui_type,int,2; 5; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].sfui_event.score_ct,int,2; 5; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].sfui_event.score_t,int,2; 10; 3
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].end_ts,string,2026-01-18T19:54:37+08:00; 2026-01-18T19:57:22+08:00; 2026-01-18T19:59:11+08:00
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].ts_real,string,0001-01-01T00:00:00Z; 2026-01-18T19:54:06+08:00; 2026-01-18T19:54:04+08:00
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].ts,int,45; 48; 46
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].t_num,int,2; 5; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].ct_num,int,2; 5; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].event_type,int,3; 1; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.Ts,string,2026-01-18T19:54:06+08:00; 2026-01-18T19:54:04+08:00; 2026-01-18T19:53:57+08:00
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.Killer,<steamid>,76561199787406643; 76561199032002725; 76561199078250590
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.Victim,<steamid>,76561199388433802; 76561199032002725; 76561199250737526
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.Weapon,int,6; 7; 5
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.KillerTeam,int,1; 2
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.KillerBot,bool,False
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.VictimBot,bool,False
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.WeaponName,string,usp_silencer; deagle; famas
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.Headshot,bool,False; True
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.Penetrated,bool,False; True
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.ThroughSmoke,bool,False; True
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.NoScope,bool,False; True
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.AttackerBlind,bool,False; True
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].kill_event.Attackerinair,bool,False
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].twin,"float, int",0.143; 0.341; 0.557
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].c_twin,"float, int",0.44299999999999995; 0.471; 0.659
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].twin_change,"float, int",-0.21600000000000003; -0.19800000000000004; 0.19899999999999995
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].c_twin_change,"float, int",0.21600000000000003; 0.19800000000000004; 0.17099999999999993
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].killer_score_change.<steamid>.score,"float, int",17.099999999999994; 19.899999999999995; 19.800000000000004
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].victim_score_change.<steamid>.score,"float, int",-15.8; -19.899999999999995; -21.6
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].assist_killer_score_change.<steamid>.score,float,2.592; 6.63; 6.45
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].trade_score_change.<steamid>.score,float,2.2100000000000004; 3.16; 3.66
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].flash_assist_killer_score_change.<steamid>.score,float,1.1520000000000001; 2.9850000000000003; 1.5299999999999996
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].protect_gun_player_score_change.<steamid>.score,float,5.8999999999999995; 7.1000000000000005
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].protect_gun_enemy_score_change.<steamid>.score,float,-1.18; -1.4200000000000002
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].disconnect_player_score_change,null,None
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].disconnect_comp_score_change,null,None
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].round_end_fixed_score_change.<steamid>.score,"float, int",20; -0.6000000000000005; -100
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].show_event[].win_reason,int,2; 5; 4
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].side_info.ct[],<steamid>,76561199032002725; 76561199078250590; 76561199076109761
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_stat[].side_info.t[],<steamid>,76561199787406643; 76561199388433802; 76561199250737526
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.player_scores.<steamid>,float,12.491187500000002; 1.5764999999999993; 2.073937500000001
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.player_t_scores.<steamid>,float,19.06; 6.3349999999999955; -8.872500000000002
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.player_ct_scores.<steamid>,float,-0.009666666666665455; 10.301583333333335; -2.9330833333333324
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.round_total,int,18; 30; 21
crane/http/api/match/leetify_rating/{match_id},data.leetify_data.player_round_scores.<steamid>.<round_n>,"float, int",32.347; -1.100000000000001; 20.040000000000006
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.uid,"<5eid>, int",14889445; 14869396; 14888575
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.uuid,string,13f7dc52-ea7c-11ed-9ce2-ec0d9a495494; e74f23a3-e8ae-11ed-9ce2-ec0d9a495494; 7ced32f8-ea70-11ed-9ce2-ec0d9a495494
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.username,string,刚拉; R1nging; RRRTINA
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.nickname,string,
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.reg_date,int,1683007881; 1683007342; 1683200437
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.username_spam_status,int,1
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.steamid_64,<steamid>,76561199032002725; 76561199078250590; 76561199076109761
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.avatar_url,string,disguise/images/6f/89/6f89b22633cb95df1754fd30573c5ad6.png; disguise/images/09/96/09961ea8fc45bed1c60157055a4c05c5.jpg; disguise/images/5d/41/5d4182b66a5004a974aee7501873164b.jpg
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.gender,int,1; 0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.country_id,string,; cn
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.language,string,; simplified-chinese
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.domain,string,rrrtina; 14869396o9jm5g; dxw123452
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.credit,int,0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.trusted_score,int,0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.trusted_status,int,0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.plus_info,null,None
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.region,int,0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.province,int,0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.province_name,string,
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.region_name,string,
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.college_id,int,0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.status,int,0
crane/http/api/match/leetify_rating/{match_id},data.uinfo_dict.<steamid>.identity,null,None
crane/http/api/match/leetify_rating/{match_id},code,int,0
crane/http/api/match/leetify_rating/{match_id},message,string,操作成功
crane/http/api/match/leetify_rating/{match_id},status,bool,True
crane/http/api/match/leetify_rating/{match_id},timestamp,int,1768833830; 1768833808; 1768833806
crane/http/api/match/leetify_rating/{match_id},trace_id,string,376e200283d19770bdef6dacf260f40f; a7dd6602d3aedb3017bb37727b5be75a; dab4013545b5581fbb089fb5c273d0a9
crane/http/api/match/leetify_rating/{match_id},success,bool,True
crane/http/api/match/leetify_rating/{match_id},errcode,int,0
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].attacker.name,string,5E-Player 我有必胜卡组; 5E-Player 青青C原懒大王w; 5E-Player xiezhongxie1
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].attacker.pos.x,int,734; 999; 1170
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].attacker.pos.y,int,125; -77; -772
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].attacker.pos.z,int,0
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].attacker.steamid_64,<steamid>,76561198330488905; 76561199032002725; 76561199076109761
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].attacker.team,int,1; 2
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].attackerblind,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].headshot,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].noscope,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].pasttime,int,45; 20; 24
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].penetrated,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].throughsmoke,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].victim.name,"<5eid>, string",5E-Player 青青C原懒大王w; 5E-Player 午夜伤心忧郁玫瑰; 5E-Player RRRTINA
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].victim.pos.x,int,1218; 706; 1298
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].victim.pos.y,int,627; 587; 219
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].victim.pos.z,int,0
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].victim.steamid_64,"<steamid>, string",76561199482118960; 76561199812085195; 76561199207654712
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].victim.team,int,1; 2
crane/http/api/match/round/{match_id},data.round_list[].all_kill[].weapon,string,usp_silencer; mag7; famas
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].attacker.name,string,5E-Player 我有必胜卡组; 5E-Player 青青C原懒大王w; 5E-Player xiezhongxie1
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].attacker.pos.x,int,734; 999; 397
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].attacker.pos.y,int,149; 125; -77
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].attacker.pos.z,int,0
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].attacker.steamid_64,<steamid>,76561198330488905; 76561199032002725; 76561199076109761
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].attacker.team,int,1; 2
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].attackerblind,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].headshot,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].noscope,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].pasttime,int,24; 57; 20
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].penetrated,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].throughsmoke,bool,False; True
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].victim.name,"<5eid>, string",5E-Player 青青C原懒大王w; 5E-Player 午夜伤心忧郁玫瑰; 5E-Player _陆小果
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].victim.pos.x,int,1218; 706; 1298
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].victim.pos.y,int,627; 587; 219
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].victim.pos.z,int,0
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].victim.steamid_64,"<steamid>, string",76561198812383596; 76561199812085195; 76561199187871084
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].victim.team,int,1; 2
crane/http/api/match/round/{match_id},data.round_list[].kill.<steamid>[].weapon,string,usp_silencer; mag7; famas
crane/http/api/match/round/{match_id},data.round_list[].c4_event[].event_name,string,planted_c4
crane/http/api/match/round/{match_id},data.round_list[].c4_event[].location,string,
crane/http/api/match/round/{match_id},data.round_list[].c4_event[].name,string,5E-Player 我有必胜卡组; 5E-Player RRRTINA; 5E-Player 俺有鱼鱼蒸
crane/http/api/match/round/{match_id},data.round_list[].c4_event[].pasttime,int,45; 30; 31
crane/http/api/match/round/{match_id},data.round_list[].c4_event[].steamid_64,<steamid>,76561198330488905; 76561199812085195; 76561199207654712
crane/http/api/match/round/{match_id},data.round_list[].current_score.ct,int,2; 10; 1
crane/http/api/match/round/{match_id},data.round_list[].current_score.final_round_time,int,68; 79; 63
crane/http/api/match/round/{match_id},data.round_list[].current_score.pasttime,int,57; 47; 62
crane/http/api/match/round/{match_id},data.round_list[].current_score.t,int,2; 5; 4
crane/http/api/match/round/{match_id},data.round_list[].current_score.type,int,2; 5; 4
crane/http/api/match/round/{match_id},data.round_list[].death_list[],"<steamid>, string",76561198812383596; 76561199812085195; 76561199187871084
crane/http/api/match/round/{match_id},data.round_list[].equiped.<steamid>[],string,usp_silencer; kevlar(100); smokegrenade
crane/http/api/match/round/{match_id},data.round_list[].equiped.[],string,
crane/http/api/match/round/{match_id},data.weapon_list.defuser[],string,defuser
crane/http/api/match/round/{match_id},data.weapon_list.item[],string,incgrenade; flashbang; molotov
crane/http/api/match/round/{match_id},data.weapon_list.main_weapon[],string,sg556; awp; ssg08
crane/http/api/match/round/{match_id},data.weapon_list.other_item[],string,kevlar; helmet
crane/http/api/match/round/{match_id},data.weapon_list.secondary_weapon[],string,usp_silencer; deagle; glock
crane/http/api/match/round/{match_id},code,int,0
crane/http/api/match/round/{match_id},message,string,操作成功
crane/http/api/match/round/{match_id},status,bool,True
crane/http/api/match/round/{match_id},timestamp,int,1768931714; 1768931731; 1768931710
crane/http/api/match/round/{match_id},trace_id,string,c2ee4f45abd89f1c90dc1cc390d21d33; f85069de4d785710dd55301334ff03c0; 98335f4087c76de69e8aeda3ca767d6f
crane/http/api/match/round/{match_id},success,bool,True
crane/http/api/match/round/{match_id},errcode,int,0
1 Category Path Types Examples
2 ats/api/v1/activityInterface/fallActivityInfo code int 401
3 ats/api/v1/activityInterface/fallActivityInfo message string User auth failed
4 ats/api/v1/activityInterface/fallActivityInfo data null None
5 ats/api/v1/activityInterface/fallActivityInfo timeStamp int 1768931732; 1768931718; 1768931709
6 ats/api/v1/activityInterface/fallActivityInfo status bool False
7 ats/api/v1/activityInterface/fallActivityInfo traceId string c3d47b6d9a6bf7099b45af1b3f516370; 96e6a86453435f463f2ff8e0b0d7611b; 2e40738b400d90ea6ece7be0abe2de3c
8 ats/api/v1/activityInterface/fallActivityInfo success bool False
9 ats/api/v1/activityInterface/fallActivityInfo errcode int 401
10 crane/http/api/data/match/{match_id} data.has_side_data_and_rating2 bool True
11 crane/http/api/data/match/{match_id} data.main.demo_url string ; https://hz-demo.5eplaycdn.com/pug/20260118/g161-20260118202243599083093_de_dust2.zip; https://hz-demo.5eplaycdn.com/pug/20260118/g161-20260118215640650728700_de_nuke.zip
12 crane/http/api/data/match/{match_id} data.main.end_time int 1739528619; 1739526455; 1739625426
13 crane/http/api/data/match/{match_id} data.main.game_mode int 6; 24; 103
14 crane/http/api/data/match/{match_id} data.main.game_name string ; nspug_c; npug_c
15 crane/http/api/data/match/{match_id} data.main.group1_all_score int 10; 9; 4
16 crane/http/api/data/match/{match_id} data.main.group1_change_elo int 0
17 crane/http/api/data/match/{match_id} data.main.group1_fh_role int 1
18 crane/http/api/data/match/{match_id} data.main.group1_fh_score int 6; 2; 7
19 crane/http/api/data/match/{match_id} data.main.group1_origin_elo float, int 1628.1; 1616.55; 1573.79
20 crane/http/api/data/match/{match_id} data.main.group1_sh_role int 0
21 crane/http/api/data/match/{match_id} data.main.group1_sh_score int 6; 5; 4
22 crane/http/api/data/match/{match_id} data.main.group1_tid int 0
23 crane/http/api/data/match/{match_id} data.main.group1_uids string 14869472,14888575,1326932,14869396,14889445; 14869472,14889445,14869396,18337753,1326932; 18337753,14869472,14869396,13889539,1326932
24 crane/http/api/data/match/{match_id} data.main.group2_all_score int 6; 5; 11
25 crane/http/api/data/match/{match_id} data.main.group2_change_elo int 0
26 crane/http/api/data/match/{match_id} data.main.group2_fh_role int 0
27 crane/http/api/data/match/{match_id} data.main.group2_fh_score int 6; 10; 7
28 crane/http/api/data/match/{match_id} data.main.group2_origin_elo float, int 1617.02; 1594.69; 1610.97
29 crane/http/api/data/match/{match_id} data.main.group2_sh_role int 1
30 crane/http/api/data/match/{match_id} data.main.group2_sh_score int 6; 5; 4
31 crane/http/api/data/match/{match_id} data.main.group2_tid int 0
32 crane/http/api/data/match/{match_id} data.main.group2_uids string 7866482,7976557,13918176,7998628,18857497; 12501578,20691317,17181895,19535157,13074509; 14889445,14869472,14888575,1326932,14869396
33 crane/http/api/data/match/{match_id} data.main.id int 232025624; 232016531; 232248045
34 crane/http/api/data/match/{match_id} data.main.knife_winner int 0
35 crane/http/api/data/match/{match_id} data.main.knife_winner_role int 0
36 crane/http/api/data/match/{match_id} data.main.location string hz; sz; cd
37 crane/http/api/data/match/{match_id} data.main.location_full string sh_pug-low; sz_pug-high; bj_pug-low_volc
38 crane/http/api/data/match/{match_id} data.main.map string de_nuke; de_ancient; de_dust2
39 crane/http/api/data/match/{match_id} data.main.map_desc string 阿努比斯; 远古遗迹; 炙热沙城2
40 crane/http/api/data/match/{match_id} data.main.match_code string g161-20250215211846894242128; g161-20250214164955786323546; g161-20250214172202090993964
41 crane/http/api/data/match/{match_id} data.main.match_mode int 9
42 crane/http/api/data/match/{match_id} data.main.match_winner int 1; 2
43 crane/http/api/data/match/{match_id} data.main.most_1v2_uid <5eid>, int 14869396; 18337753; 16009709
44 crane/http/api/data/match/{match_id} data.main.most_assist_uid <5eid>, int 14869396; 13918176; 15820822
45 crane/http/api/data/match/{match_id} data.main.most_awp_uid <5eid>, int 12501578; 21610332; 18337753
46 crane/http/api/data/match/{match_id} data.main.most_end_uid <5eid>, int 12501578; 14889445; 14565365
47 crane/http/api/data/match/{match_id} data.main.most_first_kill_uid <5eid>, int 18337753; 19535157; 14888575
48 crane/http/api/data/match/{match_id} data.main.most_headshot_uid <5eid>, int 17181895; 1326932; 16009709
49 crane/http/api/data/match/{match_id} data.main.most_jump_uid <5eid>, int 12501578; 17746844; 17783270
50 crane/http/api/data/match/{match_id} data.main.mvp_uid <5eid>, int 19535157; 14888575; 14869472
51 crane/http/api/data/match/{match_id} data.main.round_total int 24; 22; 17
52 crane/http/api/data/match/{match_id} data.main.season string 2025s2; 2025s3; 2025s4
53 crane/http/api/data/match/{match_id} data.main.server_ip string
54 crane/http/api/data/match/{match_id} data.main.server_port string 27015
55 crane/http/api/data/match/{match_id} data.main.start_time int 1739523090; 1739625610; 1739623308
56 crane/http/api/data/match/{match_id} data.main.status int 1
57 crane/http/api/data/match/{match_id} data.main.waiver int 0
58 crane/http/api/data/match/{match_id} data.main.year int 2026; 2025
59 crane/http/api/data/match/{match_id} data.main.cs_type int 0
60 crane/http/api/data/match/{match_id} data.main.priority_show_type int 3; 1; 2
61 crane/http/api/data/match/{match_id} data.main.pug10m_show_type int 1; 0
62 crane/http/api/data/match/{match_id} data.main.credit_match_status int 1; 0
63 crane/http/api/data/match/{match_id} data.group_N[].fight_any.adr string 106.58; 100.22; 62.39
64 crane/http/api/data/match/{match_id} data.group_N[].fight_any.assist string 2; 4; 3
65 crane/http/api/data/match/{match_id} data.group_N[].fight_any.awp_kill string 2; 5; 4
66 crane/http/api/data/match/{match_id} data.group_N[].fight_any.benefit_kill string 6; 5; 3
67 crane/http/api/data/match/{match_id} data.group_N[].fight_any.day string 20250218; 20250217; 20250214
68 crane/http/api/data/match/{match_id} data.group_N[].fight_any.death string 5; 16; 4
69 crane/http/api/data/match/{match_id} data.group_N[].fight_any.defused_bomb string 2; 4; 3
70 crane/http/api/data/match/{match_id} data.group_N[].fight_any.end_1v1 string 2; 4; 3
71 crane/http/api/data/match/{match_id} data.group_N[].fight_any.end_1v2 string 2; 1; 0
72 crane/http/api/data/match/{match_id} data.group_N[].fight_any.end_1v3 string 2; 1; 0
73 crane/http/api/data/match/{match_id} data.group_N[].fight_any.end_1v4 string 1; 0
74 crane/http/api/data/match/{match_id} data.group_N[].fight_any.end_1v5 string 1; 0
75 crane/http/api/data/match/{match_id} data.group_N[].fight_any.explode_bomb string 2; 5; 3
76 crane/http/api/data/match/{match_id} data.group_N[].fight_any.first_death string 5; 4; 3
77 crane/http/api/data/match/{match_id} data.group_N[].fight_any.first_kill string 2; 7; 4
78 crane/http/api/data/match/{match_id} data.group_N[].fight_any.flash_enemy string 43; 7; 4
79 crane/http/api/data/match/{match_id} data.group_N[].fight_any.flash_enemy_time string 7; 4; 15
80 crane/http/api/data/match/{match_id} data.group_N[].fight_any.flash_team string 5; 4; 3
81 crane/http/api/data/match/{match_id} data.group_N[].fight_any.flash_team_time string 21; 16; 4
82 crane/http/api/data/match/{match_id} data.group_N[].fight_any.flash_time string 6; 21; 7
83 crane/http/api/data/match/{match_id} data.group_N[].fight_any.game_mode string 6; 24; 103
84 crane/http/api/data/match/{match_id} data.group_N[].fight_any.group_id string 1; 2
85 crane/http/api/data/match/{match_id} data.group_N[].fight_any.headshot string 2; 4; 3
86 crane/http/api/data/match/{match_id} data.group_N[].fight_any.hold_total string 0
87 crane/http/api/data/match/{match_id} data.group_N[].fight_any.id string 1937230471; 168065372; 168065362
88 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_highlight string 1; 0
89 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_most_1v2 string 1; 0
90 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_most_assist string 1; 0
91 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_most_awp string 1; 0
92 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_most_end string 1; 0
93 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_most_first_kill string 1; 0
94 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_most_headshot string 1; 0
95 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_most_jump string 1; 0
96 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_mvp string 1; 0
97 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_svp string ; 1
98 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_tie string 1; 0
99 crane/http/api/data/match/{match_id} data.group_N[].fight_any.is_win string 1; 0
100 crane/http/api/data/match/{match_id} data.group_N[].fight_any.jump_total string 64; 33; 17
101 crane/http/api/data/match/{match_id} data.group_N[].fight_any.kast string 0.82; 0.7; 0.74
102 crane/http/api/data/match/{match_id} data.group_N[].fight_any.kill string 14; 21; 7
103 crane/http/api/data/match/{match_id} data.group_N[].fight_any.kill_1 string 5; 4; 3
104 crane/http/api/data/match/{match_id} data.group_N[].fight_any.kill_2 string 2; 5; 3
105 crane/http/api/data/match/{match_id} data.group_N[].fight_any.kill_3 string 2; 5; 3
106 crane/http/api/data/match/{match_id} data.group_N[].fight_any.kill_4 string 3; 2; 1
107 crane/http/api/data/match/{match_id} data.group_N[].fight_any.kill_5 string 2; 1; 0
108 crane/http/api/data/match/{match_id} data.group_N[].fight_any.map string de_nuke; de_ancient; de_dust2
109 crane/http/api/data/match/{match_id} data.group_N[].fight_any.match_code string g161-20250215211846894242128; g161-20250214164955786323546; g161-20250214172202090993964
110 crane/http/api/data/match/{match_id} data.group_N[].fight_any.match_mode string 9
111 crane/http/api/data/match/{match_id} data.group_N[].fight_any.match_team_id string 2; 4; 3
112 crane/http/api/data/match/{match_id} data.group_N[].fight_any.match_time string 1739625526; 1739623222; 1739522995
113 crane/http/api/data/match/{match_id} data.group_N[].fight_any.per_headshot string 0.44; 0.29; 0.21
114 crane/http/api/data/match/{match_id} data.group_N[].fight_any.planted_bomb string 2; 5; 3
115 crane/http/api/data/match/{match_id} data.group_N[].fight_any.rating string 0.89; 0.87; 1.21
116 crane/http/api/data/match/{match_id} data.group_N[].fight_any.many_assists_cnt1 string 6; 4; 3
117 crane/http/api/data/match/{match_id} data.group_N[].fight_any.many_assists_cnt2 string 2; 4; 3
118 crane/http/api/data/match/{match_id} data.group_N[].fight_any.many_assists_cnt3 string 1; 0
119 crane/http/api/data/match/{match_id} data.group_N[].fight_any.many_assists_cnt4 string 1; 0
120 crane/http/api/data/match/{match_id} data.group_N[].fight_any.many_assists_cnt5 string 0
121 crane/http/api/data/match/{match_id} data.group_N[].fight_any.perfect_kill string 10; 17; 7
122 crane/http/api/data/match/{match_id} data.group_N[].fight_any.assisted_kill string 5; 4; 3
123 crane/http/api/data/match/{match_id} data.group_N[].fight_any.rating2 string 1.24; 1.63; 0.87
124 crane/http/api/data/match/{match_id} data.group_N[].fight_any.rating3 string 2.15; -0.53; 0.00
125 crane/http/api/data/match/{match_id} data.group_N[].fight_any.revenge_kill string 2; 7; 3
126 crane/http/api/data/match/{match_id} data.group_N[].fight_any.round_total string 17; 5; 23
127 crane/http/api/data/match/{match_id} data.group_N[].fight_any.rws string 8.41; 8.86; 6.02
128 crane/http/api/data/match/{match_id} data.group_N[].fight_any.season string 2025s2; 2025s3; 2025s4
129 crane/http/api/data/match/{match_id} data.group_N[].fight_any.team_kill string 1; 0
130 crane/http/api/data/match/{match_id} data.group_N[].fight_any.throw_harm string 120; 119; 70
131 crane/http/api/data/match/{match_id} data.group_N[].fight_any.throw_harm_enemy string 10; 147; 3
132 crane/http/api/data/match/{match_id} data.group_N[].fight_any.uid <5eid>, string 14026928; 15478597; 21610332
133 crane/http/api/data/match/{match_id} data.group_N[].fight_any.year string 2026; 2025
134 crane/http/api/data/match/{match_id} data.group_N[].sts.data_tips_detail int -7; 0
135 crane/http/api/data/match/{match_id} data.group_N[].sts.challenge_status int 1; 0
136 crane/http/api/data/match/{match_id} data.group_N[].sts.map_reward_status int 1; 0
137 crane/http/api/data/match/{match_id} data.group_N[].sts.change_rank int -423964; -51338; -9561
138 crane/http/api/data/match/{match_id} data.group_N[].sts.origin_level_id int 103; 108; 105
139 crane/http/api/data/match/{match_id} data.group_N[].sts.rank_change_type int 5; 1; 0
140 crane/http/api/data/match/{match_id} data.group_N[].sts.star_num int 0
141 crane/http/api/data/match/{match_id} data.group_N[].sts.origin_star_num int 0
142 crane/http/api/data/match/{match_id} data.group_N[].sts.change_elo string -22.97; -36.73; -20.39
143 crane/http/api/data/match/{match_id} data.group_N[].sts.id string 1930709265; 1930709271; 1930709266
144 crane/http/api/data/match/{match_id} data.group_N[].sts.level_id string 103; 108; 104
145 crane/http/api/data/match/{match_id} data.group_N[].sts.match_code string g161-20250215211846894242128; g161-20250214164955786323546; g161-20250214172202090993964
146 crane/http/api/data/match/{match_id} data.group_N[].sts.match_flag string 32; 2; 3
147 crane/http/api/data/match/{match_id} data.group_N[].sts.match_mode string 9
148 crane/http/api/data/match/{match_id} data.group_N[].sts.match_status string 3; 2; 0
149 crane/http/api/data/match/{match_id} data.group_N[].sts.origin_elo string 1214.69; 1490.09; 1777.88
150 crane/http/api/data/match/{match_id} data.group_N[].sts.origin_match_total string 269; 145; 63
151 crane/http/api/data/match/{match_id} data.group_N[].sts.placement string 1; 0
152 crane/http/api/data/match/{match_id} data.group_N[].sts.punishment string 1; 0
153 crane/http/api/data/match/{match_id} data.group_N[].sts.rank string 3251068; 1410250; 2717215
154 crane/http/api/data/match/{match_id} data.group_N[].sts.origin_rank string 2293251; 3241507; 1358912
155 crane/http/api/data/match/{match_id} data.group_N[].sts.season string 2025s2; 2025s3; 2025s4
156 crane/http/api/data/match/{match_id} data.group_N[].sts.special_data string ; {"match_data":[{"is_win":-1,"match_id":"g161-20250214164503716847890","match_status":0,"change_elo":-100.14724769911413},{"is_win":1,"match_id":"g161-20250214172202090993964","match_status":0,"change_elo":160.71161885810778},{"is_win":0,"match_id":"","match_status":0,"change_elo":0},{"is_win":0,"match_id":"","match_status":0,"change_elo":0},{"is_win":0,"match_id":"","match_status":0,"change_elo":0}]}; {"match_data":[{"is_win":-1,"match_id":"g161-20250214164503716847890","match_status":0,"change_elo":-56.99773123078694},{"is_win":1,"match_id":"g161-20250214172202090993964","match_status":0,"change_elo":120.48283784034022},{"is_win":0,"match_id":"","match_status":0,"change_elo":0},{"is_win":0,"match_id":"","match_status":0,"change_elo":0},{"is_win":0,"match_id":"","match_status":0,"change_elo":0}]}
157 crane/http/api/data/match/{match_id} data.group_N[].sts.uid <5eid>, string 14026928; 15478597; 21610332
158 crane/http/api/data/match/{match_id} data.group_N[].level_info.level_id int 103; 108; 104
159 crane/http/api/data/match/{match_id} data.group_N[].level_info.level_name string C; E-; B-
160 crane/http/api/data/match/{match_id} data.group_N[].level_info.level_type int 2; 1; 0
161 crane/http/api/data/match/{match_id} data.group_N[].level_info.star_num int 0
162 crane/http/api/data/match/{match_id} data.group_N[].level_info.origin_star_num int 0
163 crane/http/api/data/match/{match_id} data.group_N[].level_info.dragon_flag int 0
164 crane/http/api/data/match/{match_id} data.group_N[].level_info.deduct_data.all_deduct_elo int 0
165 crane/http/api/data/match/{match_id} data.group_N[].level_info.deduct_data.deduct_remain_elo int 0
166 crane/http/api/data/match/{match_id} data.group_N[].level_info.deduct_data.deduct_elo int 0
167 crane/http/api/data/match/{match_id} data.group_N[].level_info.special_data[].is_win int 1; 0; -1
168 crane/http/api/data/match/{match_id} data.group_N[].level_info.special_data[].match_id string ; g161-n-20250103203331443454143; g161-20250214164503716847890
169 crane/http/api/data/match/{match_id} data.group_N[].level_info.special_data[].match_status int 2; 0
170 crane/http/api/data/match/{match_id} data.group_N[].level_info.special_data[].change_elo float, int -100.14724769911413; 120.48283784034022; 160.71161885810778
171 crane/http/api/data/match/{match_id} data.group_N[].level_info.match_status string 3; 2; 0
172 crane/http/api/data/match/{match_id} data.group_N[].level_info.match_flag string 32; 2; 3
173 crane/http/api/data/match/{match_id} data.group_N[].level_info.change_elo string -22.97; -36.73; -20.39
174 crane/http/api/data/match/{match_id} data.group_N[].level_info.origin_elo string 1214.69; 1490.09; 1777.88
175 crane/http/api/data/match/{match_id} data.group_N[].level_info.rank string 3251068; 1410250; 2717215
176 crane/http/api/data/match/{match_id} data.group_N[].level_info.origin_rank string ; 1444425; 1444424
177 crane/http/api/data/match/{match_id} data.group_N[].level_info.trigger_promotion int 0
178 crane/http/api/data/match/{match_id} data.group_N[].level_info.special_bo int 0
179 crane/http/api/data/match/{match_id} data.group_N[].level_info.rise_type int 0
180 crane/http/api/data/match/{match_id} data.group_N[].level_info.tie_status int 1; 0
181 crane/http/api/data/match/{match_id} data.group_N[].level_info.level_elo int 800; 1700; 1400
182 crane/http/api/data/match/{match_id} data.group_N[].level_info.max_level int 19; 30; 0
183 crane/http/api/data/match/{match_id} data.group_N[].level_info.origin_level_id int 103; 108; 105
184 crane/http/api/data/match/{match_id} data.group_N[].level_info.origin_match_total int 269; 145; 63
185 crane/http/api/data/match/{match_id} data.group_N[].level_info.star_info.change_small_star_num int 0
186 crane/http/api/data/match/{match_id} data.group_N[].level_info.star_info.origin_small_star_num int 0
187 crane/http/api/data/match/{match_id} data.group_N[].level_info.star_info.change_type int 0
188 crane/http/api/data/match/{match_id} data.group_N[].level_info.star_info.now_small_star_num int 0
189 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.uid <5eid>, int 14026928; 15478597; 21610332
190 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.username <5eid>, string Sonka; 午夜伤心忧郁玫瑰; _陆小果
191 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.uuid string e6f87d93-ea92-11ee-9ce2-ec0d9a495494; 857f1c11-49c8-11ef-ac9f-ec0d9a7185e0; 4d9e3561-c373-11ef-848e-506b4bfa3106
192 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.email string
193 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.area string ; 86; 852
194 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.mobile string
195 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.createdAt int 1711362715; 1688270111; 1676517088
196 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.updatedAt int 1767921452; 1768905111; 1767770760
197 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.uid <5eid>, int 14026928; 15478597; 21610332
198 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.domain <5eid>, string 123442; 1226wi4xw0ya; 15478597ldiutg
199 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.nickname string
200 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.avatarUrl string disguise/images/cf/b2/cfb285c3d8d1c905b648954e42dc8cb0.jpg; disguise/images/9d/94/9d94029776f802318860f1bbd19c3bca.jpg; prop/images/6f/c0/6fc0c147e94ea8b1432ed072c19b0991.png
201 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.avatarAuditStatus int 1; 0
202 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.rgbAvatarUrl string ; rgb_avatar/20230503/1fc76fccd31807fcb709d5d119522d32.rgb; rgb_avatar/20230803/d8b7ba92df98837791082ea3bcf6292b.rgb
203 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.photoUrl string
204 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.gender int 1; 0
205 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.birthday int 1141315200; 904233600; 1077638400
206 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.countryId string ; kr; bm
207 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.regionId string ; 620000; 450000
208 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.cityId string ; 360400; 451100
209 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.language string simplified-chinese;
210 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.recommendUrl string
211 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.groupId int 0
212 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.profile.regSource int 5; 4; 3
213 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.uid <5eid>, int 14026928; 15478597; 21610332
214 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.status int -4; -6; 0
215 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.expire int 0
216 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.cancellationStatus int 2; 0
217 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.newUser int 0
218 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.loginBannedTime int 1687524902; 1733207455; 0
219 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.anticheatType int 0
220 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.flagStatus1 string 32; 4224; 24704
221 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.anticheatStatus string 0
222 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.FlagHonor string 65548; 93196; 2162700
223 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.PrivacyPolicyStatus int 3; 4
224 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.status.csgoFrozenExptime int 1766231693; 1767001958; 1760438129
225 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.platformExp.uid <5eid>, int 14026928; 15478597; 21610332
226 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.platformExp.level int 22; 30; 25
227 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.platformExp.exp int 12641; 32004; 13776
228 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.steam.uid <5eid>, int 14026928; 15478597; 21610332
229 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.steam.steamId <steamid> 76561198812383596; 76561199812085195; 76561199187871084
230 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.steam.steamAccount string
231 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.steam.tradeUrl string
232 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.steam.rentSteamId string
233 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.trusted.uid <5eid>, int 14026928; 15478597; 21610332
234 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.trusted.credit int 2550; 2990; 2033
235 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.trusted.creditLevel int 3; 1; 4
236 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.trusted.score int 100000; 97059; 96082
237 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.trusted.status int 1; 0
238 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.trusted.creditStatus int 1; 2
239 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.certify.uid <5eid>, int 14026928; 15478597; 21610332
240 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.certify.idType int 0
241 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.certify.status int 1; 0
242 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.certify.age int 20; 22; 25
243 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.certify.realName string
244 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.certify.auditStatus int 1
245 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.certify.gender int 1; 0
246 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.uid <5eid>, int 14026928; 15478597; 21610332
247 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.type int 0
248 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.extras string
249 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.status int 0
250 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.slogan string
251 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.slogan_ext string
252 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.live_url string
253 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.identity.live_type int 0
254 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.usernameAuditStatus int 1
255 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.Accid string 263d37a4e1f87bce763e0d1b8ec03982; 07809f60e739d9c47648f4acda66667d; 879462b5de38dce892033adc138dec22
256 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.teamID int 99868; 132671; 117796
257 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.domain <5eid>, string 123442; 1226wi4xw0ya; 15478597ldiutg
258 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_data.trumpetCount int 2; 23; 1
259 crane/http/api/data/match/{match_id} data.group_N[].user_info.plus_info.is_plus int 1; 0
260 crane/http/api/data/match/{match_id} data.group_N[].user_info.plus_info.plus_icon string images/act/e9cf57699303d9f6b18e465156fc6291.png; images/act/dae5c4cb98ceb6eeb1700f63c9ed14b7.png; images/act/09bbeb0f83a2f13419a0d75ac93e8a0c.png
261 crane/http/api/data/match/{match_id} data.group_N[].user_info.plus_info.plus_icon_short string images/act/d53f3bd55c836e057af230e2a138e94a.png; images/act/b7e90458420245283d9878a1e92b3a74.png; images/act/49b525ee6f74f423f3c2f0f913289824.png
262 crane/http/api/data/match/{match_id} data.group_N[].user_info.plus_info.vip_level int 6; 5; 0
263 crane/http/api/data/match/{match_id} data.group_N[].user_info.plus_info.plus_grade int 6; 2; 4
264 crane/http/api/data/match/{match_id} data.group_N[].user_info.plus_info.growth_score int 540; 8196; 5458
265 crane/http/api/data/match/{match_id} data.group_N[].user_info.user_avatar_frame null None
266 crane/http/api/data/match/{match_id} data.group_N[].friend_relation int 0
267 crane/http/api/data/match/{match_id} data.level_list[].elo int 1000; 800; 900
268 crane/http/api/data/match/{match_id} data.level_list[].remark string 800-899; 700-799; 900-999
269 crane/http/api/data/match/{match_id} data.level_list[].level_id int 2; 5; 4
270 crane/http/api/data/match/{match_id} data.level_list[].level_name string E-; E+; N
271 crane/http/api/data/match/{match_id} data.level_list[].elo_type int 9
272 crane/http/api/data/match/{match_id} data.level_list[].group_id int 2; 5; 4
273 crane/http/api/data/match/{match_id} data.level_list[].level_image string
274 crane/http/api/data/match/{match_id} data.level_list[].rise_type int 0
275 crane/http/api/data/match/{match_id} data.level_list[].shelves_status int 1
276 crane/http/api/data/match/{match_id} data.room_card.id string 310; 1326; 1309
277 crane/http/api/data/match/{match_id} data.room_card.category string 48; 0
278 crane/http/api/data/match/{match_id} data.room_card.describe string ; PLUS1专属房间卡片; 灵动小5房间卡片
279 crane/http/api/data/match/{match_id} data.room_card.name string ; PLUS1专属房间卡片; 赛博少女
280 crane/http/api/data/match/{match_id} data.room_card.propTemplateId string 133841; 134304; 1001
281 crane/http/api/data/match/{match_id} data.room_card.getWay string
282 crane/http/api/data/match/{match_id} data.room_card.onShelf int 0
283 crane/http/api/data/match/{match_id} data.room_card.shelfAt string
284 crane/http/api/data/match/{match_id} data.room_card.getButton int 0
285 crane/http/api/data/match/{match_id} data.room_card.getUrl string
286 crane/http/api/data/match/{match_id} data.room_card.attrs.flagAnimation string ; https://oss-arena.5eplay.com/prop/videos/ba/23/ba2356a47ba93454a2de62c6fb817f82.avif; https://oss-arena.5eplay.com/prop/videos/59/79/59795c76433dfcadad8e6c02627e7d0f.avif
287 crane/http/api/data/match/{match_id} data.room_card.attrs.flagAnimationTime string ; 2
288 crane/http/api/data/match/{match_id} data.room_card.attrs.flagViewUrl string https://oss-arena.5eplay.com/prop/images/49/36/49365bf9f2b7fe3ac6a7ded3656e092a.png; https://oss-arena.5eplay.com/prop/images/77/8c/778c698eb83d864e49e8a90bc8837a50.png; https://oss-arena.5eplay.com/prop/images/09/a9/09a93ce3f1476005f926298491188b21.png
289 crane/http/api/data/match/{match_id} data.room_card.attrs.flagViewVideo string ; https://oss-arena.5eplay.com/prop/videos/6a/ae/6aaee03bbd40a093e5c00d6babe8e276.avif; https://oss-arena.5eplay.com/prop/videos/11/e8/11e8446dcd0202316605b08ab0b35466.avif
290 crane/http/api/data/match/{match_id} data.room_card.attrs.flagViewVideoTime string ; 5; 2
291 crane/http/api/data/match/{match_id} data.room_card.attrs.getWay string 升级至PLUS1级获取; 购买DANK1NG联名装扮获得; CS全新版本上线活动获得
292 crane/http/api/data/match/{match_id} data.room_card.attrs.mallJumpLink string
293 crane/http/api/data/match/{match_id} data.room_card.attrs.matchViewUrlLeft string https://oss-arena.5eplay.com/prop/images/13/fd/13fdb6d3b8dfaca3e8cd4987acc45606.png; https://oss-arena.5eplay.com/prop/images/1a/3a/1a3a7725e7bcb19f5a42858160e78bf8.png; https://oss-arena.5eplay.com/prop/images/f9/36/f9366f00cf41b3609a5b52194bf3b309.png
294 crane/http/api/data/match/{match_id} data.room_card.attrs.matchViewUrlRight string https://oss-arena.5eplay.com/prop/images/a9/da/a9da623d19cff27141cf6335507071ff.png; https://oss-arena.5eplay.com/prop/images/fa/45/fa45de3775d1bb75a6456c75ea454147.png; https://oss-arena.5eplay.com/prop/images/0c/f6/0cf657f3461dbd312a1083f546db9e54.png
295 crane/http/api/data/match/{match_id} data.room_card.attrs.mvpSettleAnimation string https://oss-arena.5eplay.com/dress/room_card/9e2ab6983d4ed9a6d23637abd9cd2152.mp4; https://oss-arena.5eplay.com/prop/videos/38/3e/383ec8198005d46da7194252353e7cf4.mp4; https://oss-arena.5eplay.com/prop/videos/14/05/14055e4e7cb184edb5f9849031e97231.mp4
296 crane/http/api/data/match/{match_id} data.room_card.attrs.mvpSettleColor string #9f1dea; #1ab5c6; #c89c68
297 crane/http/api/data/match/{match_id} data.room_card.attrs.mvpSettleViewAnimation string https://oss-arena.5eplay.com/dress/room_card/9e2ab6983d4ed9a6d23637abd9cd2152.mp4; https://oss-arena.5eplay.com/prop/videos/82/52/82526d004e9d0f41f3a3e7367b253003.mp4; https://oss-arena.5eplay.com/prop/videos/d2/bc/d2bc06fcc9e997c1d826537c145ea38e.mp4
298 crane/http/api/data/match/{match_id} data.room_card.attrs.pcImg string https://oss-arena.5eplay.com/prop/images/1a/47/1a47dda552d9501004d9043f637406d5.png; https://oss-arena.5eplay.com/prop/images/a1/e6/a1e6656596228734258d74b727a1aa48.png; https://oss-arena.5eplay.com/prop/images/d5/45/d545c6caf716a99a6725d24e37098078.png
299 crane/http/api/data/match/{match_id} data.room_card.attrs.sort int 1; 2
300 crane/http/api/data/match/{match_id} data.room_card.attrs.templateId int 2029; 1663; 2050
301 crane/http/api/data/match/{match_id} data.room_card.attrs.rarityLevel int 3; 4; 2
302 crane/http/api/data/match/{match_id} data.room_card.attrs.sourceId int 3; 11; 4
303 crane/http/api/data/match/{match_id} data.room_card.displayStatus int 0
304 crane/http/api/data/match/{match_id} data.room_card.sysType int 0
305 crane/http/api/data/match/{match_id} data.room_card.createdAt string
306 crane/http/api/data/match/{match_id} data.room_card.updatedAt string
307 crane/http/api/data/match/{match_id} data.round_sfui_type[] string 2; 5; 4
308 crane/http/api/data/match/{match_id} data.user_stats.map_level.map_exp int 0
309 crane/http/api/data/match/{match_id} data.user_stats.map_level.add_exp int 0
310 crane/http/api/data/match/{match_id} data.user_stats.plat_level.plat_level_exp int 0
311 crane/http/api/data/match/{match_id} data.user_stats.plat_level.add_exp int 0
312 crane/http/api/data/match/{match_id} data.group_1_team_info.team_id string
313 crane/http/api/data/match/{match_id} data.group_1_team_info.team_name string
314 crane/http/api/data/match/{match_id} data.group_1_team_info.logo_url string
315 crane/http/api/data/match/{match_id} data.group_1_team_info.team_domain string
316 crane/http/api/data/match/{match_id} data.group_1_team_info.team_tag string
317 crane/http/api/data/match/{match_id} data.group_2_team_info.team_id string
318 crane/http/api/data/match/{match_id} data.group_2_team_info.team_name string
319 crane/http/api/data/match/{match_id} data.group_2_team_info.logo_url string
320 crane/http/api/data/match/{match_id} data.group_2_team_info.team_domain string
321 crane/http/api/data/match/{match_id} data.group_2_team_info.team_tag string
322 crane/http/api/data/match/{match_id} data.treat_info.user_id <5eid>, int 13048069; 21150835
323 crane/http/api/data/match/{match_id} data.treat_info.user_data.uid <5eid>, int 13048069; 21150835
324 crane/http/api/data/match/{match_id} data.treat_info.user_data.username string 熊出没之深情熊二; Royc灬Kerat丶
325 crane/http/api/data/match/{match_id} data.treat_info.user_data.uuid string c9caad5c-a9b3-11ef-848e-506b4bfa3106; 83376211-5c36-11ed-9ce2-ec0d9a495494
326 crane/http/api/data/match/{match_id} data.treat_info.user_data.email string
327 crane/http/api/data/match/{match_id} data.treat_info.user_data.area string 86
328 crane/http/api/data/match/{match_id} data.treat_info.user_data.mobile string
329 crane/http/api/data/match/{match_id} data.treat_info.user_data.createdAt int 1667562471; 1732377512
330 crane/http/api/data/match/{match_id} data.treat_info.user_data.updatedAt int 1768911939; 1768904695
331 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.uid <5eid>, int 13048069; 21150835
332 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.domain string 13048069yf1jto; 1123rqi1bfha
333 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.nickname string
334 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.avatarUrl string prop/images/3d/c4/3dc4259c07c31adb2439f7acbf1e565f.png; disguise/images/0e/84/0e84fdbb1da54953f1985bfb206604a5.png
335 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.avatarAuditStatus int 0
336 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.rgbAvatarUrl string ; rgb_avatar/20221129/f1ba34afe43c4fa38fd7dd129b0dc303.rgb
337 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.photoUrl string
338 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.gender int 1; 0
339 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.birthday int 0
340 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.countryId string ; cn
341 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.regionId string
342 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.cityId string
343 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.language string simplified-chinese;
344 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.recommendUrl string
345 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.groupId int 0
346 crane/http/api/data/match/{match_id} data.treat_info.user_data.profile.regSource int 4; 0
347 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.uid <5eid>, int 13048069; 21150835
348 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.status int 0
349 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.expire int 0
350 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.cancellationStatus int 0
351 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.newUser int 0
352 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.loginBannedTime int 0
353 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.anticheatType int 0
354 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.flagStatus1 string 128
355 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.anticheatStatus string 0
356 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.FlagHonor string 1178636; 65548
357 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.PrivacyPolicyStatus int 4
358 crane/http/api/data/match/{match_id} data.treat_info.user_data.status.csgoFrozenExptime int 1767707372; 1765545847
359 crane/http/api/data/match/{match_id} data.treat_info.user_data.platformExp.uid <5eid>, int 13048069; 21150835
360 crane/http/api/data/match/{match_id} data.treat_info.user_data.platformExp.level int 29
361 crane/http/api/data/match/{match_id} data.treat_info.user_data.platformExp.exp int 26803; 26522
362 crane/http/api/data/match/{match_id} data.treat_info.user_data.steam.uid <5eid>, int 13048069; 21150835
363 crane/http/api/data/match/{match_id} data.treat_info.user_data.steam.steamId <steamid> 76561199192775594; 76561198290113126
364 crane/http/api/data/match/{match_id} data.treat_info.user_data.steam.steamAccount string
365 crane/http/api/data/match/{match_id} data.treat_info.user_data.steam.tradeUrl string
366 crane/http/api/data/match/{match_id} data.treat_info.user_data.steam.rentSteamId string
367 crane/http/api/data/match/{match_id} data.treat_info.user_data.trusted.uid <5eid>, int 13048069; 21150835
368 crane/http/api/data/match/{match_id} data.treat_info.user_data.trusted.credit int 2200; 5919
369 crane/http/api/data/match/{match_id} data.treat_info.user_data.trusted.creditLevel int 4
370 crane/http/api/data/match/{match_id} data.treat_info.user_data.trusted.score int 100000
371 crane/http/api/data/match/{match_id} data.treat_info.user_data.trusted.status int 1
372 crane/http/api/data/match/{match_id} data.treat_info.user_data.trusted.creditStatus int 1
373 crane/http/api/data/match/{match_id} data.treat_info.user_data.certify.uid <5eid>, int 13048069; 21150835
374 crane/http/api/data/match/{match_id} data.treat_info.user_data.certify.idType int 0
375 crane/http/api/data/match/{match_id} data.treat_info.user_data.certify.status int 1
376 crane/http/api/data/match/{match_id} data.treat_info.user_data.certify.age int 23; 42
377 crane/http/api/data/match/{match_id} data.treat_info.user_data.certify.realName string
378 crane/http/api/data/match/{match_id} data.treat_info.user_data.certify.auditStatus int 1
379 crane/http/api/data/match/{match_id} data.treat_info.user_data.certify.gender int 1
380 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.uid <5eid>, int 13048069; 21150835
381 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.type int 0
382 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.extras string
383 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.status int 0
384 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.slogan string
385 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.slogan_ext string
386 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.live_url string
387 crane/http/api/data/match/{match_id} data.treat_info.user_data.identity.live_type int 0
388 crane/http/api/data/match/{match_id} data.treat_info.user_data.usernameAuditStatus int 1
389 crane/http/api/data/match/{match_id} data.treat_info.user_data.Accid string 57cd6b98be64949589a6cecf7d258cd1; d0d986c392c55c5d422fd2c46e4d6318
390 crane/http/api/data/match/{match_id} data.treat_info.user_data.teamID int 0
391 crane/http/api/data/match/{match_id} data.treat_info.user_data.domain string 13048069yf1jto; 1123rqi1bfha
392 crane/http/api/data/match/{match_id} data.treat_info.user_data.trumpetCount int 3; 2442
393 crane/http/api/data/match/{match_id} data.season_type int 0
394 crane/http/api/data/match/{match_id} code int 0
395 crane/http/api/data/match/{match_id} message string 操作成功
396 crane/http/api/data/match/{match_id} status bool True
397 crane/http/api/data/match/{match_id} timestamp int 1768931731; 1768931718; 1768931708
398 crane/http/api/data/match/{match_id} trace_id string 8ae4feeb19cc4ed3a24a8a00f056d023; 19582ac94190e3baff795cff50c7a6f3; 87794472a94e5e40be8e12bd116dad55
399 crane/http/api/data/match/{match_id} success bool True
400 crane/http/api/data/match/{match_id} errcode int 0
401 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.fd_ct int 2; 4; 3
402 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.fd_t int 2; 4; 3
403 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.kast float, int 0.7; 0.65; 0.48
404 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.awp_kill int 2; 5; 4
405 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.awp_kill_ct int 5; 4; 3
406 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.awp_kill_t int 2; 5; 4
407 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.damage_stats int 3; 5; 50
408 crane/http/api/data/vip_plus_match_data/{match_id} data.<steamid>.damage_receive int 0
409 crane/http/api/data/vip_plus_match_data/{match_id} code int 0
410 crane/http/api/data/vip_plus_match_data/{match_id} message string 操作成功
411 crane/http/api/data/vip_plus_match_data/{match_id} status bool True
412 crane/http/api/data/vip_plus_match_data/{match_id} timestamp int 1768931714; 1768931732; 1768931710
413 crane/http/api/data/vip_plus_match_data/{match_id} trace_id string cff29d5dcdd6285b80d11bbb4a8a7da0; 6e7c0c0590b0e561c6c4c8d935ebb02c; 97c1377302559a8f5e01aedfeb208751
414 crane/http/api/data/vip_plus_match_data/{match_id} success bool True
415 crane/http/api/data/vip_plus_match_data/{match_id} errcode int 0
416 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].round int 2; 5; 4
417 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].t_money_group int 3; 1; 4
418 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].ct_money_group int 3; 1; 4
419 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].win_reason int 2; 5; 4
420 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].bron_equipment.<steamid>[].Money int 400; 200; 2900
421 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].bron_equipment.<steamid>[].WeaponName string weapon_flashbang; weapon_tec9; weapon_hegrenade
422 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].bron_equipment.<steamid>[].Weapon int 22; 33; 37
423 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].player_t_score.<steamid> float, int -21.459999999999997; -16.640000000000004; 4
424 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].player_ct_score.<steamid> float, int 17.099999999999994; 15.120000000000001; 27.507999999999996
425 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].player_bron_crash.<steamid> int 4200; 3900; 800
426 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].begin_ts string 2026-01-18T19:57:29+08:00; 2026-01-18T19:59:18+08:00; 2026-01-18T19:55:55+08:00
427 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].sfui_event.sfui_type int 2; 5; 4
428 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].sfui_event.score_ct int 2; 5; 4
429 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].sfui_event.score_t int 2; 10; 3
430 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].end_ts string 2026-01-18T19:54:37+08:00; 2026-01-18T19:57:22+08:00; 2026-01-18T19:59:11+08:00
431 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].ts_real string 0001-01-01T00:00:00Z; 2026-01-18T19:54:06+08:00; 2026-01-18T19:54:04+08:00
432 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].ts int 45; 48; 46
433 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].t_num int 2; 5; 4
434 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].ct_num int 2; 5; 4
435 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].event_type int 3; 1; 4
436 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.Ts string 2026-01-18T19:54:06+08:00; 2026-01-18T19:54:04+08:00; 2026-01-18T19:53:57+08:00
437 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.Killer <steamid> 76561199787406643; 76561199032002725; 76561199078250590
438 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.Victim <steamid> 76561199388433802; 76561199032002725; 76561199250737526
439 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.Weapon int 6; 7; 5
440 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.KillerTeam int 1; 2
441 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.KillerBot bool False
442 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.VictimBot bool False
443 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.WeaponName string usp_silencer; deagle; famas
444 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.Headshot bool False; True
445 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.Penetrated bool False; True
446 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.ThroughSmoke bool False; True
447 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.NoScope bool False; True
448 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.AttackerBlind bool False; True
449 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].kill_event.Attackerinair bool False
450 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].twin float, int 0.143; 0.341; 0.557
451 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].c_twin float, int 0.44299999999999995; 0.471; 0.659
452 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].twin_change float, int -0.21600000000000003; -0.19800000000000004; 0.19899999999999995
453 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].c_twin_change float, int 0.21600000000000003; 0.19800000000000004; 0.17099999999999993
454 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].killer_score_change.<steamid>.score float, int 17.099999999999994; 19.899999999999995; 19.800000000000004
455 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].victim_score_change.<steamid>.score float, int -15.8; -19.899999999999995; -21.6
456 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].assist_killer_score_change.<steamid>.score float 2.592; 6.63; 6.45
457 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].trade_score_change.<steamid>.score float 2.2100000000000004; 3.16; 3.66
458 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].flash_assist_killer_score_change.<steamid>.score float 1.1520000000000001; 2.9850000000000003; 1.5299999999999996
459 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].protect_gun_player_score_change.<steamid>.score float 5.8999999999999995; 7.1000000000000005
460 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].protect_gun_enemy_score_change.<steamid>.score float -1.18; -1.4200000000000002
461 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].disconnect_player_score_change null None
462 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].disconnect_comp_score_change null None
463 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].round_end_fixed_score_change.<steamid>.score float, int 20; -0.6000000000000005; -100
464 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].show_event[].win_reason int 2; 5; 4
465 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].side_info.ct[] <steamid> 76561199032002725; 76561199078250590; 76561199076109761
466 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_stat[].side_info.t[] <steamid> 76561199787406643; 76561199388433802; 76561199250737526
467 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.player_scores.<steamid> float 12.491187500000002; 1.5764999999999993; 2.073937500000001
468 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.player_t_scores.<steamid> float 19.06; 6.3349999999999955; -8.872500000000002
469 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.player_ct_scores.<steamid> float -0.009666666666665455; 10.301583333333335; -2.9330833333333324
470 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.round_total int 18; 30; 21
471 crane/http/api/match/leetify_rating/{match_id} data.leetify_data.player_round_scores.<steamid>.<round_n> float, int 32.347; -1.100000000000001; 20.040000000000006
472 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.uid <5eid>, int 14889445; 14869396; 14888575
473 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.uuid string 13f7dc52-ea7c-11ed-9ce2-ec0d9a495494; e74f23a3-e8ae-11ed-9ce2-ec0d9a495494; 7ced32f8-ea70-11ed-9ce2-ec0d9a495494
474 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.username string 刚拉; R1nging; RRRTINA
475 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.nickname string
476 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.reg_date int 1683007881; 1683007342; 1683200437
477 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.username_spam_status int 1
478 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.steamid_64 <steamid> 76561199032002725; 76561199078250590; 76561199076109761
479 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.avatar_url string disguise/images/6f/89/6f89b22633cb95df1754fd30573c5ad6.png; disguise/images/09/96/09961ea8fc45bed1c60157055a4c05c5.jpg; disguise/images/5d/41/5d4182b66a5004a974aee7501873164b.jpg
480 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.gender int 1; 0
481 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.country_id string ; cn
482 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.language string ; simplified-chinese
483 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.domain string rrrtina; 14869396o9jm5g; dxw123452
484 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.credit int 0
485 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.trusted_score int 0
486 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.trusted_status int 0
487 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.plus_info null None
488 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.region int 0
489 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.province int 0
490 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.province_name string
491 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.region_name string
492 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.college_id int 0
493 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.status int 0
494 crane/http/api/match/leetify_rating/{match_id} data.uinfo_dict.<steamid>.identity null None
495 crane/http/api/match/leetify_rating/{match_id} code int 0
496 crane/http/api/match/leetify_rating/{match_id} message string 操作成功
497 crane/http/api/match/leetify_rating/{match_id} status bool True
498 crane/http/api/match/leetify_rating/{match_id} timestamp int 1768833830; 1768833808; 1768833806
499 crane/http/api/match/leetify_rating/{match_id} trace_id string 376e200283d19770bdef6dacf260f40f; a7dd6602d3aedb3017bb37727b5be75a; dab4013545b5581fbb089fb5c273d0a9
500 crane/http/api/match/leetify_rating/{match_id} success bool True
501 crane/http/api/match/leetify_rating/{match_id} errcode int 0
502 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].attacker.name string 5E-Player 我有必胜卡组; 5E-Player 青青C原懒大王w; 5E-Player xiezhongxie1
503 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].attacker.pos.x int 734; 999; 1170
504 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].attacker.pos.y int 125; -77; -772
505 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].attacker.pos.z int 0
506 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].attacker.steamid_64 <steamid> 76561198330488905; 76561199032002725; 76561199076109761
507 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].attacker.team int 1; 2
508 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].attackerblind bool False; True
509 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].headshot bool False; True
510 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].noscope bool False; True
511 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].pasttime int 45; 20; 24
512 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].penetrated bool False; True
513 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].throughsmoke bool False; True
514 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].victim.name <5eid>, string 5E-Player 青青C原懒大王w; 5E-Player 午夜伤心忧郁玫瑰; 5E-Player RRRTINA
515 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].victim.pos.x int 1218; 706; 1298
516 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].victim.pos.y int 627; 587; 219
517 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].victim.pos.z int 0
518 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].victim.steamid_64 <steamid>, string 76561199482118960; 76561199812085195; 76561199207654712
519 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].victim.team int 1; 2
520 crane/http/api/match/round/{match_id} data.round_list[].all_kill[].weapon string usp_silencer; mag7; famas
521 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].attacker.name string 5E-Player 我有必胜卡组; 5E-Player 青青C原懒大王w; 5E-Player xiezhongxie1
522 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].attacker.pos.x int 734; 999; 397
523 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].attacker.pos.y int 149; 125; -77
524 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].attacker.pos.z int 0
525 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].attacker.steamid_64 <steamid> 76561198330488905; 76561199032002725; 76561199076109761
526 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].attacker.team int 1; 2
527 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].attackerblind bool False; True
528 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].headshot bool False; True
529 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].noscope bool False; True
530 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].pasttime int 24; 57; 20
531 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].penetrated bool False; True
532 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].throughsmoke bool False; True
533 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].victim.name <5eid>, string 5E-Player 青青C原懒大王w; 5E-Player 午夜伤心忧郁玫瑰; 5E-Player _陆小果
534 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].victim.pos.x int 1218; 706; 1298
535 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].victim.pos.y int 627; 587; 219
536 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].victim.pos.z int 0
537 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].victim.steamid_64 <steamid>, string 76561198812383596; 76561199812085195; 76561199187871084
538 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].victim.team int 1; 2
539 crane/http/api/match/round/{match_id} data.round_list[].kill.<steamid>[].weapon string usp_silencer; mag7; famas
540 crane/http/api/match/round/{match_id} data.round_list[].c4_event[].event_name string planted_c4
541 crane/http/api/match/round/{match_id} data.round_list[].c4_event[].location string
542 crane/http/api/match/round/{match_id} data.round_list[].c4_event[].name string 5E-Player 我有必胜卡组; 5E-Player RRRTINA; 5E-Player 俺有鱼鱼蒸
543 crane/http/api/match/round/{match_id} data.round_list[].c4_event[].pasttime int 45; 30; 31
544 crane/http/api/match/round/{match_id} data.round_list[].c4_event[].steamid_64 <steamid> 76561198330488905; 76561199812085195; 76561199207654712
545 crane/http/api/match/round/{match_id} data.round_list[].current_score.ct int 2; 10; 1
546 crane/http/api/match/round/{match_id} data.round_list[].current_score.final_round_time int 68; 79; 63
547 crane/http/api/match/round/{match_id} data.round_list[].current_score.pasttime int 57; 47; 62
548 crane/http/api/match/round/{match_id} data.round_list[].current_score.t int 2; 5; 4
549 crane/http/api/match/round/{match_id} data.round_list[].current_score.type int 2; 5; 4
550 crane/http/api/match/round/{match_id} data.round_list[].death_list[] <steamid>, string 76561198812383596; 76561199812085195; 76561199187871084
551 crane/http/api/match/round/{match_id} data.round_list[].equiped.<steamid>[] string usp_silencer; kevlar(100); smokegrenade
552 crane/http/api/match/round/{match_id} data.round_list[].equiped.[] string
553 crane/http/api/match/round/{match_id} data.weapon_list.defuser[] string defuser
554 crane/http/api/match/round/{match_id} data.weapon_list.item[] string incgrenade; flashbang; molotov
555 crane/http/api/match/round/{match_id} data.weapon_list.main_weapon[] string sg556; awp; ssg08
556 crane/http/api/match/round/{match_id} data.weapon_list.other_item[] string kevlar; helmet
557 crane/http/api/match/round/{match_id} data.weapon_list.secondary_weapon[] string usp_silencer; deagle; glock
558 crane/http/api/match/round/{match_id} code int 0
559 crane/http/api/match/round/{match_id} message string 操作成功
560 crane/http/api/match/round/{match_id} status bool True
561 crane/http/api/match/round/{match_id} timestamp int 1768931714; 1768931731; 1768931710
562 crane/http/api/match/round/{match_id} trace_id string c2ee4f45abd89f1c90dc1cc390d21d33; f85069de4d785710dd55301334ff03c0; 98335f4087c76de69e8aeda3ca767d6f
563 crane/http/api/match/round/{match_id} success bool True
564 crane/http/api/match/round/{match_id} errcode int 0
+708
View File
@@ -0,0 +1,708 @@
## Category: `crane/http/api/data/match/{match_id}`
**Total Requests**: 179
- **data** (dict)
- **has_side_data_and_rating2** (bool, e.g. True)
- **main** (dict)
- **demo_url** (string, e.g. )
- **end_time** (int, e.g. 1739528619)
- **game_mode** (int, e.g. 6)
- **game_name** (string, e.g. )
- **group1_all_score** (int, e.g. 10)
- **group1_change_elo** (int, e.g. 0)
- **group1_fh_role** (int, e.g. 1)
- **group1_fh_score** (int, e.g. 6)
- **group1_origin_elo** (float, int, e.g. 1628.1)
- **group1_sh_role** (int, e.g. 0)
- **group1_sh_score** (int, e.g. 6)
- **group1_tid** (int, e.g. 0)
- **group1_uids** (string, e.g. 14869472,14888575,1326932,14869396,14889445)
- **group2_all_score** (int, e.g. 6)
- **group2_change_elo** (int, e.g. 0)
- **group2_fh_role** (int, e.g. 0)
- **group2_fh_score** (int, e.g. 6)
- **group2_origin_elo** (float, int, e.g. 1617.02)
- **group2_sh_role** (int, e.g. 1)
- **group2_sh_score** (int, e.g. 6)
- **group2_tid** (int, e.g. 0)
- **group2_uids** (string, e.g. 7866482,7976557,13918176,7998628,18857497)
- **id** (int, e.g. 232025624)
- **knife_winner** (int, e.g. 0)
- **knife_winner_role** (int, e.g. 0)
- **location** (string, e.g. hz)
- **location_full** (string, e.g. sh_pug-low)
- **map** (string, e.g. de_nuke)
- **map_desc** (string, e.g. 阿努比斯)
- **match_code** (string, e.g. g161-20250215211846894242128)
- **match_mode** (int, e.g. 9)
- **match_winner** (int, e.g. 1)
- **most_1v2_uid** (<5eid>, int, e.g. 14869396)
- **most_assist_uid** (<5eid>, int, e.g. 14869396)
- **most_awp_uid** (<5eid>, int, e.g. 12501578)
- **most_end_uid** (<5eid>, int, e.g. 12501578)
- **most_first_kill_uid** (<5eid>, int, e.g. 18337753)
- **most_headshot_uid** (<5eid>, int, e.g. 17181895)
- **most_jump_uid** (<5eid>, int, e.g. 12501578)
- **mvp_uid** (<5eid>, int, e.g. 19535157)
- **round_total** (int, e.g. 24)
- **season** (string, e.g. 2025s2)
- **server_ip** (string, e.g. )
- **server_port** (string, e.g. 27015)
- **start_time** (int, e.g. 1739523090)
- **status** (int, e.g. 1)
- **waiver** (int, e.g. 0)
- **year** (int, e.g. 2026)
- **cs_type** (int, e.g. 0)
- **priority_show_type** (int, e.g. 3)
- **pug10m_show_type** (int, e.g. 1)
- **credit_match_status** (int, e.g. 1)
- **group_N** (list)
- *[Array Items]*
- **fight_any** (dict)
- **adr** (string, e.g. 106.58)
- **assist** (string, e.g. 2)
- **awp_kill** (string, e.g. 2)
- **benefit_kill** (string, e.g. 6)
- **day** (string, e.g. 20250218)
- **death** (string, e.g. 5)
- **defused_bomb** (string, e.g. 2)
- **end_1v1** (string, e.g. 2)
- **end_1v2** (string, e.g. 2)
- **end_1v3** (string, e.g. 2)
- **end_1v4** (string, e.g. 1)
- **end_1v5** (string, e.g. 1)
- **explode_bomb** (string, e.g. 2)
- **first_death** (string, e.g. 5)
- **first_kill** (string, e.g. 2)
- **flash_enemy** (string, e.g. 43)
- **flash_enemy_time** (string, e.g. 7)
- **flash_team** (string, e.g. 5)
- **flash_team_time** (string, e.g. 21)
- **flash_time** (string, e.g. 6)
- **game_mode** (string, e.g. 6)
- **group_id** (string, e.g. 1)
- **headshot** (string, e.g. 2)
- **hold_total** (string, e.g. 0)
- **id** (string, e.g. 1937230471)
- **is_highlight** (string, e.g. 1)
- **is_most_1v2** (string, e.g. 1)
- **is_most_assist** (string, e.g. 1)
- **is_most_awp** (string, e.g. 1)
- **is_most_end** (string, e.g. 1)
- **is_most_first_kill** (string, e.g. 1)
- **is_most_headshot** (string, e.g. 1)
- **is_most_jump** (string, e.g. 1)
- **is_mvp** (string, e.g. 1)
- **is_svp** (string, e.g. )
- **is_tie** (string, e.g. 1)
- **is_win** (string, e.g. 1)
- **jump_total** (string, e.g. 64)
- **kast** (string, e.g. 0.82)
- **kill** (string, e.g. 14)
- **kill_1** (string, e.g. 5)
- **kill_2** (string, e.g. 2)
- **kill_3** (string, e.g. 2)
- **kill_4** (string, e.g. 3)
- **kill_5** (string, e.g. 2)
- **map** (string, e.g. de_nuke)
- **match_code** (string, e.g. g161-20250215211846894242128)
- **match_mode** (string, e.g. 9)
- **match_team_id** (string, e.g. 2)
- **match_time** (string, e.g. 1739625526)
- **per_headshot** (string, e.g. 0.44)
- **planted_bomb** (string, e.g. 2)
- **rating** (string, e.g. 0.89)
- **many_assists_cnt1** (string, e.g. 6)
- **many_assists_cnt2** (string, e.g. 2)
- **many_assists_cnt3** (string, e.g. 1)
- **many_assists_cnt4** (string, e.g. 1)
- **many_assists_cnt5** (string, e.g. 0)
- **perfect_kill** (string, e.g. 10)
- **assisted_kill** (string, e.g. 5)
- **rating2** (string, e.g. 1.24)
- **rating3** (string, e.g. 2.15)
- **revenge_kill** (string, e.g. 2)
- **round_total** (string, e.g. 17)
- **rws** (string, e.g. 8.41)
- **season** (string, e.g. 2025s2)
- **team_kill** (string, e.g. 1)
- **throw_harm** (string, e.g. 120)
- **throw_harm_enemy** (string, e.g. 10)
- **uid** (<5eid>, string, e.g. 14026928)
- **year** (string, e.g. 2026)
- **sts** (dict)
- **data_tips_detail** (int, e.g. -7)
- **challenge_status** (int, e.g. 1)
- **map_reward_status** (int, e.g. 1)
- **change_rank** (int, e.g. -423964)
- **origin_level_id** (int, e.g. 103)
- **rank_change_type** (int, e.g. 5)
- **star_num** (int, e.g. 0)
- **origin_star_num** (int, e.g. 0)
- **change_elo** (string, e.g. -22.97)
- **id** (string, e.g. 1930709265)
- **level_id** (string, e.g. 103)
- **match_code** (string, e.g. g161-20250215211846894242128)
- **match_flag** (string, e.g. 32)
- **match_mode** (string, e.g. 9)
- **match_status** (string, e.g. 3)
- **origin_elo** (string, e.g. 1214.69)
- **origin_match_total** (string, e.g. 269)
- **placement** (string, e.g. 1)
- **punishment** (string, e.g. 1)
- **rank** (string, e.g. 3251068)
- **origin_rank** (string, e.g. 2293251)
- **season** (string, e.g. 2025s2)
- **special_data** (string, e.g. )
- **uid** (<5eid>, string, e.g. 14026928)
- **level_info** (dict)
- **level_id** (int, e.g. 103)
- **level_name** (string, e.g. C)
- **level_type** (int, e.g. 2)
- **star_num** (int, e.g. 0)
- **origin_star_num** (int, e.g. 0)
- **dragon_flag** (int, e.g. 0)
- **deduct_data** (dict)
- **all_deduct_elo** (int, e.g. 0)
- **deduct_remain_elo** (int, e.g. 0)
- **deduct_elo** (int, e.g. 0)
- **special_data** (list, null)
- *[Array Items]*
- **is_win** (int, e.g. 1)
- **match_id** (string, e.g. )
- **match_status** (int, e.g. 2)
- **change_elo** (float, int, e.g. -100.14724769911413)
- **match_status** (string, e.g. 3)
- **match_flag** (string, e.g. 32)
- **change_elo** (string, e.g. -22.97)
- **origin_elo** (string, e.g. 1214.69)
- **rank** (string, e.g. 3251068)
- **origin_rank** (string, e.g. )
- **trigger_promotion** (int, e.g. 0)
- **special_bo** (int, e.g. 0)
- **rise_type** (int, e.g. 0)
- **tie_status** (int, e.g. 1)
- **level_elo** (int, e.g. 800)
- **max_level** (int, e.g. 19)
- **origin_level_id** (int, e.g. 103)
- **origin_match_total** (int, e.g. 269)
- **star_info** (dict)
- **change_small_star_num** (int, e.g. 0)
- **origin_small_star_num** (int, e.g. 0)
- **change_type** (int, e.g. 0)
- **now_small_star_num** (int, e.g. 0)
- **user_info** (dict)
- **user_data** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **username** (<5eid>, string, e.g. Sonka)
- **uuid** (string, e.g. e6f87d93-ea92-11ee-9ce2-ec0d9a495494)
- **email** (string, e.g. )
- **area** (string, e.g. )
- **mobile** (string, e.g. )
- **createdAt** (int, e.g. 1711362715)
- **updatedAt** (int, e.g. 1767921452)
- **profile** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **domain** (<5eid>, string, e.g. 123442)
- **nickname** (string, e.g. )
- **avatarUrl** (string, e.g. disguise/images/cf/b2/cfb285c3d8d1c905b648954e42dc8cb0.jpg)
- **avatarAuditStatus** (int, e.g. 1)
- **rgbAvatarUrl** (string, e.g. )
- **photoUrl** (string, e.g. )
- **gender** (int, e.g. 1)
- **birthday** (int, e.g. 1141315200)
- **countryId** (string, e.g. )
- **regionId** (string, e.g. )
- **cityId** (string, e.g. )
- **language** (string, e.g. simplified-chinese)
- **recommendUrl** (string, e.g. )
- **groupId** (int, e.g. 0)
- **regSource** (int, e.g. 5)
- **status** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **status** (int, e.g. -4)
- **expire** (int, e.g. 0)
- **cancellationStatus** (int, e.g. 2)
- **newUser** (int, e.g. 0)
- **loginBannedTime** (int, e.g. 1687524902)
- **anticheatType** (int, e.g. 0)
- **flagStatus1** (string, e.g. 32)
- **anticheatStatus** (string, e.g. 0)
- **FlagHonor** (string, e.g. 65548)
- **PrivacyPolicyStatus** (int, e.g. 3)
- **csgoFrozenExptime** (int, e.g. 1766231693)
- **platformExp** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **level** (int, e.g. 22)
- **exp** (int, e.g. 12641)
- **steam** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **steamId** (<steamid>, e.g. 76561198812383596)
- **steamAccount** (string, e.g. )
- **tradeUrl** (string, e.g. )
- **rentSteamId** (string, e.g. )
- **trusted** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **credit** (int, e.g. 2550)
- **creditLevel** (int, e.g. 3)
- **score** (int, e.g. 100000)
- **status** (int, e.g. 1)
- **creditStatus** (int, e.g. 1)
- **certify** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **idType** (int, e.g. 0)
- **status** (int, e.g. 1)
- **age** (int, e.g. 20)
- **realName** (string, e.g. )
- **uidList** (list)
- *[Array Items]*
- **auditStatus** (int, e.g. 1)
- **gender** (int, e.g. 1)
- **identity** (dict)
- **uid** (<5eid>, int, e.g. 14026928)
- **type** (int, e.g. 0)
- **extras** (string, e.g. )
- **status** (int, e.g. 0)
- **slogan** (string, e.g. )
- **identity_list** (list)
- *[Array Items]*
- **slogan_ext** (string, e.g. )
- **live_url** (string, e.g. )
- **live_type** (int, e.g. 0)
- **usernameAuditStatus** (int, e.g. 1)
- **Accid** (string, e.g. 263d37a4e1f87bce763e0d1b8ec03982)
- **teamID** (int, e.g. 99868)
- **domain** (<5eid>, string, e.g. 123442)
- **trumpetCount** (int, e.g. 2)
- **plus_info** (dict)
- **is_plus** (int, e.g. 1)
- **plus_icon** (string, e.g. images/act/e9cf57699303d9f6b18e465156fc6291.png)
- **plus_icon_short** (string, e.g. images/act/d53f3bd55c836e057af230e2a138e94a.png)
- **vip_level** (int, e.g. 6)
- **plus_grade** (int, e.g. 6)
- **growth_score** (int, e.g. 540)
- **user_avatar_frame** (null, e.g. None)
- **friend_relation** (int, e.g. 0)
- **level_list** (list, null)
- *[Array Items]*
- **elo** (int, e.g. 1000)
- **remark** (string, e.g. 800-899)
- **level_id** (int, e.g. 2)
- **level_name** (string, e.g. E-)
- **elo_type** (int, e.g. 9)
- **group_id** (int, e.g. 2)
- **level_image** (string, e.g. )
- **rise_type** (int, e.g. 0)
- **shelves_status** (int, e.g. 1)
- **room_card** (dict)
- **id** (string, e.g. 310)
- **category** (string, e.g. 48)
- **describe** (string, e.g. )
- **name** (string, e.g. )
- **propTemplateId** (string, e.g. 133841)
- **getWay** (string, e.g. )
- **onShelf** (int, e.g. 0)
- **shelfAt** (string, e.g. )
- **getButton** (int, e.g. 0)
- **getUrl** (string, e.g. )
- **attrs** (dict)
- **flagAnimation** (string, e.g. )
- **flagAnimationTime** (string, e.g. )
- **flagViewUrl** (string, e.g. https://oss-arena.5eplay.com/prop/images/49/36/49365bf9f2b7fe3ac6a7ded3656e092a.png)
- **flagViewVideo** (string, e.g. )
- **flagViewVideoTime** (string, e.g. )
- **getWay** (string, e.g. 升级至PLUS1级获取)
- **mallJumpLink** (string, e.g. )
- **matchViewUrlLeft** (string, e.g. https://oss-arena.5eplay.com/prop/images/13/fd/13fdb6d3b8dfaca3e8cd4987acc45606.png)
- **matchViewUrlRight** (string, e.g. https://oss-arena.5eplay.com/prop/images/a9/da/a9da623d19cff27141cf6335507071ff.png)
- **mvpSettleAnimation** (string, e.g. https://oss-arena.5eplay.com/dress/room_card/9e2ab6983d4ed9a6d23637abd9cd2152.mp4)
- **mvpSettleColor** (string, e.g. #9f1dea)
- **mvpSettleViewAnimation** (string, e.g. https://oss-arena.5eplay.com/dress/room_card/9e2ab6983d4ed9a6d23637abd9cd2152.mp4)
- **pcImg** (string, e.g. https://oss-arena.5eplay.com/prop/images/1a/47/1a47dda552d9501004d9043f637406d5.png)
- **sort** (int, e.g. 1)
- **templateId** (int, e.g. 2029)
- **rarityLevel** (int, e.g. 3)
- **sourceId** (int, e.g. 3)
- **displayStatus** (int, e.g. 0)
- **sysType** (int, e.g. 0)
- **createdAt** (string, e.g. )
- **updatedAt** (string, e.g. )
- **round_sfui_type** (list)
- *[Array Items]*
- **user_stats** (dict)
- **map_level** (dict)
- **map_exp** (int, e.g. 0)
- **add_exp** (int, e.g. 0)
- **plat_level** (dict)
- **plat_level_exp** (int, e.g. 0)
- **add_exp** (int, e.g. 0)
- **group_1_team_info** (dict)
- **team_id** (string, e.g. )
- **team_name** (string, e.g. )
- **logo_url** (string, e.g. )
- **team_domain** (string, e.g. )
- **team_tag** (string, e.g. )
- **group_2_team_info** (dict)
- **team_id** (string, e.g. )
- **team_name** (string, e.g. )
- **logo_url** (string, e.g. )
- **team_domain** (string, e.g. )
- **team_tag** (string, e.g. )
- **treat_info** (dict, null)
- **user_id** (<5eid>, int, e.g. 13048069)
- **user_data** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **username** (string, e.g. 熊出没之深情熊二)
- **uuid** (string, e.g. c9caad5c-a9b3-11ef-848e-506b4bfa3106)
- **email** (string, e.g. )
- **area** (string, e.g. 86)
- **mobile** (string, e.g. )
- **createdAt** (int, e.g. 1667562471)
- **updatedAt** (int, e.g. 1768911939)
- **profile** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **domain** (string, e.g. 13048069yf1jto)
- **nickname** (string, e.g. )
- **avatarUrl** (string, e.g. prop/images/3d/c4/3dc4259c07c31adb2439f7acbf1e565f.png)
- **avatarAuditStatus** (int, e.g. 0)
- **rgbAvatarUrl** (string, e.g. )
- **photoUrl** (string, e.g. )
- **gender** (int, e.g. 1)
- **birthday** (int, e.g. 0)
- **countryId** (string, e.g. )
- **regionId** (string, e.g. )
- **cityId** (string, e.g. )
- **language** (string, e.g. simplified-chinese)
- **recommendUrl** (string, e.g. )
- **groupId** (int, e.g. 0)
- **regSource** (int, e.g. 4)
- **status** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **status** (int, e.g. 0)
- **expire** (int, e.g. 0)
- **cancellationStatus** (int, e.g. 0)
- **newUser** (int, e.g. 0)
- **loginBannedTime** (int, e.g. 0)
- **anticheatType** (int, e.g. 0)
- **flagStatus1** (string, e.g. 128)
- **anticheatStatus** (string, e.g. 0)
- **FlagHonor** (string, e.g. 1178636)
- **PrivacyPolicyStatus** (int, e.g. 4)
- **csgoFrozenExptime** (int, e.g. 1767707372)
- **platformExp** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **level** (int, e.g. 29)
- **exp** (int, e.g. 26803)
- **steam** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **steamId** (<steamid>, e.g. 76561199192775594)
- **steamAccount** (string, e.g. )
- **tradeUrl** (string, e.g. )
- **rentSteamId** (string, e.g. )
- **trusted** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **credit** (int, e.g. 2200)
- **creditLevel** (int, e.g. 4)
- **score** (int, e.g. 100000)
- **status** (int, e.g. 1)
- **creditStatus** (int, e.g. 1)
- **certify** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **idType** (int, e.g. 0)
- **status** (int, e.g. 1)
- **age** (int, e.g. 23)
- **realName** (string, e.g. )
- **uidList** (list)
- *[Array Items]*
- **auditStatus** (int, e.g. 1)
- **gender** (int, e.g. 1)
- **identity** (dict)
- **uid** (<5eid>, int, e.g. 13048069)
- **type** (int, e.g. 0)
- **extras** (string, e.g. )
- **status** (int, e.g. 0)
- **slogan** (string, e.g. )
- **identity_list** (list)
- *[Array Items]*
- **slogan_ext** (string, e.g. )
- **live_url** (string, e.g. )
- **live_type** (int, e.g. 0)
- **usernameAuditStatus** (int, e.g. 1)
- **Accid** (string, e.g. 57cd6b98be64949589a6cecf7d258cd1)
- **teamID** (int, e.g. 0)
- **domain** (string, e.g. 13048069yf1jto)
- **trumpetCount** (int, e.g. 3)
- **season_type** (int, e.g. 0)
- **code** (int, e.g. 0)
- **message** (string, e.g. 操作成功)
- **status** (bool, e.g. True)
- **timestamp** (int, e.g. 1768931731)
- **ext** (list)
- *[Array Items]*
- **trace_id** (string, e.g. 8ae4feeb19cc4ed3a24a8a00f056d023)
- **success** (bool, e.g. True)
- **errcode** (int, e.g. 0)
---
## Category: `crane/http/api/data/vip_plus_match_data/{match_id}`
**Total Requests**: 179
- **data** (dict)
- **<steamid>** (dict)
- **fd_ct** (int, e.g. 2)
- **fd_t** (int, e.g. 2)
- **kast** (float, int, e.g. 0.7)
- **awp_kill** (int, e.g. 2)
- **awp_kill_ct** (int, e.g. 5)
- **awp_kill_t** (int, e.g. 2)
- **damage_stats** (int, e.g. 3)
- **damage_receive** (int, e.g. 0)
- **code** (int, e.g. 0)
- **message** (string, e.g. 操作成功)
- **status** (bool, e.g. True)
- **timestamp** (int, e.g. 1768931714)
- **ext** (list)
- *[Array Items]*
- **trace_id** (string, e.g. cff29d5dcdd6285b80d11bbb4a8a7da0)
- **success** (bool, e.g. True)
- **errcode** (int, e.g. 0)
---
## Category: `crane/http/api/match/leetify_rating/{match_id}`
**Total Requests**: 5
- **data** (dict)
- **leetify_data** (dict)
- **round_stat** (list)
- *[Array Items]*
- **round** (int, e.g. 2)
- **t_money_group** (int, e.g. 3)
- **ct_money_group** (int, e.g. 3)
- **win_reason** (int, e.g. 2)
- **bron_equipment** (dict)
- **<steamid>** (list)
- *[Array Items]*
- **Money** (int, e.g. 400)
- **WeaponName** (string, e.g. weapon_flashbang)
- **Weapon** (int, e.g. 22)
- **player_t_score** (dict)
- **<steamid>** (float, int, e.g. -21.459999999999997)
- **player_ct_score** (dict)
- **<steamid>** (float, int, e.g. 17.099999999999994)
- **player_bron_crash** (dict)
- **<steamid>** (int, e.g. 4200)
- **begin_ts** (string, e.g. 2026-01-18T19:57:29+08:00)
- **sfui_event** (dict)
- **sfui_type** (int, e.g. 2)
- **score_ct** (int, e.g. 2)
- **score_t** (int, e.g. 2)
- **end_ts** (string, e.g. 2026-01-18T19:54:37+08:00)
- **show_event** (list)
- *[Array Items]*
- **ts_real** (string, e.g. 0001-01-01T00:00:00Z)
- **ts** (int, e.g. 45)
- **t_num** (int, e.g. 2)
- **ct_num** (int, e.g. 2)
- **event_type** (int, e.g. 3)
- **kill_event** (dict, null)
- **Ts** (string, e.g. 2026-01-18T19:54:06+08:00)
- **Killer** (<steamid>, e.g. 76561199787406643)
- **Victim** (<steamid>, e.g. 76561199388433802)
- **Weapon** (int, e.g. 6)
- **KillerTeam** (int, e.g. 1)
- **KillerBot** (bool, e.g. False)
- **VictimBot** (bool, e.g. False)
- **WeaponName** (string, e.g. usp_silencer)
- **Headshot** (bool, e.g. False)
- **Penetrated** (bool, e.g. False)
- **ThroughSmoke** (bool, e.g. False)
- **NoScope** (bool, e.g. False)
- **AttackerBlind** (bool, e.g. False)
- **Attackerinair** (bool, e.g. False)
- **twin** (float, int, e.g. 0.143)
- **c_twin** (float, int, e.g. 0.44299999999999995)
- **twin_change** (float, int, e.g. -0.21600000000000003)
- **c_twin_change** (float, int, e.g. 0.21600000000000003)
- **killer_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, int, e.g. 17.099999999999994)
- **victim_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, int, e.g. -15.8)
- **assist_killer_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, e.g. 2.592)
- **trade_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, e.g. 2.2100000000000004)
- **flash_assist_killer_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, e.g. 1.1520000000000001)
- **protect_gun_player_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, e.g. 5.8999999999999995)
- **protect_gun_enemy_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, e.g. -1.18)
- **disconnect_player_score_change** (null, e.g. None)
- **disconnect_comp_score_change** (null, e.g. None)
- **round_end_fixed_score_change** (dict, null)
- **<steamid>** (dict)
- **score** (float, int, e.g. 20)
- **win_reason** (int, e.g. 2)
- **side_info** (dict)
- **ct** (list)
- *[Array Items]*
- **t** (list)
- *[Array Items]*
- **player_scores** (dict)
- **<steamid>** (float, e.g. 12.491187500000002)
- **player_t_scores** (dict)
- **<steamid>** (float, e.g. 19.06)
- **player_ct_scores** (dict)
- **<steamid>** (float, e.g. -0.009666666666665455)
- **round_total** (int, e.g. 18)
- **player_round_scores** (dict)
- **<steamid>** (dict)
- **<round_n>** (float, int, e.g. 32.347)
- **uinfo_dict** (dict)
- **<steamid>** (dict)
- **uid** (<5eid>, int, e.g. 14889445)
- **uuid** (string, e.g. 13f7dc52-ea7c-11ed-9ce2-ec0d9a495494)
- **username** (string, e.g. 刚拉)
- **nickname** (string, e.g. )
- **reg_date** (int, e.g. 1683007881)
- **username_spam_status** (int, e.g. 1)
- **steamid_64** (<steamid>, e.g. 76561199032002725)
- **avatar_url** (string, e.g. disguise/images/6f/89/6f89b22633cb95df1754fd30573c5ad6.png)
- **gender** (int, e.g. 1)
- **country_id** (string, e.g. )
- **language** (string, e.g. )
- **domain** (string, e.g. rrrtina)
- **credit** (int, e.g. 0)
- **trusted_score** (int, e.g. 0)
- **trusted_status** (int, e.g. 0)
- **plus_info** (null, e.g. None)
- **region** (int, e.g. 0)
- **province** (int, e.g. 0)
- **province_name** (string, e.g. )
- **region_name** (string, e.g. )
- **college_id** (int, e.g. 0)
- **status** (int, e.g. 0)
- **identity** (null, e.g. None)
- **code** (int, e.g. 0)
- **message** (string, e.g. 操作成功)
- **status** (bool, e.g. True)
- **timestamp** (int, e.g. 1768833830)
- **ext** (list)
- *[Array Items]*
- **trace_id** (string, e.g. 376e200283d19770bdef6dacf260f40f)
- **success** (bool, e.g. True)
- **errcode** (int, e.g. 0)
---
## Category: `crane/http/api/match/round/{match_id}`
**Total Requests**: 174
- **data** (dict)
- **round_list** (list)
- *[Array Items]*
- **all_kill** (list)
- *[Array Items]*
- **attacker** (dict)
- **name** (string, e.g. 5E-Player 我有必胜卡组)
- **pos** (dict)
- **x** (int, e.g. 734)
- **y** (int, e.g. 125)
- **z** (int, e.g. 0)
- **steamid_64** (<steamid>, e.g. 76561198330488905)
- **team** (int, e.g. 1)
- **attackerblind** (bool, e.g. False)
- **headshot** (bool, e.g. False)
- **noscope** (bool, e.g. False)
- **pasttime** (int, e.g. 45)
- **penetrated** (bool, e.g. False)
- **throughsmoke** (bool, e.g. False)
- **victim** (dict)
- **name** (<5eid>, string, e.g. 5E-Player 青青C原懒大王w)
- **pos** (dict)
- **x** (int, e.g. 1218)
- **y** (int, e.g. 627)
- **z** (int, e.g. 0)
- **steamid_64** (<steamid>, string, e.g. 76561199482118960)
- **team** (int, e.g. 1)
- **weapon** (string, e.g. usp_silencer)
- **kill** (dict)
- **<steamid>** (list)
- *[Array Items]*
- **attacker** (dict)
- **name** (string, e.g. 5E-Player 我有必胜卡组)
- **pos** (dict)
- **x** (int, e.g. 734)
- **y** (int, e.g. 149)
- **z** (int, e.g. 0)
- **steamid_64** (<steamid>, e.g. 76561198330488905)
- **team** (int, e.g. 1)
- **attackerblind** (bool, e.g. False)
- **headshot** (bool, e.g. False)
- **noscope** (bool, e.g. False)
- **pasttime** (int, e.g. 24)
- **penetrated** (bool, e.g. False)
- **throughsmoke** (bool, e.g. False)
- **victim** (dict)
- **name** (<5eid>, string, e.g. 5E-Player 青青C原懒大王w)
- **pos** (dict)
- **x** (int, e.g. 1218)
- **y** (int, e.g. 627)
- **z** (int, e.g. 0)
- **steamid_64** (<steamid>, string, e.g. 76561198812383596)
- **team** (int, e.g. 1)
- **weapon** (string, e.g. usp_silencer)
- **c4_event** (list)
- *[Array Items]*
- **event_name** (string, e.g. planted_c4)
- **location** (string, e.g. )
- **name** (string, e.g. 5E-Player 我有必胜卡组)
- **pasttime** (int, e.g. 45)
- **steamid_64** (<steamid>, e.g. 76561198330488905)
- **current_score** (dict)
- **ct** (int, e.g. 2)
- **final_round_time** (int, e.g. 68)
- **pasttime** (int, e.g. 57)
- **t** (int, e.g. 2)
- **type** (int, e.g. 2)
- **death_list** (list)
- *[Array Items]*
- **equiped** (dict)
- **<steamid>** (list)
- *[Array Items]*
- **** (list)
- *[Array Items]*
- **round_kill_event** (list)
- *[Array Items]*
- **weapon_list** (dict)
- **defuser** (list)
- *[Array Items]*
- **item** (list)
- *[Array Items]*
- **main_weapon** (list)
- *[Array Items]*
- **other_item** (list)
- *[Array Items]*
- **secondary_weapon** (list)
- *[Array Items]*
- **code** (int, e.g. 0)
- **message** (string, e.g. 操作成功)
- **status** (bool, e.g. True)
- **timestamp** (int, e.g. 1768931714)
- **ext** (list)
- *[Array Items]*
- **trace_id** (string, e.g. c2ee4f45abd89f1c90dc1cc390d21d33)
- **success** (bool, e.g. True)
- **errcode** (int, e.g. 0)
---