event log

This commit is contained in:
FURRO404
2026-06-18 02:38:44 -07:00
parent 92eb461e2d
commit d5fc2b5831
4 changed files with 78 additions and 31 deletions
+29 -9
View File
@@ -7,7 +7,7 @@ use axum::{
};
use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{json, Value};
use std::{
collections::{BTreeMap, HashMap},
env, fs,
@@ -218,6 +218,7 @@ struct GameResponse {
struct GameLogsResponse {
chat_log: Vec<String>,
battle_log: Vec<String>,
event_log: Value,
}
#[derive(Serialize)]
@@ -581,21 +582,37 @@ async fn game_logs(
let session_id = validate_session_id(&session_id)?;
let conn = open_db(&state.battles_db)?;
// Logs are non-critical: a missing match_logs table or row yields empty arrays.
let row: Option<(Option<String>, Option<String>)> = conn
let row: Option<(Option<String>, Option<String>, Option<String>)> = match conn
.query_row(
"SELECT chat_log_json, battle_log_json FROM match_logs WHERE session_id = ?1",
"SELECT chat_log_json, battle_log_json, event_log_json FROM match_logs WHERE session_id = ?1",
params![session_id],
|r| Ok((r.get(0)?, r.get(1)?)),
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
)
.optional()
.unwrap_or(None);
let parse = |s: Option<String>| -> Vec<String> {
s.and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default()
{
Ok(row) => row,
Err(_) => conn
.query_row(
"SELECT chat_log_json, battle_log_json FROM match_logs WHERE session_id = ?1",
params![session_id],
|r| Ok((r.get(0)?, r.get(1)?, None)),
)
.optional()
.unwrap_or(None),
};
let (chat, battle) = row.unwrap_or((None, None));
let parse = |s: Option<String>| -> Vec<String> {
s.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_default()
};
let parse_event_log = |s: Option<String>| -> Value {
s.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_else(|| json!({ "kills": [], "damage": [] }))
};
let (chat, battle, event_log) = row.unwrap_or((None, None, None));
Ok(Json(GameLogsResponse {
chat_log: parse(chat),
battle_log: parse(battle),
event_log: parse_event_log(event_log),
}))
}
@@ -1929,7 +1946,10 @@ mod tests {
names.insert("ussr_t_34".to_string(), t);
assert_eq!(lookup_vehicle_name(&names, "ussr_t_34", "ru"), "Т-34");
assert_eq!(lookup_vehicle_name(&names, "ussr_t_34", "de"), "T-34");
assert_eq!(lookup_vehicle_name(&names, "unknown_cdk", "en"), "unknown_cdk");
assert_eq!(
lookup_vehicle_name(&names, "unknown_cdk", "en"),
"unknown_cdk"
);
}
#[test]
+39 -17
View File
@@ -2789,16 +2789,33 @@ function battleLineColor(line) {
return 'text-text-soft'
}
function deadVehicleKey(uid, cdk) {
return `${String(uid || '').trim()}:${String(cdk || '').trim()}`
}
function deadVehicleKeysFromEventLog(eventLog) {
const keys = new Set()
const kills = Array.isArray(eventLog?.kills) ? eventLog.kills : []
kills.forEach((kill) => {
const uid = kill?.offended_uid
const cdk = kill?.offended_unit
if (uid !== undefined && uid !== null && cdk) {
keys.add(deadVehicleKey(uid, cdk))
}
})
return keys
}
function GamePage({ gameId, navigate }) {
const [gameState, setGameState] = useState({ status: 'loading', data: null, error: null })
const [logs, setLogs] = useState({ chat_log: [], battle_log: [] })
const [logs, setLogs] = useState({ chat_log: [], battle_log: [], event_log: { kills: [], damage: [] } })
useEffect(() => {
if (!gameId) return
const controller = new AbortController()
setGameState({ status: 'loading', data: null, error: null })
setLogs({ chat_log: [], battle_log: [] })
setLogs({ chat_log: [], battle_log: [], event_log: { kills: [], damage: [] } })
fetchJson(apiEndpoints.game(gameId), controller.signal)
.then((data) => {
@@ -2818,6 +2835,7 @@ function GamePage({ gameId, navigate }) {
setLogs({
chat_log: Array.isArray(data?.chat_log) ? data.chat_log : [],
battle_log: Array.isArray(data?.battle_log) ? data.battle_log : [],
event_log: data?.event_log || { kills: [], damage: [] },
})
}
})
@@ -2830,6 +2848,7 @@ function GamePage({ gameId, navigate }) {
const game = gameState.data?.game
const participants = gameState.data?.participants || []
const deadVehicleKeys = useMemo(() => deadVehicleKeysFromEventLog(logs.event_log), [logs.event_log])
const participantNames = participants.length
? participants.map((participant) => ({
name: participant.team_name,
@@ -2931,21 +2950,24 @@ function GamePage({ gameId, navigate }) {
<p className="truncate font-semibold text-text">{player.nick || player.uid}</p>
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5">
{(player.vehicles || []).length ? (
player.vehicles.map((vehicle) => (
<span
className="vehicle-name inline-flex items-center gap-1 text-xs text-text-soft"
key={vehicle.cdk}
>
<img
alt=""
className="h-4 w-4 object-contain"
loading="lazy"
onError={(event) => { event.currentTarget.style.display = 'none' }}
src={`/vehicle-icons/${vehicle.icon}`}
/>
{vehicle.name}
</span>
))
player.vehicles.map((vehicle) => {
const isDead = deadVehicleKeys.has(deadVehicleKey(player.uid, vehicle.cdk))
return (
<span
className={`vehicle-name inline-flex items-center gap-1 text-xs text-text-soft transition-opacity ${isDead ? 'vehicle-dead' : ''}`}
key={vehicle.cdk}
>
<img
alt=""
className="h-4 w-4 object-contain"
loading="lazy"
onError={(event) => { event.currentTarget.style.display = 'none' }}
src={`/vehicle-icons/${vehicle.icon}`}
/>
{vehicle.name}
</span>
)
})
) : (
<span className="text-xs text-text-muted">{player.uid}</span>
)}
+4
View File
@@ -133,6 +133,10 @@
"Segoe UI", Inter, ui-sans-serif, system-ui, sans-serif;
}
.vehicle-dead {
opacity: 0.45;
}
/* Chat/battle logs: monospace for column alignment, with skyquake before the
generic keyword so country/event glyphs still resolve per-character. */
.log-mono {
+6 -5
View File
@@ -122,16 +122,17 @@ CREATE TABLE player_games_hist (UID TEXT, nick TEXT, team_name TEXT, team_slot T
assists INT, captures INT, deaths INT, score INT, missile_evades INT, shell_interceptions INT,
team_kills_stat INT, country_id INT, victor_bool TEXT, endtime_unix INT, team_id INT,
tss_role TEXT, pvp_ratio REAL);
CREATE TABLE match_logs (session_id TEXT PRIMARY KEY, chat_log_json TEXT, battle_log_json TEXT, built_unix INT);
CREATE TABLE match_logs (session_id TEXT PRIMARY KEY, chat_log_json TEXT, battle_log_json TEXT, event_log_json TEXT, built_unix INT);
""")
b.execute("INSERT INTO match_summary VALUES ('abc','Dom','Test Map','','','',0,1000,420.0,0,'1','2',0,0,'Cup Finals','','')")
# alice used TWO vehicles -> two rows, identical per-player stats (score 100).
b.execute("INSERT INTO player_games_hist VALUES ('1','alice','TeamWin','1','abc','t34','ussr_t_34',2,0,1,0,0,100,0,0,0,0,'Win',1000,10,'',1.5)")
b.execute("INSERT INTO player_games_hist VALUES ('1','alice','TeamWin','1','abc','is2','ussr_is_2',2,0,1,0,0,100,0,0,0,0,'Win',1000,10,'',1.5)")
b.execute("INSERT INTO player_games_hist VALUES ('2','bob','TeamLose','2','abc','pz','germ_pz_iv',0,0,0,0,1,10,0,0,0,0,'Loss',1000,11,'',0.5)")
b.execute("INSERT INTO match_logs VALUES ('abc', ?, ?, 1000)",
b.execute("INSERT INTO match_logs VALUES ('abc', ?, ?, ?, 1000)",
(json.dumps(["[00:01] [ALL] [WIN] `alice`: gg"]),
json.dumps(["+[00:30] [WIN] alice (T-34) destroyed bob (Pz.IV)"])))
json.dumps(["+[00:30] [WIN] alice (T-34) destroyed bob (Pz.IV)"]),
json.dumps({"kills": [{"offender_uid": "1", "offender_unit": "ussr_t_34", "offended_uid": "2", "offended_unit": "germ_pz_iv", "crashed": False, "time": 30000}], "damage": []})))
b.commit()
t = sqlite3.connect(f"{wd}/tss_teams.db")
t.executescript("CREATE TABLE teams_data (team_id INT PRIMARY KEY, name TEXT, members INT DEFAULT 0, captain_uid TEXT);")
@@ -187,8 +188,8 @@ if [[ $? -eq 0 ]]; then ok "backend game detail (en)"; else bad "backend game de
RU_NAME="$(curl -s "localhost:$BE_PORT/api/tss/games/abc?lang=ru" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print([v['name'] for p in d['participants'] for pl in p['players'] for v in pl['vehicles'] if v['cdk']=='ussr_t_34'][0])")"
assert_eq "ru translation of ussr_t_34" "$RU_NAME" "Т-34"
LOG_COUNTS="$(curl -s "localhost:$BE_PORT/api/tss/games/abc/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log']), len(d['battle_log']))")"
assert_eq "logs chat/battle counts" "$LOG_COUNTS" "1 1"
LOG_COUNTS="$(curl -s "localhost:$BE_PORT/api/tss/games/abc/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log']), len(d['battle_log']), len(d.get('event_log', {}).get('kills', [])))")"
assert_eq "logs chat/battle/kill counts" "$LOG_COUNTS" "1 1 1"
MISS="$(curl -s "localhost:$BE_PORT/api/tss/games/deadbeef/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log'])+len(d['battle_log']))")"
assert_eq "missing-session logs empty" "$MISS" "0"