From d5fc2b5831e881d56576b3ded7657d16ebbc8a81 Mon Sep 17 00:00:00 2001 From: FURRO404 Date: Thu, 18 Jun 2026 02:38:44 -0700 Subject: [PATCH] event log --- backend/src/main.rs | 38 ++++++++++++++++++------ frontend/src/App.jsx | 56 ++++++++++++++++++++++++----------- frontend/src/styles.css | 4 +++ scripts/verify_game_detail.sh | 11 +++---- 4 files changed, 78 insertions(+), 31 deletions(-) diff --git a/backend/src/main.rs b/backend/src/main.rs index 9edeb95..76954f6 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -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, battle_log: Vec, + 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, Option)> = conn + let row: Option<(Option, Option, Option)> = 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| -> Vec { - 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| -> Vec { + s.and_then(|t| serde_json::from_str(&t).ok()) + .unwrap_or_default() + }; + let parse_event_log = |s: Option| -> 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] diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 06bc99f..4abcb18 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 }) {

{player.nick || player.uid}

{(player.vehicles || []).length ? ( - player.vehicles.map((vehicle) => ( - - { event.currentTarget.style.display = 'none' }} - src={`/vehicle-icons/${vehicle.icon}`} - /> - {vehicle.name} - - )) + player.vehicles.map((vehicle) => { + const isDead = deadVehicleKeys.has(deadVehicleKey(player.uid, vehicle.cdk)) + return ( + + { event.currentTarget.style.display = 'none' }} + src={`/vehicle-icons/${vehicle.icon}`} + /> + {vehicle.name} + + ) + }) ) : ( {player.uid} )} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index b0c9e20..50c468c 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -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 { diff --git a/scripts/verify_game_detail.sh b/scripts/verify_game_detail.sh index 654c70e..48e1c44 100755 --- a/scripts/verify_game_detail.sh +++ b/scripts/verify_game_detail.sh @@ -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"