From d67d52fa8640a633db7fcf41abd29a65906e0afd Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 27 Mar 2026 13:42:12 +0100 Subject: [PATCH] record-daemon: refactor to record raw league data --- record-daemon/src/lqp/client.rs | 244 +++------------------ record-daemon/src/lqp/mappings.rs | 261 ----------------------- record-daemon/src/lqp/mod.rs | 2 - record-daemon/src/lqp/websocket.rs | 19 +- record-daemon/src/main.rs | 218 ++++--------------- record-daemon/src/timeline/mod.rs | 78 ++----- record-daemon/src/timeline/store.rs | 320 ++++++++-------------------- 7 files changed, 191 insertions(+), 951 deletions(-) delete mode 100644 record-daemon/src/lqp/mappings.rs diff --git a/record-daemon/src/lqp/client.rs b/record-daemon/src/lqp/client.rs index ac77df0..83f7521 100644 --- a/record-daemon/src/lqp/client.rs +++ b/record-daemon/src/lqp/client.rs @@ -17,7 +17,6 @@ use super::api_types::{ use super::auth::LockfileCredentials; use super::endpoints; use super::events::GameEvent; -use super::mappings::{champion_id_to_name, spell_id_to_name}; use super::state::{ClientState, GameflowPhase}; use super::tls::create_insecure_tls_config; use super::websocket::parse_websocket_message; @@ -469,6 +468,36 @@ impl LqpClient { // Metadata Fetching Methods // ========================================================================= + /// Fetch raw session data as JSON. + pub async fn fetch_raw_session(&self) -> Result { + self.request("GET", endpoints::SESSION).await + } + + /// Fetch raw summoner data as JSON. + pub async fn fetch_raw_summoner(&self) -> Result { + self.request("GET", endpoints::SUMMONER).await + } + + /// Fetch raw champion select data as JSON. + pub async fn fetch_raw_champion_select(&self) -> Result { + self.request("GET", endpoints::CHAMPION_SELECT).await + } + + /// Fetch raw rune page data as JSON. + pub async fn fetch_raw_rune_page(&self) -> Result { + self.request("GET", endpoints::RUNE_PAGES).await + } + + /// Fetch raw live client data as JSON. + pub async fn fetch_raw_live_client_data(&self) -> Result { + self.request("GET", endpoints::LIVE_CLIENT_DATA).await + } + + /// Fetch raw end-of-game stats as JSON. + pub async fn fetch_raw_end_game_stats(&self) -> Result { + self.request("GET", endpoints::GAME_STATS).await + } + /// Fetch pre-game data (stores raw API responses directly). pub async fn fetch_pregame_data(&self) -> Result { let mut data = PreGameData::default(); @@ -511,219 +540,6 @@ impl LqpClient { pub async fn fetch_game_end_stats(&self) -> Result { self.get_game_stats_typed().await } - - /// Fetch complete player game metadata including runes, summoner spells, and items. - pub async fn fetch_player_game_metadata(&self) -> Result { - use super::{RunePage, SummonerSpells}; - - let mut metadata = super::PlayerGameMetadata::default(); - - // Get summoner info (typed) - if let Ok(summoner) = self.get_summoner_typed().await { - metadata.puuid = summoner.puuid; - metadata.summoner_name = summoner - .display_name - .or(summoner.name) - .or(summoner.internal_name); - } - - // Get rune page (typed) - if let Ok(rune_page) = self.get_rune_page_typed().await { - let primary_style_id = rune_page.primary_style_id.unwrap_or(0) as u32; - let secondary_style_id = rune_page.sub_style_id.unwrap_or(0) as u32; - let selected_perks = rune_page - .selected_perk_ids - .unwrap_or_default() - .iter() - .map(|id| *id as u32) - .collect(); - - if primary_style_id > 0 { - metadata.runes = Some(RunePage { - primary_style_id, - secondary_style_id, - selected_perks, - stat_modifiers: Vec::new(), - name: rune_page.name, - current: rune_page.current.unwrap_or(true), - }); - } - } - - // Get summoner spells from live client data (typed) - if let Ok(active_player) = self.get_live_client_active_player_typed().await { - debug!("[METADATA] Live client active player data received"); - - if let Some(ref spells) = active_player.summoner_spells { - let spell1_id = spells - .summoner_spell_one - .as_ref() - .and_then(|s| s.spell_id) - .or(spells.spell1_id) - .unwrap_or(0) as u32; - - let spell2_id = spells - .summoner_spell_two - .as_ref() - .and_then(|s| s.spell_id) - .or(spells.spell2_id) - .unwrap_or(0) as u32; - - if spell1_id > 0 || spell2_id > 0 { - metadata.summoner_spells = Some(SummonerSpells { - spell1_id, - spell2_id, - spell1_name: spell_id_to_name(spell1_id), - spell2_name: spell_id_to_name(spell2_id), - }); - } - } - - if metadata.summoner_name.is_none() - || metadata.summoner_name.as_ref().is_none_or(|n| n.is_empty()) - { - metadata.summoner_name = active_player - .summoner_name - .or(active_player.display_name) - .or(active_player.riot_id); - } - } - - // Fallback: Get summoner spells from session gameData (typed) - if metadata.summoner_spells.is_none() { - if let Ok(session) = self.get_session_typed().await { - if let Some(local_puuid) = &metadata.puuid { - if let Some(ref game_data) = session.game_data { - // Check team one - if let Some(ref team) = game_data.team_one { - for player in team { - if player.puuid.as_deref() == Some(local_puuid.as_str()) { - let spell1_id = player.spell1_id.unwrap_or(0) as u32; - let spell2_id = player.spell2_id.unwrap_or(0) as u32; - - if spell1_id > 0 || spell2_id > 0 { - metadata.summoner_spells = Some(SummonerSpells { - spell1_id, - spell2_id, - spell1_name: spell_id_to_name(spell1_id), - spell2_name: spell_id_to_name(spell2_id), - }); - } - - metadata.champion_id = player.champion_id.map(|id| id as u32); - metadata.team = player.team_id.map(|id| id as u32); - break; - } - } - } - // Check team two if not found - if metadata.summoner_spells.is_none() { - if let Some(ref team) = game_data.team_two { - for player in team { - if player.puuid.as_deref() == Some(local_puuid.as_str()) { - let spell1_id = player.spell1_id.unwrap_or(0) as u32; - let spell2_id = player.spell2_id.unwrap_or(0) as u32; - - if spell1_id > 0 || spell2_id > 0 { - metadata.summoner_spells = Some(SummonerSpells { - spell1_id, - spell2_id, - spell1_name: spell_id_to_name(spell1_id), - spell2_name: spell_id_to_name(spell2_id), - }); - } - - metadata.champion_id = - player.champion_id.map(|id| id as u32); - metadata.team = player.team_id.map(|id| id as u32); - break; - } - } - } - } - } - } - } - } - - if let Some(champ_id) = metadata.champion_id { - metadata.champion_name = champion_id_to_name(champ_id); - } - - Ok(metadata) - } - - /// Fetch all players' puuid to summoner name mapping. - pub async fn fetch_all_players_identities(&self) -> Result> { - let mut players = Vec::new(); - - // Try live client data first (typed) - if let Ok(player_list) = self.get_live_client_player_list_typed().await { - for player in &player_list.0 { - let summoner_name = player - .summoner_name - .as_deref() - .or(player.riot_id.as_deref()) - .unwrap_or(""); - - if let Some(ref puuid) = player.puuid { - players.push(super::PlayerIdentity { - puuid: puuid.clone(), - summoner_name: summoner_name.to_string(), - summoner_id: player.summoner_id, - champion_name: player.champion_name.clone(), - team: player.team.map(|id| id as u32), - }); - } - } - } - - // Fallback: try from gameflow session (typed) - if players.is_empty() { - if let Ok(session) = self.get_session_typed().await { - if let Some(ref game_data) = session.game_data { - // Team one (team ID 100) - if let Some(ref team) = game_data.team_one { - for player in team { - if let (Some(ref puuid), Some(ref summoner_name)) = - (&player.puuid, &player.summoner_name) - { - let champion_id = player.champion_id.map(|id| id as u32); - let champion_name = champion_id.and_then(champion_id_to_name); - players.push(super::PlayerIdentity { - puuid: puuid.clone(), - summoner_name: summoner_name.clone(), - summoner_id: player.summoner_id, - champion_name, - team: Some(100), - }); - } - } - } - // Team two (team ID 200) - if let Some(ref team) = game_data.team_two { - for player in team { - if let (Some(ref puuid), Some(ref summoner_name)) = - (&player.puuid, &player.summoner_name) - { - let champion_id = player.champion_id.map(|id| id as u32); - let champion_name = champion_id.and_then(champion_id_to_name); - players.push(super::PlayerIdentity { - puuid: puuid.clone(), - summoner_name: summoner_name.clone(), - summoner_id: player.summoner_id, - champion_name, - team: Some(200), - }); - } - } - } - } - } - } - - Ok(players) - } } impl Default for LqpClient { diff --git a/record-daemon/src/lqp/mappings.rs b/record-daemon/src/lqp/mappings.rs deleted file mode 100644 index bab8879..0000000 --- a/record-daemon/src/lqp/mappings.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! ID to name mappings for League of Legends data. -//! -//! Provides conversion functions for champion IDs, summoner spell IDs, -//! and map IDs to their human-readable names. - -/// Convert summoner spell ID to name. -pub fn spell_id_to_name(id: u32) -> Option { - let name = match id { - 1 => "Cleanse", - 3 => "Exhaust", - 4 => "Flash", - 6 => "Ghost", - 7 => "Heal", - 11 => "Smite", - 12 => "Teleport", - 13 => "Clarity", - 14 => "Ignite", - 21 => "Barrier", - 32 => "Mark", - 39 => "Mark", - 54 => "Placeholder", - 55 => "Placeholder", - _ => return None, - }; - Some(name.to_string()) -} - -/// Convert champion ID to champion name. -/// This is a simplified mapping for common champions. -pub fn champion_id_to_name(id: u32) -> Option { - let name = match id { - 1 => "Annie", - 2 => "Olaf", - 3 => "Galio", - 4 => "TwistedFate", - 5 => "XinZhao", - 6 => "Urgot", - 7 => "LeBlanc", - 8 => "Vladimir", - 9 => "Fiddlesticks", - 10 => "Kayle", - 11 => "MasterYi", - 12 => "Alistar", - 13 => "Ryze", - 14 => "Sion", - 15 => "Sivir", - 16 => "Soraka", - 17 => "Teemo", - 18 => "Tristana", - 19 => "Warwick", - 20 => "Nunu", - 21 => "MissFortune", - 22 => "Ashe", - 23 => "Tryndamere", - 24 => "Jax", - 25 => "Morgana", - 26 => "Zilean", - 27 => "Singed", - 28 => "Evelynn", - 29 => "Twitch", - 30 => "Karthus", - 31 => "Cho'Gath", - 32 => "Amumu", - 33 => "Rammus", - 34 => "Anivia", - 35 => "Shaco", - 36 => "DrMundo", - 37 => "Sona", - 38 => "Kassadin", - 39 => "Irelia", - 40 => "Janna", - 41 => "Gangplank", - 42 => "Corki", - 43 => "Karma", - 44 => "Taric", - 45 => "Veigar", - 48 => "Trundle", - 50 => "Swain", - 51 => "Caitlyn", - 52 => "Blitzcrank", - 53 => "Malphite", - 54 => "Katarina", - 55 => "Nocturne", - 56 => "Maokai", - 57 => "Renekton", - 58 => "JarvanIV", - 59 => "Elise", - 60 => "Talon", - 61 => "Orianna", - 62 => "Wukong", - 63 => "Brand", - 64 => "LeeSin", - 67 => "Vayne", - 68 => "Rumble", - 69 => "Cassiopeia", - 72 => "Skarner", - 74 => "Heimerdinger", - 75 => "Nasus", - 76 => "Nidalee", - 77 => "Udyr", - 78 => "Poppy", - 79 => "Gragas", - 80 => "Pantheon", - 81 => "Ezreal", - 82 => "Mordekaiser", - 83 => "Yorick", - 84 => "Akali", - 85 => "Kennedy", - 86 => "Garen", - 89 => "Leona", - 90 => "Malzahar", - 91 => "Talon", - 92 => "Riven", - 96 => "Kog'Maw", - 98 => "Shen", - 99 => "Lux", - 101 => "Xerath", - 102 => "Shyvana", - 103 => "Ahri", - 104 => "Graves", - 105 => "Fizz", - 106 => "Volibear", - 107 => "Rengar", - 110 => "Varus", - 111 => "Nautilus", - 112 => "Viktor", - 113 => "Sejuani", - 114 => "Fiora", - 115 => "Ziggs", - 117 => "Lulu", - 119 => "Draven", - 120 => "Hecarim", - 121 => "Kha'Zix", - 122 => "Darius", - 126 => "Jayce", - 127 => "Lissandra", - 131 => "Diana", - 133 => "Quinn", - 134 => "Syndra", - 136 => "AurelionSol", - 141 => "Kayn", - 142 => "Zoe", - 143 => "Lillia", - 145 => "Samira", - 147 => "Seraphine", - 150 => "Gnar", - 154 => "Zac", - 157 => "Yasuo", - 161 => "Vel'Koz", - 163 => "Taliyah", - 164 => "Camille", - 166 => "Akshan", - 167 => "Nilah", - 201 => "Braum", - 202 => "Jhin", - 203 => "Kindred", - 222 => "Jinx", - 223 => "TahmKench", - 236 => "Lucian", - 238 => "Zed", - 240 => "Kled", - 245 => "Ekko", - 246 => "Qiyana", - 254 => "Vi", - 255 => "Janna", - 256 => "Pyke", - 257 => "Nami", - 266 => "Aatrox", - 267 => "Nami", - 268 => "Azir", - 350 => "Yuumi", - 360 => "Samira", - 412 => "Thresh", - 420 => "Illaoi", - 421 => "Rek'Sai", - 427 => "Ivern", - 429 => "Kalista", - 432 => "Bard", - 497 => "Rakan", - 498 => "Xayah", - 516 => "Ornn", - 517 => "Sylas", - 518 => "Neeko", - 523 => "Aphelios", - 526 => "Rell", - 555 => "Pyke", - 711 => "Vex", - 777 => "Yone", - 875 => "Sett", - 876 => "Lillia", - 887 => "Gwen", - 888 => "Viego", - 895 => "KSante", - 901 => "Smolder", - 902 => "Hwei", - 950 => "Naafiri", - 951 => "Briar", - _ => return None, - }; - Some(name.to_string()) -} - -/// Convert map ID to map name. -pub fn map_id_to_name(id: u64) -> Option { - let name = match id { - 1 => "Summoner's Rift", - 2 => "Summoner's Rift", - 3 => "The Proving Grounds", - 4 => "Twisted Treeline", - 8 => "The Crystal Scar", - 10 => "Twisted Treeline", - 11 => "Summoner's Rift", - 12 => "Howling Abyss", - 14 => "Butcher's Bridge", - 16 => "Cosmic Ruins", - 18 => "Valoran City Park", - 19 => "Substructure 43", - 20 => "Crash Site", - 21 => "Nexus Blitz", - 22 => "Convergence", - 23 => "Arena", - 24 => "Arena", - 25 => "Rings of Wrath", - 30 => "Swarm", - 31 => "Swarm", - 32 => "Swarm", - 33 => "Swarm", - 34 => "Swarm", - 35 => "Swarm", - _ => return None, - }; - Some(name.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_spell_id_to_name() { - assert_eq!(spell_id_to_name(4), Some("Flash".to_string())); - assert_eq!(spell_id_to_name(7), Some("Heal".to_string())); - assert_eq!(spell_id_to_name(11), Some("Smite".to_string())); - assert_eq!(spell_id_to_name(999), None); - } - - #[test] - fn test_champion_id_to_name() { - assert_eq!(champion_id_to_name(1), Some("Annie".to_string())); - assert_eq!(champion_id_to_name(22), Some("Ashe".to_string())); - assert_eq!(champion_id_to_name(157), Some("Yasuo".to_string())); - assert_eq!(champion_id_to_name(9999), None); - } - - #[test] - fn test_map_id_to_name() { - assert_eq!(map_id_to_name(11), Some("Summoner's Rift".to_string())); - assert_eq!(map_id_to_name(12), Some("Howling Abyss".to_string())); - assert_eq!(map_id_to_name(999), None); - } -} diff --git a/record-daemon/src/lqp/mod.rs b/record-daemon/src/lqp/mod.rs index aa9cf6f..d89bbd0 100644 --- a/record-daemon/src/lqp/mod.rs +++ b/record-daemon/src/lqp/mod.rs @@ -8,7 +8,6 @@ mod auth; mod client; mod endpoints; mod events; -mod mappings; mod state; mod tls; mod websocket; @@ -33,6 +32,5 @@ pub use events::{ ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity, QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember, }; -pub use mappings::{champion_id_to_name, map_id_to_name, spell_id_to_name}; pub use state::{ClientState, GameflowPhase}; pub use websocket::{parse_event_from_uri, parse_websocket_message}; diff --git a/record-daemon/src/lqp/websocket.rs b/record-daemon/src/lqp/websocket.rs index 0742f7d..95defd5 100644 --- a/record-daemon/src/lqp/websocket.rs +++ b/record-daemon/src/lqp/websocket.rs @@ -6,7 +6,6 @@ use tracing::{debug, info, warn}; use super::events::{GameEvent, GameflowSession}; -use super::mappings::map_id_to_name; /// Parse a WebSocket message into a game event. pub fn parse_websocket_message(text: &str) -> Option { @@ -233,22 +232,9 @@ fn parse_game_start_event(data: &serde_json::Value) -> Option { .map(|s| s.to_string()) }); - // Extract map name - let map_name = session - .as_ref() - .and_then(|s| s.map_id()) - .and_then(|id| map_id_to_name(id as u64)) - .or_else(|| { - data.get("gameData") - .and_then(|gd| gd.get("queue")) - .and_then(|q| q.get("mapId")) - .and_then(|id| id.as_u64()) - .and_then(map_id_to_name) - }); - info!( - "Extracted game metadata: game_id={}, queue={:?}, queue_id={:?}, mode={:?}, map={:?}", - game_id, queue_type, queue_id, game_mode, map_name + "Extracted game metadata: game_id={}, queue={:?}, queue_id={:?}, mode={:?}", + game_id, queue_type, queue_id, game_mode ); // Note: Player-specific data (champion, team, summoner_name) is NOT extracted here. @@ -263,7 +249,6 @@ fn parse_game_start_event(data: &serde_json::Value) -> Option { "queueType": queue_type, "queueId": queue_id, "gameMode": game_mode, - "map": map_name, "session": session })) .unwrap_or(GameEvent::Unknown), diff --git a/record-daemon/src/main.rs b/record-daemon/src/main.rs index 0cfb4a9..c547fed 100644 --- a/record-daemon/src/main.rs +++ b/record-daemon/src/main.rs @@ -354,24 +354,16 @@ impl Daemon { // Handle recording start/stop match transition { StateTransition::GameStarted => { - // Extract data from the GameStart event that was stored in the timeline - let (game_id, queue_type, queue_id, game_mode, map_name, session) = - if let GameEvent::GameStart(ref info) = event { - ( - info.game_id, - info.queue_type.clone(), - info.queue_id, - info.game_mode.clone(), - info.map_name.clone(), - info.session.clone(), - ) - } else { - (0, None, None, None, None, None) - }; + // Extract game_id from the GameStart event + let game_id = if let GameEvent::GameStart(ref info) = event { + info.game_id + } else { + 0 + }; info!( - "[EVENT_HANDLER] GameStarted transition - game_id: {}, queue_type: {:?}, game_mode: {:?}", - game_id, queue_type, game_mode + "[EVENT_HANDLER] GameStarted transition - game_id: {}", + game_id ); // If already recording, stop the current recording first @@ -387,110 +379,34 @@ impl Daemon { info!("[EVENT_HANDLER] Calling start_recording..."); - // Fetch player game metadata (puuid, runes, summoner spells) - let player_metadata = - self.lqp_client.fetch_player_game_metadata().await.ok(); - let (puuid, runes, summoner_spells, champion_id, team, summoner_name) = - player_metadata.map_or((None, None, None, None, None, None), |m| { - ( - m.puuid, - m.runes, - m.summoner_spells, - m.champion_id, - m.team, - m.summoner_name, - ) - }); - - // If we have puuid and session, extract player-specific data from session - let (champion, final_team, final_summoner_name, final_summoner_spells) = - if let Some(ref p) = puuid { - if let Some(ref sess) = session { - let champ_id = sess.get_champion_id(p); - let champ_name = - champ_id.and_then(record_daemon::lqp::champion_id_to_name); - let team_id = sess.get_team(p); - let summoner = sess.get_summoner_name(p).map(|s| s.to_string()); - - // Extract summoner spells from session's player_champion_selections - let spells_from_session = - sess.get_player_selection(p).map(|selection| { - record_daemon::lqp::SummonerSpells { - spell1_id: selection.spell1_id, - spell2_id: selection.spell2_id, - spell1_name: None, - spell2_name: None, - } - }); - - ( - champ_name.or_else(|| { - champion_id - .and_then(record_daemon::lqp::champion_id_to_name) - }), - team_id.or(team), - summoner.or(summoner_name), - spells_from_session.or(summoner_spells), - ) - } else { - ( - champion_id - .and_then(record_daemon::lqp::champion_id_to_name), - team, - summoner_name, - summoner_spells, - ) - } - } else { - ( - champion_id.and_then(record_daemon::lqp::champion_id_to_name), - team, - summoner_name, - summoner_spells, - ) - }; - - info!( - "[EVENT_HANDLER] Final values: puuid={:?}, runes={:?}, summoner_spells={:?}", - puuid, runes, final_summoner_spells + // Fetch raw API data in parallel + let ( + raw_session, + raw_summoner, + raw_champion_select, + raw_rune_page, + raw_live_client_data, + ) = tokio::join!( + self.lqp_client.fetch_raw_session(), + self.lqp_client.fetch_raw_summoner(), + self.lqp_client.fetch_raw_champion_select(), + self.lqp_client.fetch_raw_rune_page(), + self.lqp_client.fetch_raw_live_client_data() ); - // Fetch all players identities for puuid mapping - let all_players_identities = - self.lqp_client.fetch_all_players_identities().await.ok(); - let all_players_info: Vec = - all_players_identities - .unwrap_or_default() - .into_iter() - .map(|p| record_daemon::timeline::PlayerIdentityInfo { - puuid: p.puuid, - summoner_name: p.summoner_name, - champion_name: p.champion_name, - team: p.team, - }) - .collect(); - - // Build game metadata for timeline + // Build game metadata for timeline with raw JSON let metadata_update = record_daemon::timeline::MetadataUpdate { - queue_type, - queue_id, - game_mode, - map_name, - team: final_team, - summoner_name: final_summoner_name, - puuid, - runes, - summoner_spells: final_summoner_spells, - all_players: all_players_info, - ..Default::default() + game_id: Some(game_id), + raw_session: raw_session.ok(), + raw_summoner: raw_summoner.ok(), + raw_champion_select: raw_champion_select.ok(), + raw_rune_page: raw_rune_page.ok(), + raw_live_client_data: raw_live_client_data.ok(), + raw_end_game_stats: None, }; if let Err(e) = self - .start_recording_with_metadata( - game_id, - champion.as_deref(), - metadata_update, - ) + .start_recording_with_metadata(game_id, metadata_update) .await { error!("[EVENT_HANDLER] Failed to start recording: {}", e); @@ -503,15 +419,17 @@ impl Daemon { StateTransition::GameEnded => { info!("[EVENT_HANDLER] GameEnded transition"); - // Fetch end-of-game stats from API - let game_end_stats = self.lqp_client.fetch_game_end_stats().await.ok(); + // Fetch raw end-of-game stats from API + let raw_end_game_stats = + self.lqp_client.fetch_raw_end_game_stats().await.ok(); info!( "[EVENT_HANDLER] Game end stats from API: {:?}", - game_end_stats + raw_end_game_stats.is_some() ); - if let Err(e) = self.stop_recording_with_metadata(game_end_stats).await { + if let Err(e) = self.stop_recording_with_metadata(raw_end_game_stats).await + { error!("[EVENT_HANDLER] Failed to stop recording: {}", e); // Don't propagate error - keep daemon running @@ -539,19 +457,18 @@ impl Daemon { async fn start_recording_with_metadata( &self, game_id: u64, - champion: Option<&str>, metadata_update: record_daemon::timeline::MetadataUpdate, ) -> Result<()> { info!( - "Daemon::start_recording_with_metadata called - game {} ({:?})", - game_id, champion + "Daemon::start_recording_with_metadata called - game {}", + game_id ); // Create a recording entry in the timeline store first let recording_id = self .timeline_store .write() - .start_recording_entry(Some(game_id), champion.map(|s| s.to_string())); + .start_recording_entry(Some(game_id), None); // Update metadata immediately with game start info if let Err(e) = self @@ -569,7 +486,6 @@ impl Daemon { // Clone Arc references for use in spawn_blocking let recording_engine = self.recording_engine.clone(); let event_mapper = self.event_mapper.clone(); - let champion_owned = champion.map(|s| s.to_string()); // Use spawn_blocking to avoid blocking the async runtime tokio::task::spawn_blocking(move || { @@ -579,7 +495,7 @@ impl Daemon { if let Some(ref mut engine) = *engine_guard { info!("Calling engine.start_recording..."); - engine.start_recording(Some(game_id), champion_owned.as_deref())?; + engine.start_recording(Some(game_id), None)?; info!("engine.start_recording returned successfully"); event_mapper.write().start(); info!("Event mapper started"); @@ -604,10 +520,10 @@ impl Daemon { self.stop_recording_with_metadata(None).await } - /// Stop recording with optional game end stats. + /// Stop recording with optional raw game end stats JSON. async fn stop_recording_with_metadata( &self, - game_end_stats: Option, + raw_end_game_stats: Option, ) -> Result<()> { info!("Stopping recording"); @@ -618,7 +534,6 @@ impl Daemon { let recording_engine = self.recording_engine.clone(); let event_mapper = self.event_mapper.clone(); let timeline_store = self.timeline_store.clone(); - let pregame_data = self.pregame_data.read().clone(); // Use spawn_blocking to avoid blocking the async runtime tokio::task::spawn_blocking(move || { @@ -642,50 +557,11 @@ impl Daemon { } }; - // Update metadata if we have it - let mut update = record_daemon::timeline::MetadataUpdate::default(); - - // Add pre-game data - if let Some(pregame) = pregame_data { - // Convert champion_id to name if available - if let Some(champion_id) = pregame.champion_id() { - update.champion = - record_daemon::lqp::champion_id_to_name(champion_id as u32); - } - update.summoner_name = pregame.summoner_name().map(|s| s.to_string()); - update.queue_id = pregame.queue_id().map(|id| id as u32); - update.game_mode = pregame.game_mode().map(|s| s.to_string()); - update.map_name = pregame.map_name().map(|s| s.to_string()); - update.team = pregame.team().map(|id| id as u32); - } - - // Add game end stats from API response - if let Some(stats) = game_end_stats { - update.victory = Some(stats.is_victory()); - update.match_id = stats.match_id.map(|id| id.to_string()); - - // Store raw end-of-game stats as JSON - update.raw_end_game_stats = serde_json::to_value(&stats).ok(); - - // Get local player stats - if let Some(player) = stats.get_local_player() { - if let Some(player_stats) = &player.stats { - update.final_stats = Some(record_daemon::timeline::GameFinalStats { - kills: player_stats.champions_killed.unwrap_or(0) as u32, - deaths: player_stats.num_deaths.unwrap_or(0) as u32, - assists: player_stats.assists.unwrap_or(0) as u32, - creep_score: player_stats.minions_killed.unwrap_or(0) as u32, - gold_earned: player_stats.gold_earned.unwrap_or(0) as u32, - damage_dealt: player_stats - .total_damage_dealt_to_champions - .unwrap_or(0), - damage_taken: player_stats.total_damage_taken.unwrap_or(0), - vision_score: player_stats.vision_score.unwrap_or(0.0), - game_duration: stats.game_length.unwrap_or(0.0), - }); - } - } - } + // Update metadata with raw end game stats + let update = record_daemon::timeline::MetadataUpdate { + raw_end_game_stats, + ..Default::default() + }; // Apply the update if let Err(e) = timeline_store.write().update_metadata(recording_id, update) { diff --git a/record-daemon/src/timeline/mod.rs b/record-daemon/src/timeline/mod.rs index b678775..a072b69 100644 --- a/record-daemon/src/timeline/mod.rs +++ b/record-daemon/src/timeline/mod.rs @@ -4,18 +4,16 @@ mod mapper; mod store; pub use mapper::EventMapper; -pub use store::{ - GameFinalStats, MetadataUpdate, PlayerIdentityInfo, RecordingMetadata, TimelineStore, - TimestampedEvent, -}; +pub use store::{MetadataUpdate, RecordingMetadata, TimelineStore, TimestampedEvent}; use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::lqp::{GameEvent, RunePage, SummonerSpells}; +use crate::lqp::GameEvent; /// A timeline of events for a recording. +/// Stores raw API responses for maximum flexibility and future-proofing. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Timeline { /// Recording ID. @@ -28,51 +26,27 @@ pub struct Timeline { pub duration_secs: i64, /// Events in the timeline. pub events: Vec, - /// Champion played. + /// Game ID if available. #[serde(default)] - pub champion: Option, - /// Skin name. + pub game_id: Option, + /// Raw session data from `/lol-gameflow/v1/session`. #[serde(default)] - pub skin_name: Option, - /// Queue type. + pub raw_session: Option, + /// Raw summoner data from `/lol-summoner/v1/current-summoner`. #[serde(default)] - pub queue_type: Option, - /// Queue ID. + pub raw_summoner: Option, + /// Raw champion select data from `/lol-champ-select/v1/session`. #[serde(default)] - pub queue_id: Option, - /// Game mode. + pub raw_champion_select: Option, + /// Raw rune page data from `/lol-perks/v1/currentpage`. #[serde(default)] - pub game_mode: Option, - /// Map name. + pub raw_rune_page: Option, + /// Raw live client data from `/liveclientdata/allgamedata`. #[serde(default)] - pub map_name: Option, - /// Summoner name. - #[serde(default)] - pub summoner_name: Option, - /// Player's PUUID. - #[serde(default)] - pub puuid: Option, - /// Team (100 = blue, 200 = red). - #[serde(default)] - pub team: Option, - /// Whether the game was won. - #[serde(default)] - pub victory: Option, - /// Final player stats. - #[serde(default)] - pub final_stats: Option, - /// Rune page at game start. - #[serde(default)] - pub runes: Option, - /// Summoner spells. - #[serde(default)] - pub summoner_spells: Option, - /// Raw end-of-game stats JSON from the API. + pub raw_live_client_data: Option, + /// Raw end-of-game stats from `/lol-end-of-game/v1/eog-stats-block`. #[serde(default)] pub raw_end_game_stats: Option, - /// All players in the game (puuid to summoner name mapping). - #[serde(default)] - pub all_players: Vec, } impl Timeline { @@ -91,21 +65,13 @@ impl Timeline { end_time: None, duration_secs: 0, events: Vec::new(), - champion: None, - skin_name: None, - queue_type: None, - queue_id: None, - game_mode: None, - map_name: None, - summoner_name: None, - puuid: None, - team: None, - victory: None, - final_stats: None, - runes: None, - summoner_spells: None, + game_id: None, + raw_session: None, + raw_summoner: None, + raw_champion_select: None, + raw_rune_page: None, + raw_live_client_data: None, raw_end_game_stats: None, - all_players: Vec::new(), } } diff --git a/record-daemon/src/timeline/store.rs b/record-daemon/src/timeline/store.rs index ffe0542..0217e22 100644 --- a/record-daemon/src/timeline/store.rs +++ b/record-daemon/src/timeline/store.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::error::{Result, TimelineError}; -use crate::lqp::{GameEvent, RunePage, SummonerSpells}; +use crate::lqp::GameEvent; use crate::recording::RecordingResult; /// A timestamped event in the timeline. @@ -32,55 +32,36 @@ pub struct TimestampedEvent { } /// Metadata for a recording. +/// Stores raw API responses for maximum flexibility and future-proofing. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecordingMetadata { /// Unique recording ID. pub id: Uuid, /// Game ID if available. pub game_id: Option, - /// Match ID if available. - pub match_id: Option, - /// Champion played. - pub champion: Option, - /// Skin name. - pub skin_name: Option, - /// Queue type (ranked, normal, aram, etc.). - pub queue_type: Option, - /// Queue ID. - pub queue_id: Option, - /// Game mode. - pub game_mode: Option, - /// Map name. - pub map_name: Option, - /// Player's summoner name. - pub summoner_name: Option, - /// Player's PUUID. + /// Raw session data from `/lol-gameflow/v1/session`. #[serde(default)] - pub puuid: Option, - /// Team (100 = blue, 200 = red). - pub team: Option, - /// Whether the game was won. - pub victory: Option, - /// Final player stats. - pub final_stats: Option, - /// Player's rune page at game start. + pub raw_session: Option, + /// Raw summoner data from `/lol-summoner/v1/current-summoner`. #[serde(default)] - pub runes: Option, - /// Player's summoner spells. + pub raw_summoner: Option, + /// Raw champion select data from `/lol-champ-select/v1/session`. #[serde(default)] - pub summoner_spells: Option, - /// Raw end-of-game stats JSON from the API. + pub raw_champion_select: Option, + /// Raw rune page data from `/lol-perks/v1/currentpage`. + #[serde(default)] + pub raw_rune_page: Option, + /// Raw live client data from `/liveclientdata/allgamedata`. + #[serde(default)] + pub raw_live_client_data: Option, + /// Raw end-of-game stats from `/lol-end-of-game/v1/eog-stats-block`. #[serde(default)] pub raw_end_game_stats: Option, - /// All players in the game (puuid -> summoner name mapping). - #[serde(default)] - pub all_players: Vec, /// Recording start time. pub start_time: DateTime, /// Recording end time. pub end_time: Option>, /// Recording duration. - // #[serde(with = "chrono::serde::seconds")] pub duration: Duration, /// Output file path. pub file_path: Option, @@ -92,64 +73,16 @@ pub struct RecordingMetadata { pub finalized: bool, } -/// Player identity information for puuid to summoner name mapping. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PlayerIdentityInfo { - /// Player's PUUID. - pub puuid: String, - /// Player's summoner name. - pub summoner_name: String, - /// Player's champion name. - #[serde(default)] - pub champion_name: Option, - /// Player's team (100 or 200). - #[serde(default)] - pub team: Option, -} - -/// Final game statistics for the player. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GameFinalStats { - /// Kills. - pub kills: u32, - /// Deaths. - pub deaths: u32, - /// Assists. - pub assists: u32, - /// Creep score. - pub creep_score: u32, - /// Gold earned. - pub gold_earned: u32, - /// Damage dealt. - pub damage_dealt: u64, - /// Damage taken. - pub damage_taken: u64, - /// Vision score. - pub vision_score: f64, - /// Game duration in seconds. - pub game_duration: f64, -} - /// Update for recording metadata. #[derive(Debug, Clone, Default)] pub struct MetadataUpdate { - pub champion: Option, - pub match_id: Option, - pub skin_name: Option, - pub queue_type: Option, - pub queue_id: Option, - pub game_mode: Option, - pub map_name: Option, - pub summoner_name: Option, - pub puuid: Option, - pub team: Option, - pub victory: Option, - pub final_stats: Option, - pub runes: Option, - pub summoner_spells: Option, + pub game_id: Option, + pub raw_session: Option, + pub raw_summoner: Option, + pub raw_champion_select: Option, + pub raw_rune_page: Option, + pub raw_live_client_data: Option, pub raw_end_game_stats: Option, - pub all_players: Vec, } impl RecordingMetadata { @@ -158,22 +91,12 @@ impl RecordingMetadata { Self { id: Uuid::new_v4(), game_id: result.game_id, - match_id: None, - champion: result.champion.clone(), - skin_name: None, - queue_type: None, - queue_id: None, - game_mode: None, - map_name: None, - summoner_name: None, - puuid: None, - team: None, - victory: None, - final_stats: None, - runes: None, - summoner_spells: None, + raw_session: None, + raw_summoner: None, + raw_champion_select: None, + raw_rune_page: None, + raw_live_client_data: None, raw_end_game_stats: None, - all_players: Vec::new(), start_time: result.start_time, end_time: Some(result.end_time), duration: result.duration, @@ -220,27 +143,17 @@ impl TimelineStore { /// Start a new recording entry (called when recording begins). /// Returns the recording ID for tracking events during recording. - pub fn start_recording_entry(&self, game_id: Option, champion: Option) -> Uuid { + pub fn start_recording_entry(&self, game_id: Option, _champion: Option) -> Uuid { let id = Uuid::new_v4(); let metadata = RecordingMetadata { id, game_id, - match_id: None, - champion, - skin_name: None, - queue_type: None, - queue_id: None, - game_mode: None, - map_name: None, - summoner_name: None, - puuid: None, - team: None, - victory: None, - final_stats: None, - runes: None, - summoner_spells: None, + raw_session: None, + raw_summoner: None, + raw_champion_select: None, + raw_rune_page: None, + raw_live_client_data: None, raw_end_game_stats: None, - all_players: Vec::new(), start_time: Utc::now(), end_time: None, duration: Duration::zero(), @@ -286,22 +199,12 @@ impl TimelineStore { let metadata = RecordingMetadata { id, game_id: result.game_id, - match_id: None, - champion: result.champion.clone(), - skin_name: None, - queue_type: None, - queue_id: None, - game_mode: None, - map_name: None, - summoner_name: None, - puuid: None, - team: None, - victory: None, - final_stats: None, - runes: None, - summoner_spells: None, + raw_session: None, + raw_summoner: None, + raw_champion_select: None, + raw_rune_page: None, + raw_live_client_data: None, raw_end_game_stats: None, - all_players: Vec::new(), start_time: result.start_time, end_time: Some(result.end_time), duration: result.duration, @@ -344,54 +247,27 @@ impl TimelineStore { pub fn update_metadata(&self, recording_id: Uuid, update: MetadataUpdate) -> Result<()> { let mut recordings = self.recordings.write(); if let Some(metadata) = recordings.get_mut(&recording_id) { - if let Some(champion) = update.champion { - metadata.champion = Some(champion); + if let Some(game_id) = update.game_id { + metadata.game_id = Some(game_id); } - if let Some(match_id) = update.match_id { - metadata.match_id = Some(match_id); + if let Some(raw_session) = update.raw_session { + metadata.raw_session = Some(raw_session); } - if let Some(skin_name) = update.skin_name { - metadata.skin_name = Some(skin_name); + if let Some(raw_summoner) = update.raw_summoner { + metadata.raw_summoner = Some(raw_summoner); } - if let Some(queue_type) = update.queue_type { - metadata.queue_type = Some(queue_type); + if let Some(raw_champion_select) = update.raw_champion_select { + metadata.raw_champion_select = Some(raw_champion_select); } - if let Some(queue_id) = update.queue_id { - metadata.queue_id = Some(queue_id); + if let Some(raw_rune_page) = update.raw_rune_page { + metadata.raw_rune_page = Some(raw_rune_page); } - if let Some(game_mode) = update.game_mode { - metadata.game_mode = Some(game_mode); - } - if let Some(map_name) = update.map_name { - metadata.map_name = Some(map_name); - } - if let Some(summoner_name) = update.summoner_name { - metadata.summoner_name = Some(summoner_name); - } - if let Some(puuid) = update.puuid { - metadata.puuid = Some(puuid); - } - if let Some(team) = update.team { - metadata.team = Some(team); - } - if let Some(victory) = update.victory { - metadata.victory = Some(victory); - } - if let Some(final_stats) = update.final_stats { - metadata.final_stats = Some(final_stats); - } - if let Some(runes) = update.runes { - metadata.runes = Some(runes); - } - if let Some(summoner_spells) = update.summoner_spells { - metadata.summoner_spells = Some(summoner_spells); + if let Some(raw_live_client_data) = update.raw_live_client_data { + metadata.raw_live_client_data = Some(raw_live_client_data); } if let Some(raw_end_game_stats) = update.raw_end_game_stats { metadata.raw_end_game_stats = Some(raw_end_game_stats); } - if !update.all_players.is_empty() { - metadata.all_players = update.all_players; - } } drop(recordings); self.persist_recording(recording_id)?; @@ -429,21 +305,13 @@ impl TimelineStore { end_time: metadata.end_time, duration_secs: metadata.duration.num_seconds(), events, - champion: metadata.champion.clone(), - skin_name: metadata.skin_name.clone(), - queue_type: metadata.queue_type.clone(), - queue_id: metadata.queue_id, - game_mode: metadata.game_mode.clone(), - map_name: metadata.map_name.clone(), - summoner_name: metadata.summoner_name.clone(), - puuid: metadata.puuid.clone(), - team: metadata.team, - victory: metadata.victory, - final_stats: metadata.final_stats.clone(), - runes: metadata.runes.clone(), - summoner_spells: metadata.summoner_spells.clone(), + game_id: metadata.game_id, + raw_session: metadata.raw_session.clone(), + raw_summoner: metadata.raw_summoner.clone(), + raw_champion_select: metadata.raw_champion_select.clone(), + raw_rune_page: metadata.raw_rune_page.clone(), + raw_live_client_data: metadata.raw_live_client_data.clone(), raw_end_game_stats: metadata.raw_end_game_stats.clone(), - all_players: metadata.all_players.clone(), }) } @@ -480,21 +348,13 @@ impl TimelineStore { end_time: metadata.end_time, duration_secs: metadata.duration.num_seconds(), events, - champion: metadata.champion, - skin_name: metadata.skin_name, - queue_type: metadata.queue_type, - queue_id: metadata.queue_id, - game_mode: metadata.game_mode, - map_name: metadata.map_name, - summoner_name: metadata.summoner_name, - puuid: metadata.puuid, - team: metadata.team, - victory: metadata.victory, - final_stats: metadata.final_stats, - runes: metadata.runes, - summoner_spells: metadata.summoner_spells, + game_id: metadata.game_id, + raw_session: metadata.raw_session, + raw_summoner: metadata.raw_summoner, + raw_champion_select: metadata.raw_champion_select, + raw_rune_page: metadata.raw_rune_page, + raw_live_client_data: metadata.raw_live_client_data, raw_end_game_stats: metadata.raw_end_game_stats, - all_players: metadata.all_players, }; let file_path = self.storage_dir.join(format!("{}.json", id)); @@ -518,40 +378,40 @@ impl TimelineStore { if path.extension().map(|e| e == "json").unwrap_or(false) { if let Ok(contents) = std::fs::read_to_string(&path) { if let Ok(timeline) = serde_json::from_str::(&contents) { + let recording_id = timeline.recording_id; + let start_time = timeline.start_time; + let end_time = timeline.end_time; + let duration = timeline.duration(); + let event_count = timeline.events.len(); + let game_id = timeline.game_id; + let raw_session = timeline.raw_session; + let raw_summoner = timeline.raw_summoner; + let raw_champion_select = timeline.raw_champion_select; + let raw_rune_page = timeline.raw_rune_page; + let raw_live_client_data = timeline.raw_live_client_data; + let raw_end_game_stats = timeline.raw_end_game_stats; + let events = timeline.events; + let metadata = RecordingMetadata { - id: timeline.recording_id, - game_id: None, - match_id: None, - champion: None, - skin_name: None, - queue_type: None, - queue_id: None, - game_mode: None, - map_name: None, - summoner_name: None, - puuid: None, - team: None, - victory: None, - final_stats: None, - runes: None, - summoner_spells: None, - raw_end_game_stats: None, - all_players: Vec::new(), - start_time: timeline.start_time, - end_time: timeline.end_time, - duration: timeline.duration(), + id: recording_id, + game_id, + raw_session, + raw_summoner, + raw_champion_select, + raw_rune_page, + raw_live_client_data, + raw_end_game_stats, + start_time, + end_time, + duration, file_path: None, file_size: None, - event_count: timeline.events.len(), + event_count, finalized: true, }; - self.recordings - .write() - .insert(timeline.recording_id, metadata); - self.timelines - .write() - .insert(timeline.recording_id, timeline.events); + self.recordings.write().insert(recording_id, metadata); + self.timelines.write().insert(recording_id, events); } } } @@ -597,6 +457,6 @@ mod tests { assert_eq!(recordings.len(), 1); let metadata = store.get_recording(id).unwrap(); - assert_eq!(metadata.champion, Some("Ahri".to_string())); + assert_eq!(metadata.game_id, Some(12345)); } }