event log
This commit is contained in:
+28
-8
@@ -7,7 +7,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
env, fs,
|
env, fs,
|
||||||
@@ -218,6 +218,7 @@ struct GameResponse {
|
|||||||
struct GameLogsResponse {
|
struct GameLogsResponse {
|
||||||
chat_log: Vec<String>,
|
chat_log: Vec<String>,
|
||||||
battle_log: Vec<String>,
|
battle_log: Vec<String>,
|
||||||
|
event_log: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -581,21 +582,37 @@ async fn game_logs(
|
|||||||
let session_id = validate_session_id(&session_id)?;
|
let session_id = validate_session_id(&session_id)?;
|
||||||
let conn = open_db(&state.battles_db)?;
|
let conn = open_db(&state.battles_db)?;
|
||||||
// Logs are non-critical: a missing match_logs table or row yields empty arrays.
|
// 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, event_log_json FROM match_logs WHERE session_id = ?1",
|
||||||
|
params![session_id],
|
||||||
|
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
{
|
||||||
|
Ok(row) => row,
|
||||||
|
Err(_) => conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT chat_log_json, battle_log_json FROM match_logs WHERE session_id = ?1",
|
"SELECT chat_log_json, battle_log_json FROM match_logs WHERE session_id = ?1",
|
||||||
params![session_id],
|
params![session_id],
|
||||||
|r| Ok((r.get(0)?, r.get(1)?)),
|
|r| Ok((r.get(0)?, r.get(1)?, None)),
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.unwrap_or(None);
|
.unwrap_or(None),
|
||||||
let parse = |s: Option<String>| -> Vec<String> {
|
|
||||||
s.and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default()
|
|
||||||
};
|
};
|
||||||
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 {
|
Ok(Json(GameLogsResponse {
|
||||||
chat_log: parse(chat),
|
chat_log: parse(chat),
|
||||||
battle_log: parse(battle),
|
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);
|
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", "ru"), "Т-34");
|
||||||
assert_eq!(lookup_vehicle_name(&names, "ussr_t_34", "de"), "T-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]
|
#[test]
|
||||||
|
|||||||
+27
-5
@@ -2789,16 +2789,33 @@ function battleLineColor(line) {
|
|||||||
return 'text-text-soft'
|
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 }) {
|
function GamePage({ gameId, navigate }) {
|
||||||
const [gameState, setGameState] = useState({ status: 'loading', data: null, error: null })
|
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(() => {
|
useEffect(() => {
|
||||||
if (!gameId) return
|
if (!gameId) return
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
setGameState({ status: 'loading', data: null, error: null })
|
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)
|
fetchJson(apiEndpoints.game(gameId), controller.signal)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -2818,6 +2835,7 @@ function GamePage({ gameId, navigate }) {
|
|||||||
setLogs({
|
setLogs({
|
||||||
chat_log: Array.isArray(data?.chat_log) ? data.chat_log : [],
|
chat_log: Array.isArray(data?.chat_log) ? data.chat_log : [],
|
||||||
battle_log: Array.isArray(data?.battle_log) ? data.battle_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 game = gameState.data?.game
|
||||||
const participants = gameState.data?.participants || []
|
const participants = gameState.data?.participants || []
|
||||||
|
const deadVehicleKeys = useMemo(() => deadVehicleKeysFromEventLog(logs.event_log), [logs.event_log])
|
||||||
const participantNames = participants.length
|
const participantNames = participants.length
|
||||||
? participants.map((participant) => ({
|
? participants.map((participant) => ({
|
||||||
name: participant.team_name,
|
name: participant.team_name,
|
||||||
@@ -2931,9 +2950,11 @@ function GamePage({ gameId, navigate }) {
|
|||||||
<p className="truncate font-semibold text-text">{player.nick || player.uid}</p>
|
<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">
|
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5">
|
||||||
{(player.vehicles || []).length ? (
|
{(player.vehicles || []).length ? (
|
||||||
player.vehicles.map((vehicle) => (
|
player.vehicles.map((vehicle) => {
|
||||||
|
const isDead = deadVehicleKeys.has(deadVehicleKey(player.uid, vehicle.cdk))
|
||||||
|
return (
|
||||||
<span
|
<span
|
||||||
className="vehicle-name inline-flex items-center gap-1 text-xs text-text-soft"
|
className={`vehicle-name inline-flex items-center gap-1 text-xs text-text-soft transition-opacity ${isDead ? 'vehicle-dead' : ''}`}
|
||||||
key={vehicle.cdk}
|
key={vehicle.cdk}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -2945,7 +2966,8 @@ function GamePage({ gameId, navigate }) {
|
|||||||
/>
|
/>
|
||||||
{vehicle.name}
|
{vehicle.name}
|
||||||
</span>
|
</span>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-text-muted">{player.uid}</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;
|
"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
|
/* Chat/battle logs: monospace for column alignment, with skyquake before the
|
||||||
generic keyword so country/event glyphs still resolve per-character. */
|
generic keyword so country/event glyphs still resolve per-character. */
|
||||||
.log-mono {
|
.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,
|
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,
|
team_kills_stat INT, country_id INT, victor_bool TEXT, endtime_unix INT, team_id INT,
|
||||||
tss_role TEXT, pvp_ratio REAL);
|
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','','')")
|
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).
|
# 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','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 ('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 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: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()
|
b.commit()
|
||||||
t = sqlite3.connect(f"{wd}/tss_teams.db")
|
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);")
|
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])")"
|
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"
|
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']))")"
|
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 counts" "$LOG_COUNTS" "1 1"
|
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']))")"
|
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"
|
assert_eq "missing-session logs empty" "$MISS" "0"
|
||||||
|
|||||||
Reference in New Issue
Block a user