event log
This commit is contained in:
+29
-9
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user