diff --git a/record-daemon/src/lqp/client.rs b/record-daemon/src/lqp/client.rs index 5d2ce3d..ac77df0 100644 --- a/record-daemon/src/lqp/client.rs +++ b/record-daemon/src/lqp/client.rs @@ -16,8 +16,7 @@ use super::api_types::{ }; use super::auth::LockfileCredentials; use super::endpoints; -use super::events::{GameEvent, ItemBuild}; -use super::items::{parse_items_from_game_stats, parse_items_from_live_client}; +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; @@ -725,122 +724,6 @@ impl LqpClient { Ok(players) } - - /// Fetch final items from end-of-game stats or live client data. - pub async fn fetch_final_items(&self) -> Result> { - info!("[ITEMS] Fetching final items..."); - - // First try live client data (typed) - match self.get_live_client_player_list_typed().await { - Ok(player_list) => { - info!( - "[ITEMS] Live client player list response received with {} players", - player_list.0.len() - ); - for player in &player_list.0 { - if player.is_local_player == Some(true) { - info!("[ITEMS] Found local player in live client data"); - if let Some(ref items) = player.items { - info!("[ITEMS] Items array has {} items", items.len()); - // Convert LiveClientItem to serde_json::Value for parsing - let items_json: Vec = items - .iter() - .filter_map(|item| { - item.item_id.map(|id| { - serde_json::json!({"itemId": id, "displayName": item.display_name}) - }) - }) - .collect(); - let item_build = parse_items_from_live_client(&items_json); - if item_build.is_some() { - info!("[ITEMS] Successfully parsed items from live client data"); - return Ok(item_build); - } - } - } - } - } - Err(e) => { - info!("[ITEMS] Failed to get live client player list: {:?}", e); - } - } - - // Fallback: try end-of-game stats (typed) - match self.get_game_stats_typed().await { - Ok(stats) => { - info!("[ITEMS] Game stats response received"); - - // Try local player first - if let Some(local_player) = stats.get_local_player() { - info!("[ITEMS] Found localPlayer in game stats"); - if let Some(ref items) = local_player.items { - info!("[ITEMS] localPlayer.items array has {} items", items.len()); - // Convert item IDs to serde_json::Value for parsing (as raw numbers) - let items_json: Vec = - items.iter().map(|id| serde_json::json!(*id)).collect(); - let item_build = parse_items_from_game_stats(&items_json); - if item_build.is_some() { - info!("[ITEMS] Successfully parsed items from localPlayer"); - return Ok(item_build); - } - } - } - - // Try teams - if let Some(ref teams) = stats.teams { - info!("[ITEMS] Found {} teams in game stats", teams.len()); - for team in teams { - if let Some(ref players) = team.players { - for player in players { - if player.is_local_player == Some(true) { - info!("[ITEMS] Found local player in teams[].players[]"); - if let Some(ref items) = player.items { - info!( - "[ITEMS] Player items array has {} items", - items.len() - ); - let items_json: Vec = - items.iter().map(|id| serde_json::json!(*id)).collect(); - let item_build = parse_items_from_game_stats(&items_json); - if item_build.is_some() { - info!("[ITEMS] Successfully parsed items from teams[].players[]"); - return Ok(item_build); - } - } - } - } - } - } - } - - // Try legacy players array - if let Some(ref players) = stats.players { - info!( - "[ITEMS] Found {} players in game stats (legacy)", - players.len() - ); - if let Some(player) = players.first() { - if let Some(ref items) = player.items { - let items_json: Vec = - items.iter().map(|id| serde_json::json!(*id)).collect(); - let item_build = parse_items_from_game_stats(&items_json); - if item_build.is_some() { - return Ok(item_build); - } - } - } - } - - info!("[ITEMS] Could not find items in game stats structure"); - } - Err(e) => { - info!("[ITEMS] Failed to get game stats: {:?}", e); - } - } - - info!("[ITEMS] Could not fetch final items from any source"); - Ok(None) - } } impl Default for LqpClient { diff --git a/record-daemon/src/lqp/items.rs b/record-daemon/src/lqp/items.rs deleted file mode 100644 index 93ae925..0000000 --- a/record-daemon/src/lqp/items.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Item parsing utilities for LQP client. -//! -//! Provides functions for parsing item builds from different data formats. - -use super::events::{ItemBuild, ItemInfo}; - -/// Parse items from live client data format. -/// -/// Live client data items are objects with `itemID` and `displayName` fields. -pub fn parse_items_from_live_client(items: &[serde_json::Value]) -> Option { - let mut item_list = Vec::new(); - let mut trinket = None; - - for (slot, item) in items.iter().enumerate() { - if let Some(item_id) = item.get("itemID").and_then(|id| id.as_u64()) { - if item_id > 0 { - let item_info = ItemInfo { - item_id: item_id as u32, - name: item - .get("displayName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()), - slot: slot as u32, - }; - - // Slot 6 is typically the trinket - if slot == 6 { - trinket = Some(item_info); - } else { - item_list.push(item_info); - } - } - } - } - - if !item_list.is_empty() || trinket.is_some() { - Some(ItemBuild { - items: item_list, - trinket, - }) - } else { - None - } -} - -/// Parse items from game stats format. -/// -/// Game stats items are just numbers (item IDs) in an array. -pub fn parse_items_from_game_stats(items: &[serde_json::Value]) -> Option { - let mut item_list = Vec::new(); - let mut trinket = None; - - for (slot, item) in items.iter().enumerate() { - // Items in game stats are just numbers (item IDs), not objects - if let Some(item_id) = item.as_u64() { - if item_id > 0 { - let item_info = ItemInfo { - item_id: item_id as u32, - name: None, // Item names would need a separate mapping - slot: slot as u32, - }; - - // Slot 6 is typically the trinket - if slot == 6 { - trinket = Some(item_info); - } else { - item_list.push(item_info); - } - } - } - } - - if !item_list.is_empty() || trinket.is_some() { - Some(ItemBuild { - items: item_list, - trinket, - }) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_items_from_live_client_empty() { - let items = vec![]; - let result = parse_items_from_live_client(&items); - assert!(result.is_none()); - } - - #[test] - fn test_parse_items_from_live_client_with_items() { - let items = vec![ - serde_json::json!({"itemID": 1001, "displayName": "Boots"}), - serde_json::json!({"itemID": 0, "displayName": ""}), - ]; - let result = parse_items_from_live_client(&items); - assert!(result.is_some()); - let build = result.unwrap(); - assert_eq!(build.items.len(), 1); - assert_eq!(build.items[0].item_id, 1001); - } - - #[test] - fn test_parse_items_from_game_stats_empty() { - let items = vec![]; - let result = parse_items_from_game_stats(&items); - assert!(result.is_none()); - } - - #[test] - fn test_parse_items_from_game_stats_with_items() { - let items = vec![ - serde_json::json!(1001), - serde_json::json!(0), - serde_json::json!(3020), - ]; - let result = parse_items_from_game_stats(&items); - assert!(result.is_some()); - let build = result.unwrap(); - assert_eq!(build.items.len(), 2); - assert_eq!(build.items[0].item_id, 1001); - assert_eq!(build.items[1].item_id, 3020); - } - - #[test] - fn test_parse_items_with_trinket() { - // Slot 6 is trinket - let items = vec![ - serde_json::json!(1001), - serde_json::json!(0), - serde_json::json!(0), - serde_json::json!(0), - serde_json::json!(0), - serde_json::json!(0), - serde_json::json!(3340), // Trinket in slot 6 - ]; - let result = parse_items_from_game_stats(&items); - assert!(result.is_some()); - let build = result.unwrap(); - assert_eq!(build.items.len(), 1); - assert!(build.trinket.is_some()); - assert_eq!(build.trinket.unwrap().item_id, 3340); - } -} diff --git a/record-daemon/src/lqp/mod.rs b/record-daemon/src/lqp/mod.rs index 4f8c1f0..aa9cf6f 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 items; mod mappings; mod state; mod tls; @@ -34,7 +33,6 @@ pub use events::{ ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity, QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember, }; -pub use items::{parse_items_from_game_stats, parse_items_from_live_client}; 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/main.rs b/record-daemon/src/main.rs index 032b579..0cfb4a9 100644 --- a/record-daemon/src/main.rs +++ b/record-daemon/src/main.rs @@ -503,10 +503,6 @@ impl Daemon { StateTransition::GameEnded => { info!("[EVENT_HANDLER] GameEnded transition"); - // Fetch final items before stopping - let fetched_final_items = - self.lqp_client.fetch_final_items().await.ok().flatten(); - // Fetch end-of-game stats from API let game_end_stats = self.lqp_client.fetch_game_end_stats().await.ok(); @@ -515,10 +511,7 @@ impl Daemon { game_end_stats ); - if let Err(e) = self - .stop_recording_with_metadata(game_end_stats, fetched_final_items) - .await - { + if let Err(e) = self.stop_recording_with_metadata(game_end_stats).await { error!("[EVENT_HANDLER] Failed to stop recording: {}", e); // Don't propagate error - keep daemon running @@ -608,14 +601,13 @@ impl Daemon { /// Stop recording. async fn stop_recording(&self) -> Result<()> { - self.stop_recording_with_metadata(None, None).await + self.stop_recording_with_metadata(None).await } /// Stop recording with optional game end stats. async fn stop_recording_with_metadata( &self, game_end_stats: Option, - final_items: Option, ) -> Result<()> { info!("Stopping recording"); @@ -672,6 +664,9 @@ impl Daemon { 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 { @@ -692,9 +687,6 @@ impl Daemon { } } - // Add final items - update.final_items = final_items; - // Apply the update if let Err(e) = timeline_store.write().update_metadata(recording_id, update) { warn!("Failed to update recording metadata: {}", e); diff --git a/record-daemon/src/timeline/mod.rs b/record-daemon/src/timeline/mod.rs index fab6550..b678775 100644 --- a/record-daemon/src/timeline/mod.rs +++ b/record-daemon/src/timeline/mod.rs @@ -13,7 +13,7 @@ use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::lqp::{GameEvent, ItemBuild, RunePage, SummonerSpells}; +use crate::lqp::{GameEvent, RunePage, SummonerSpells}; /// A timeline of events for a recording. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -67,9 +67,9 @@ pub struct Timeline { /// Summoner spells. #[serde(default)] pub summoner_spells: Option, - /// Final item build at game end. + /// Raw end-of-game stats JSON from the API. #[serde(default)] - pub final_items: Option, + pub raw_end_game_stats: Option, /// All players in the game (puuid to summoner name mapping). #[serde(default)] pub all_players: Vec, @@ -104,7 +104,7 @@ impl Timeline { final_stats: None, runes: None, summoner_spells: None, - final_items: 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 775649f..ffe0542 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, ItemBuild, RunePage, SummonerSpells}; +use crate::lqp::{GameEvent, RunePage, SummonerSpells}; use crate::recording::RecordingResult; /// A timestamped event in the timeline. @@ -69,9 +69,9 @@ pub struct RecordingMetadata { /// Player's summoner spells. #[serde(default)] pub summoner_spells: Option, - /// Final item build at game end. + /// Raw end-of-game stats JSON from the API. #[serde(default)] - pub final_items: Option, + pub raw_end_game_stats: Option, /// All players in the game (puuid -> summoner name mapping). #[serde(default)] pub all_players: Vec, @@ -148,7 +148,7 @@ pub struct MetadataUpdate { pub final_stats: Option, pub runes: Option, pub summoner_spells: Option, - pub final_items: Option, + pub raw_end_game_stats: Option, pub all_players: Vec, } @@ -172,7 +172,7 @@ impl RecordingMetadata { final_stats: None, runes: None, summoner_spells: None, - final_items: None, + raw_end_game_stats: None, all_players: Vec::new(), start_time: result.start_time, end_time: Some(result.end_time), @@ -239,7 +239,7 @@ impl TimelineStore { final_stats: None, runes: None, summoner_spells: None, - final_items: None, + raw_end_game_stats: None, all_players: Vec::new(), start_time: Utc::now(), end_time: None, @@ -300,7 +300,7 @@ impl TimelineStore { final_stats: None, runes: None, summoner_spells: None, - final_items: None, + raw_end_game_stats: None, all_players: Vec::new(), start_time: result.start_time, end_time: Some(result.end_time), @@ -386,8 +386,8 @@ impl TimelineStore { if let Some(summoner_spells) = update.summoner_spells { metadata.summoner_spells = Some(summoner_spells); } - if let Some(final_items) = update.final_items { - metadata.final_items = Some(final_items); + 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; @@ -442,7 +442,7 @@ impl TimelineStore { final_stats: metadata.final_stats.clone(), runes: metadata.runes.clone(), summoner_spells: metadata.summoner_spells.clone(), - final_items: metadata.final_items.clone(), + raw_end_game_stats: metadata.raw_end_game_stats.clone(), all_players: metadata.all_players.clone(), }) } @@ -493,7 +493,7 @@ impl TimelineStore { final_stats: metadata.final_stats, runes: metadata.runes, summoner_spells: metadata.summoner_spells, - final_items: metadata.final_items, + raw_end_game_stats: metadata.raw_end_game_stats, all_players: metadata.all_players, }; @@ -535,7 +535,7 @@ impl TimelineStore { final_stats: None, runes: None, summoner_spells: None, - final_items: None, + raw_end_game_stats: None, all_players: Vec::new(), start_time: timeline.start_time, end_time: timeline.end_time, diff --git a/tauri-app/src/components/GameHistory.vue b/tauri-app/src/components/GameHistory.vue index 8a0464a..154f663 100644 --- a/tauri-app/src/components/GameHistory.vue +++ b/tauri-app/src/components/GameHistory.vue @@ -1,7 +1,7 @@