tauri-app: fix end-of-game stats reading, pass raw json file to front
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m6s

This commit is contained in:
2026-03-26 23:42:31 +01:00
parent 0871703b11
commit b09f669e73
3 changed files with 30 additions and 180 deletions

View File

@@ -1,163 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use std::path::PathBuf;
use uuid::Uuid;
/// A timestamped event in the timeline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimestampedEvent {
/// Video timestamp (offset from recording start) as [seconds, nanos].
pub video_timestamp: (i64, i32),
/// Game timestamp (in-game time) as [seconds, nanos].
pub game_timestamp: Option<(i64, i32)>,
/// Real-world timestamp.
pub timestamp: DateTime<Utc>,
/// Event type name.
pub event_type: String,
/// Human-readable description.
pub description: String,
}
impl TimestampedEvent {
/// Get video timestamp in seconds.
pub fn video_timestamp_secs(&self) -> i64 {
self.video_timestamp.0
}
}
/// Final game statistics for the player.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameFinalStats {
pub kills: u32,
pub deaths: u32,
pub assists: u32,
pub creep_score: u32,
pub gold_earned: u32,
pub damage_dealt: u64,
pub damage_taken: u64,
pub vision_score: f64,
pub game_duration: f64,
}
/// Rune page configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunePage {
pub primary_style_id: u32,
pub secondary_style_id: u32,
pub selected_perks: Vec<u32>,
#[serde(default)]
pub stat_modifiers: Vec<u32>,
pub name: Option<String>,
#[serde(default)]
pub current: bool,
}
/// Summoner spell information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SummonerSpells {
pub spell1_id: u32,
pub spell2_id: u32,
pub spell1_name: Option<String>,
pub spell2_name: Option<String>,
}
/// Individual item information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemInfo {
pub item_id: u32,
pub name: Option<String>,
pub slot: u32,
}
/// Item build at game end.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemBuild {
pub items: Vec<ItemInfo>,
pub trinket: Option<ItemInfo>,
}
/// Player identity information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerIdentityInfo {
pub puuid: String,
#[serde(default)]
pub summoner_name: String,
pub champion_name: Option<String>,
pub team: Option<u32>,
}
/// A timeline of events for a recording.
/// This matches the full JSON structure from record-daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timeline {
/// Recording ID.
pub recording_id: Uuid,
/// Recording start time.
pub start_time: DateTime<Utc>,
/// Recording end time.
pub end_time: Option<DateTime<Utc>>,
/// Total duration in seconds.
pub duration_secs: i64,
/// Events in the timeline.
pub events: Vec<TimestampedEvent>,
/// Champion played.
#[serde(default)]
pub champion: Option<String>,
/// Skin name.
#[serde(default)]
pub skin_name: Option<String>,
/// Queue type.
#[serde(default)]
pub queue_type: Option<String>,
/// Queue ID.
#[serde(default)]
pub queue_id: Option<u32>,
/// Game mode.
#[serde(default)]
pub game_mode: Option<String>,
/// Map name.
#[serde(default)]
pub map_name: Option<String>,
/// Summoner name.
#[serde(default)]
pub summoner_name: Option<String>,
/// Player's PUUID.
#[serde(default)]
pub puuid: Option<String>,
/// Team (100 = blue, 200 = red).
#[serde(default)]
pub team: Option<u32>,
/// Whether the game was won.
#[serde(default)]
pub victory: Option<bool>,
/// Final player stats.
#[serde(default)]
pub final_stats: Option<GameFinalStats>,
/// Game ID.
#[serde(default)]
pub game_id: Option<u64>,
/// Match ID.
#[serde(default)]
pub match_id: Option<String>,
/// Rune page at game start.
#[serde(default)]
pub runes: Option<RunePage>,
/// Summoner spells.
#[serde(default)]
pub summoner_spells: Option<SummonerSpells>,
/// Final item build at game end.
#[serde(default)]
pub final_items: Option<ItemBuild>,
/// All players in the game.
#[serde(default)]
pub all_players: Vec<PlayerIdentityInfo>,
}
/// Get the default output directory for recordings.
/// Uses the same directory structure as the record-daemon.
@@ -174,8 +17,8 @@ fn get_timeline_dir() -> PathBuf {
.unwrap_or_else(|| PathBuf::from("./recordings/timelines"))
}
/// Load all timelines from disk.
fn load_timelines() -> Vec<Timeline> {
/// Load all timelines from disk as raw JSON values.
fn load_timelines() -> Vec<Value> {
let timeline_dir = get_timeline_dir();
let mut timelines = Vec::new();
@@ -188,7 +31,7 @@ fn load_timelines() -> Vec<Timeline> {
let path = entry.path();
if path.extension().map(|e| e == "json").unwrap_or(false) {
if let Ok(contents) = fs::read_to_string(&path) {
if let Ok(timeline) = serde_json::from_str::<Timeline>(&contents) {
if let Ok(timeline) = serde_json::from_str::<Value>(&contents) {
timelines.push(timeline);
}
}
@@ -197,25 +40,29 @@ fn load_timelines() -> Vec<Timeline> {
}
// Sort by start time, newest first
timelines.sort_by(|a, b| b.start_time.cmp(&a.start_time));
timelines.sort_by(|a, b| {
let a_time = a.get("start_time").and_then(|v| v.as_str()).unwrap_or("");
let b_time = b.get("start_time").and_then(|v| v.as_str()).unwrap_or("");
b_time.cmp(a_time)
});
timelines
}
/// Get game history - returns full timeline data for each game.
/// Get game history - returns full timeline data for each game as raw JSON.
#[tauri::command]
fn get_game_history() -> Vec<Timeline> {
fn get_game_history() -> Vec<Value> {
load_timelines()
}
/// Get a specific timeline by recording ID.
/// Get a specific timeline by recording ID as raw JSON.
#[tauri::command]
fn get_timeline(recording_id: String) -> Option<Timeline> {
fn get_timeline(recording_id: String) -> Option<Value> {
let timeline_dir = get_timeline_dir();
let path = timeline_dir.join(format!("{}.json", recording_id));
if path.exists() {
if let Ok(contents) = fs::read_to_string(&path) {
return serde_json::from_str::<Timeline>(&contents).ok();
return serde_json::from_str::<Value>(&contents).ok();
}
}