change to hex and update DBs (#1284)
This commit is contained in:
+147
-36
@@ -1,28 +1,25 @@
|
||||
"""TSSBOT storage layer — SQLite paths + idempotent DB init.
|
||||
"""TSSBOT storage layer — SQLite paths, idempotent DB init, and insert helpers.
|
||||
|
||||
Two databases live under ``STORAGE_VOL_PATH`` (set in ``TSSBOT/.env``):
|
||||
|
||||
* ``tss_teams.db`` — persistent team registry (analogue of SREBOT's squadrons.db)
|
||||
* ``tss_battles.db`` — per-match summary + per-player game history
|
||||
(analogue of SREBOT's sq_battles.db)
|
||||
* ``tss_teams.db`` — persistent team registry
|
||||
* ``tss_battles.db`` — per-match summary + per-player/per-vehicle game history
|
||||
|
||||
Schemas mirror SREBOT shape so query patterns transfer directly. The
|
||||
TSS-specific differences are:
|
||||
One row is written to ``player_games_hist`` per vehicle *actually used* by each
|
||||
player. Player-level stats (kills, deaths, etc.) are Spectra player totals and
|
||||
are duplicated across vehicle rows — aggregation queries that sum stats must
|
||||
``DISTINCT`` on session_id first to avoid double-counting.
|
||||
|
||||
* "squadron" → "team" / "clan_id" → "team_id"
|
||||
* Spectra ships an untransformed per-player stat blob, so we keep extra
|
||||
columns the raw payload provides (``score``, ``missile_evades``,
|
||||
``shell_interceptions``, ``team_kills_stat``, ``country_id``, ``team_slot``).
|
||||
* No vehicle-translate / stat-divide transform — one row per
|
||||
``(UID, session_id, vehicle_internal)`` for each used unit, with the
|
||||
player's full stat line copied onto each row. Aggregation queries that
|
||||
sum across vehicles must ``DISTINCT`` on session_id first.
|
||||
Session IDs are stored as hex strings throughout (Spectra sends decimal integers;
|
||||
``tss_ws._session_id`` normalises them before anything hits the DB).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import aiosqlite
|
||||
|
||||
@@ -53,31 +50,25 @@ TSS_TEAMS_DB_PATH: Path = STORAGE_DIR / "tss_teams.db"
|
||||
|
||||
_MATCH_SUMMARY_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS match_summary (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
map_name TEXT,
|
||||
mission_mode TEXT,
|
||||
starttime_unix INTEGER,
|
||||
endtime_unix INTEGER,
|
||||
draw INTEGER NOT NULL DEFAULT 0,
|
||||
winning_slot TEXT,
|
||||
losing_slot TEXT,
|
||||
winning_team TEXT,
|
||||
losing_team TEXT,
|
||||
winning_team_json TEXT,
|
||||
losing_team_json TEXT,
|
||||
received_unix INTEGER,
|
||||
winning_team_id INTEGER,
|
||||
losing_team_id INTEGER
|
||||
session_id TEXT PRIMARY KEY,
|
||||
map_name TEXT,
|
||||
mission_mode TEXT,
|
||||
difficulty TEXT,
|
||||
starttime_unix INTEGER,
|
||||
endtime_unix INTEGER,
|
||||
duration REAL,
|
||||
draw INTEGER NOT NULL DEFAULT 0,
|
||||
winning_slot TEXT,
|
||||
losing_slot TEXT,
|
||||
received_unix INTEGER,
|
||||
tournament_id INTEGER
|
||||
)
|
||||
"""
|
||||
|
||||
_MATCH_SUMMARY_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_map_name ON match_summary(map_name)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_endtime ON match_summary(endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_winning_team ON match_summary(winning_team)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_losing_team ON match_summary(losing_team)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_winning_team_id ON match_summary(winning_team_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_losing_team_id ON match_summary(losing_team_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_map_name ON match_summary(map_name)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_endtime ON match_summary(endtime_unix)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ms_difficulty ON match_summary(difficulty)",
|
||||
]
|
||||
|
||||
|
||||
@@ -85,7 +76,7 @@ _PLAYER_GAMES_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS player_games_hist (
|
||||
UID TEXT NOT NULL,
|
||||
nick TEXT NOT NULL,
|
||||
team_name TEXT NOT NULL DEFAULT 'UNKNOWN',
|
||||
team_name TEXT,
|
||||
team_tag TEXT NOT NULL DEFAULT 'UNKNOWN',
|
||||
team_slot TEXT,
|
||||
session_id TEXT NOT NULL,
|
||||
@@ -104,6 +95,10 @@ CREATE TABLE IF NOT EXISTS player_games_hist (
|
||||
victor_bool TEXT NOT NULL DEFAULT 'Loss',
|
||||
endtime_unix INTEGER NOT NULL DEFAULT 0,
|
||||
team_id INTEGER,
|
||||
tss_team_uuid TEXT,
|
||||
tss_role TEXT,
|
||||
tss_place INTEGER,
|
||||
pvp_ratio REAL,
|
||||
UNIQUE (UID, session_id, vehicle_internal)
|
||||
)
|
||||
"""
|
||||
@@ -243,6 +238,15 @@ async def _init_battles_db() -> None:
|
||||
await conn.execute(_PLAYER_GAMES_SQL)
|
||||
await _apply(conn, _MATCH_SUMMARY_INDEXES)
|
||||
await _apply(conn, _PLAYER_GAMES_INDEXES)
|
||||
await _migrate(conn, "match_summary", {
|
||||
"tournament_id": "tournament_id INTEGER",
|
||||
})
|
||||
await _migrate(conn, "player_games_hist", {
|
||||
"tss_team_uuid": "tss_team_uuid TEXT",
|
||||
"tss_role": "tss_role TEXT",
|
||||
"tss_place": "tss_place INTEGER",
|
||||
"pvp_ratio": "pvp_ratio REAL",
|
||||
})
|
||||
await conn.commit()
|
||||
|
||||
|
||||
@@ -261,6 +265,7 @@ async def _init_teams_db() -> None:
|
||||
await _migrate(conn, "teams_data", {
|
||||
"clanrating": "clanrating INTEGER",
|
||||
})
|
||||
|
||||
await _migrate(conn, "team_members", {
|
||||
"points": "points INTEGER NOT NULL DEFAULT 0",
|
||||
})
|
||||
@@ -285,3 +290,109 @@ async def init_tss_dbs() -> None:
|
||||
TSS_BATTLES_DB_PATH,
|
||||
TSS_TEAMS_DB_PATH,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Insertion helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def insert_match(game: Dict[str, Any]) -> None:
|
||||
"""Insert one row into match_summary from a normalised game dict.
|
||||
|
||||
``game["_id"]`` must already be a hex string (normalised by tss_ws).
|
||||
Safe to call multiple times — INSERT OR IGNORE skips duplicates.
|
||||
"""
|
||||
async with aiosqlite.connect(TSS_BATTLES_DB_PATH) as conn:
|
||||
for sql in _PRAGMAS:
|
||||
await conn.execute(sql)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO match_summary
|
||||
(session_id, map_name, mission_mode, difficulty,
|
||||
starttime_unix, endtime_unix, duration,
|
||||
draw, winning_slot, losing_slot, received_unix)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(game["_id"]),
|
||||
str(game.get("mission_name") or game.get("level_path") or ""),
|
||||
str(game.get("mission_mode") or ""),
|
||||
str(game.get("difficulty") or ""),
|
||||
int(game.get("start_ts") or 0),
|
||||
int(game.get("end_ts") or 0),
|
||||
float(game.get("duration") or 0),
|
||||
1 if game.get("draw") else 0,
|
||||
str(game.get("winner") or ""),
|
||||
str(game.get("loser") or ""),
|
||||
int(time.time()),
|
||||
),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def insert_player_games(game: Dict[str, Any]) -> None:
|
||||
"""Insert one row per used vehicle per player into player_games_hist.
|
||||
|
||||
victor_bool is set to 'Win' when the player's team slot matches the
|
||||
winning slot, 'Loss' otherwise.
|
||||
Safe to call multiple times — INSERT OR IGNORE skips duplicates.
|
||||
"""
|
||||
session_id = str(game["_id"])
|
||||
winner_slot = str(game.get("winner") or "")
|
||||
end_ts = int(game.get("end_ts") or 0)
|
||||
players = game.get("players") or {}
|
||||
|
||||
rows = []
|
||||
for uid_str, p in players.items():
|
||||
victor_bool = "Win" if str(p.get("team", "")) == winner_slot else "Loss"
|
||||
tag_raw = p.get("tag") or ""
|
||||
team_tag = tag_raw[1:-1] if len(tag_raw) > 2 else tag_raw
|
||||
|
||||
used_units = [u for u in (p.get("units") or []) if u.get("used")]
|
||||
if not used_units:
|
||||
continue
|
||||
|
||||
for unit in used_units:
|
||||
rows.append((
|
||||
str(uid_str),
|
||||
str(p.get("name") or ""),
|
||||
"", # team_name — resolved later
|
||||
team_tag,
|
||||
str(p.get("team") or ""), # team_slot ("1" or "2")
|
||||
session_id,
|
||||
str(unit.get("unit_normalized") or ""),
|
||||
str(unit.get("unit") or ""),
|
||||
int(p.get("ground_kills") or 0),
|
||||
int(p.get("air_kills") or 0),
|
||||
int(p.get("assists") or 0),
|
||||
int(p.get("captures") or 0),
|
||||
int(p.get("deaths") or 0),
|
||||
int(p.get("score") or 0),
|
||||
int(p.get("missile_evades") or 0),
|
||||
int(p.get("shell_interceptions") or 0),
|
||||
int(p.get("team_kills") or 0),
|
||||
p.get("country_id"),
|
||||
victor_bool,
|
||||
end_ts,
|
||||
None, # team_id — resolved later
|
||||
))
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
async with aiosqlite.connect(TSS_BATTLES_DB_PATH) as conn:
|
||||
for sql in _PRAGMAS:
|
||||
await conn.execute(sql)
|
||||
await conn.executemany(
|
||||
"""
|
||||
INSERT OR IGNORE INTO player_games_hist
|
||||
(UID, nick, team_name, team_tag, team_slot, session_id,
|
||||
vehicle, vehicle_internal,
|
||||
ground_kills, air_kills, assists, captures, deaths, score,
|
||||
missile_evades, shell_interceptions, team_kills_stat,
|
||||
country_id, victor_bool, endtime_unix, team_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
rows,
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
Reference in New Issue
Block a user