diff --git a/record-daemon/src/lqp/api_types.rs b/record-daemon/src/lqp/api_types.rs index 286598d..63e00ed 100644 --- a/record-daemon/src/lqp/api_types.rs +++ b/record-daemon/src/lqp/api_types.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; // ============================================================================= /// Response from `/lol-summoner/v1/current-summoner` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SummonerResponse { /// Summoner ID. @@ -51,7 +51,7 @@ pub struct SummonerResponse { // ============================================================================= /// Response from `/lol-gameflow/v1/session` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GameflowSessionResponse { /// Current gameflow phase. @@ -552,6 +552,80 @@ impl ChampionSelectResponse { } } +// ============================================================================= +// Aggregated Data Containers +// ============================================================================= + +/// Container for pre-game data collected from multiple API calls. +/// Stores raw API responses directly for maximum flexibility. +#[derive(Debug, Clone, Default)] +pub struct PreGameData { + /// Session data (map, game mode, queue info). + pub session: Option, + /// Summoner data (puuid, name). + pub summoner: Option, + /// Champion select data (champion, skin, team). + pub champion_select: Option, + /// Rune page data. + pub rune_page: Option, +} + +impl PreGameData { + /// Get the summoner name from any available source. + pub fn summoner_name(&self) -> Option<&str> { + self.summoner + .as_ref() + .and_then(|s| s.display_name.as_deref()) + .or_else(|| self.summoner.as_ref().and_then(|s| s.name.as_deref())) + } + + /// Get the PUUID. + pub fn puuid(&self) -> Option<&str> { + self.summoner.as_ref()?.puuid.as_deref() + } + + /// Get the champion ID from champion select. + pub fn champion_id(&self) -> Option { + let cs = self.champion_select.as_ref()?; + let player = cs.get_local_player_selection()?; + player.champion_id + } + + /// Get the team ID from champion select. + pub fn team(&self) -> Option { + let cs = self.champion_select.as_ref()?; + let player = cs.get_local_player_selection()?; + player.team + } + + /// Get the skin ID from champion select. + pub fn skin_id(&self) -> Option { + let cs = self.champion_select.as_ref()?; + let player = cs.get_local_player_selection()?; + player.skin_id + } + + /// Get the map name from session. + pub fn map_name(&self) -> Option<&str> { + self.session.as_ref()?.map.as_deref() + } + + /// Get the game mode from session. + pub fn game_mode(&self) -> Option<&str> { + self.session.as_ref()?.game_mode.as_deref() + } + + /// Get the queue ID from session. + pub fn queue_id(&self) -> Option { + self.session.as_ref()?.queue_id + } + + /// Get the rune page name. + pub fn rune_page_name(&self) -> Option<&str> { + self.rune_page.as_ref()?.name.as_deref() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/record-daemon/src/lqp/client.rs b/record-daemon/src/lqp/client.rs index 126db3c..a9ebe2b 100644 --- a/record-daemon/src/lqp/client.rs +++ b/record-daemon/src/lqp/client.rs @@ -12,14 +12,13 @@ use tracing::{debug, error, info, trace, warn}; use super::api_types::{ ActivePlayerResponse, ChampionSelectResponse, EndOfGameStatsResponse, GameflowSessionResponse, - PlayerListResponse, RunePageResponse, SummonerResponse, + PlayerListResponse, PreGameData, RunePageResponse, SummonerResponse, }; 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::mappings::{champion_id_to_name, spell_id_to_name}; -use super::metadata::{GameEndMetadata, PreGameMetadata}; use super::state::{ClientState, GameflowPhase}; use super::tls::create_insecure_tls_config; use super::websocket::parse_websocket_message; @@ -478,69 +477,47 @@ impl LqpClient { // Metadata Fetching Methods // ========================================================================= - /// Fetch pre-game metadata (champion, skin, runes, queue info). - pub async fn fetch_pregame_metadata(&self) -> Result { - let mut metadata = PreGameMetadata::default(); + /// Fetch pre-game data (stores raw API responses directly). + pub async fn fetch_pregame_data(&self) -> Result { + let mut data = PreGameData::default(); - // Get session info for queue type and game mode - if let Ok(session) = self.get_session_typed().await { - metadata.map_name = session.map; - metadata.game_mode = session.game_mode; - metadata.queue_id = session.queue_id.map(|id| id as u32); + // Fetch all API responses in parallel where possible + let (session, summoner, champ_select, rune_page) = tokio::join!( + self.get_session_typed(), + self.get_summoner_typed(), + self.get_champion_select_typed(), + self.get_rune_page_typed() + ); + + // Store session data + if let Ok(session) = session { + data.session = Some(session); } - // Get summoner info (including puuid) - if let Ok(summoner) = self.get_summoner_typed().await { - metadata.summoner_name = summoner.display_name; - if let Some(puuid) = &summoner.puuid { + // Store summoner data and update state + if let Ok(summoner) = summoner { + if let Some(ref puuid) = summoner.puuid { self.state.write().await.local_puuid = Some(puuid.clone()); } - metadata.local_puuid = summoner.puuid; + data.summoner = Some(summoner); } - // Get champion select info - if let Ok(champ_select) = self.get_champion_select_typed().await { - if let Some(player) = champ_select.get_local_player_selection() { - metadata.champion_id = player.champion_id.map(|id| id as u32); - metadata.team = player.team.map(|id| id as u32); - metadata.skin_id = player.skin_id.map(|id| id as u32); - } + // Store champion select data + if let Ok(champ_select) = champ_select { + data.champion_select = Some(champ_select); } - // Get rune page - if let Ok(rune_page) = self.get_rune_page_typed().await { - metadata.rune_page_name = rune_page.name; + // Store rune page data + if let Ok(rune_page) = rune_page { + data.rune_page = Some(rune_page); } - Ok(metadata) + Ok(data) } - /// Fetch end-of-game stats. - pub async fn fetch_game_end_stats(&self) -> Result { - let mut metadata = GameEndMetadata::default(); - - if let Ok(stats) = self.get_game_stats_typed().await { - metadata.victory = Some(stats.is_victory()); - metadata.game_duration = stats.game_length.unwrap_or(0.0); - metadata.match_id = stats.match_id.map(|id| id.to_string()); - - // Extract player stats - if let Some(player) = stats.get_local_player() { - if let Some(player_stats) = &player.stats { - metadata.kills = player_stats.champions_killed.unwrap_or(0) as u32; - metadata.deaths = player_stats.num_deaths.unwrap_or(0) as u32; - metadata.assists = player_stats.assists.unwrap_or(0) as u32; - metadata.creep_score = player_stats.minions_killed.unwrap_or(0) as u32; - metadata.gold_earned = player_stats.gold_earned.unwrap_or(0) as u32; - metadata.damage_dealt = - player_stats.total_damage_dealt_to_champions.unwrap_or(0); - metadata.damage_taken = player_stats.total_damage_taken.unwrap_or(0); - metadata.vision_score = player_stats.vision_score.unwrap_or(0.0); - } - } - } - - Ok(metadata) + /// Fetch end-of-game stats (returns raw API response). + 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. diff --git a/record-daemon/src/lqp/metadata.rs b/record-daemon/src/lqp/metadata.rs deleted file mode 100644 index 99b754f..0000000 --- a/record-daemon/src/lqp/metadata.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Metadata structures for pre-game and end-of-game data. -//! -//! Contains structures for capturing game information before and after matches. - -/// Pre-game metadata fetched before the game starts. -#[derive(Debug, Clone, Default)] -pub struct PreGameMetadata { - /// Champion ID selected. - pub champion_id: Option, - /// Skin ID selected. - pub skin_id: Option, - /// Name of the rune page. - pub rune_page_name: Option, - /// Summoner name. - pub summoner_name: Option, - /// Queue type (e.g., "RANKED_SOLO_5x5"). - pub queue_type: Option, - /// Queue ID. - pub queue_id: Option, - /// Game mode (e.g., "CLASSIC", "ARAM"). - pub game_mode: Option, - /// Map name. - pub map_name: Option, - /// Team ID (100 or 200). - pub team: Option, - /// Local player's PUUID. - pub local_puuid: Option, -} - -/// End-of-game metadata fetched after the game ends. -#[derive(Debug, Clone, Default)] -pub struct GameEndMetadata { - /// Whether the player won. - pub victory: Option, - /// Match ID. - pub match_id: Option, - /// Number of kills. - pub kills: u32, - /// Number of deaths. - pub deaths: u32, - /// Number of assists. - pub assists: u32, - /// Creep score (minions killed). - pub creep_score: u32, - /// Gold earned. - pub gold_earned: u32, - /// Damage dealt to champions. - pub damage_dealt: u64, - /// Damage taken. - pub damage_taken: u64, - /// Vision score. - pub vision_score: f64, - /// Game duration in seconds. - pub game_duration: f64, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pre_game_metadata_default() { - let metadata = PreGameMetadata::default(); - assert!(metadata.champion_id.is_none()); - assert!(metadata.skin_id.is_none()); - assert!(metadata.summoner_name.is_none()); - } - - #[test] - fn test_game_end_metadata_default() { - let metadata = GameEndMetadata::default(); - assert!(metadata.victory.is_none()); - assert_eq!(metadata.kills, 0); - assert_eq!(metadata.deaths, 0); - assert_eq!(metadata.assists, 0); - } -} diff --git a/record-daemon/src/lqp/mod.rs b/record-daemon/src/lqp/mod.rs index 19ceacf..4f8c1f0 100644 --- a/record-daemon/src/lqp/mod.rs +++ b/record-daemon/src/lqp/mod.rs @@ -10,7 +10,6 @@ mod endpoints; mod events; mod items; mod mappings; -mod metadata; mod state; mod tls; mod websocket; @@ -18,7 +17,7 @@ mod websocket; pub use api_types::{ ActivePlayerResponse, ChampionSelectPlayer, ChampionSelectResponse, EndOfGamePlayer, EndOfGameStatsResponse, EndOfGameTeam, GameData, GameflowSessionResponse, LiveClientItem, - LiveClientPlayer, PlayerListResponse, PlayerStats, QueueData, RunePageResponse, + LiveClientPlayer, PlayerListResponse, PlayerStats, PreGameData, QueueData, RunePageResponse, SummonerResponse, SummonerSpellsData, TeamPlayer, Timers, }; pub use auth::{LockfileCredentials, LockfileWatcher}; @@ -37,6 +36,5 @@ pub use events::{ }; 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 metadata::{GameEndMetadata, PreGameMetadata}; 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 cfe2da3..e26a385 100644 --- a/record-daemon/src/main.rs +++ b/record-daemon/src/main.rs @@ -56,8 +56,8 @@ struct Daemon { event_mapper: Arc>, /// Current recording ID (if recording). current_recording_id: Arc>>, - /// Pre-game metadata (collected before game starts). - pregame_metadata: Arc>>, + /// Pre-game data (collected before game starts). + pregame_data: Arc>>, /// IPC server. ipc_server: Option, /// Shutdown signal. @@ -77,7 +77,7 @@ impl Daemon { timeline_store: Arc::new(RwLock::new(TimelineStore::new())), event_mapper: Arc::new(RwLock::new(EventMapper::new())), current_recording_id: Arc::new(RwLock::new(None)), - pregame_metadata: Arc::new(RwLock::new(None)), + pregame_data: Arc::new(RwLock::new(None)), ipc_server: None, shutdown_tx, } @@ -229,17 +229,17 @@ impl Daemon { async fn handle_game_event(&self, event: GameEvent) -> Result<()> { info!("[EVENT_HANDLER] Game event received: {:?}", event); - // Handle pre-game metadata collection + // Handle pre-game data collection match &event { GameEvent::PhaseChange(info) if info.phase == "ChampSelect" => { - info!("[EVENT_HANDLER] Champion select started, fetching pre-game metadata"); - match self.lqp_client.fetch_pregame_metadata().await { - Ok(metadata) => { - info!("[EVENT_HANDLER] Pre-game metadata fetched: {:?}", metadata); - *self.pregame_metadata.write() = Some(metadata); + info!("[EVENT_HANDLER] Champion select started, fetching pre-game data"); + match self.lqp_client.fetch_pregame_data().await { + Ok(data) => { + info!("[EVENT_HANDLER] Pre-game data fetched: {:?}", data); + *self.pregame_data.write() = Some(data); } Err(e) => { - warn!("[EVENT_HANDLER] Failed to fetch pre-game metadata: {}", e); + warn!("[EVENT_HANDLER] Failed to fetch pre-game data: {}", e); } } } @@ -248,13 +248,8 @@ impl Daemon { "[EVENT_HANDLER] Local player picked champion: {}", pick.champion_name ); - let mut metadata = self.pregame_metadata.write(); - if let Some(ref mut meta) = *metadata { - meta.champion_id = Some(pick.champion_id); - if let Some(_skin_name) = &pick.skin_name { - // Store skin name for later use - } - } + // Champion pick info is now stored in champion_select response + // No need to manually update - refetch if needed } GameEvent::GameStart(info) => { info!( @@ -262,13 +257,13 @@ impl Daemon { info.queue_type, info.game_mode, info.map_name ); - // Update pre-game metadata with game start info - let mut pregame = self.pregame_metadata.write(); + // Update pre-game data with game start info if needed + let mut pregame = self.pregame_data.write(); // Extract player-specific data from session using puuid - let (champion_id, team, summoner_name) = if let Some(ref session) = info.session { - // Get puuid from pregame metadata (fetched earlier) - let puuid = pregame.as_ref().and_then(|m| m.local_puuid.as_ref()); + let (_champion_id, _team, summoner_name) = if let Some(ref session) = info.session { + // Get puuid from pregame data (fetched earlier) + let puuid = pregame.as_ref().and_then(|d| d.puuid()); if let Some(puuid) = puuid { // Use the puuid to find the correct player's data @@ -289,45 +284,24 @@ impl Daemon { (None, None, None) }; - if let Some(ref mut meta) = *pregame { - // Fill in champion_id from session if not already set - if meta.champion_id.is_none() { - meta.champion_id = champion_id; - } - // Fill in team from session if not already set - if meta.team.is_none() { - meta.team = team; - } - // Fill in summoner_name from session if not already set - if meta.summoner_name.is_none() { - meta.summoner_name = summoner_name; - } - // Fill in queue info - if meta.queue_type.is_none() { - meta.queue_type = info.queue_type.clone(); - } - if meta.queue_id.is_none() { - meta.queue_id = info.queue_id; - } - if meta.game_mode.is_none() { - meta.game_mode = info.game_mode.clone(); - } - if meta.map_name.is_none() { - meta.map_name = info.map_name.clone(); - } - } else { - // Create pre-game metadata from game start info - *pregame = Some(record_daemon::lqp::PreGameMetadata { - summoner_name, - champion_id, - skin_id: None, - rune_page_name: None, - queue_type: info.queue_type.clone(), - queue_id: info.queue_id, - game_mode: info.game_mode.clone(), - map_name: info.map_name.clone(), - team, - local_puuid: None, + // If we don't have pre-game data, create minimal from game start info + if pregame.is_none() { + use record_daemon::lqp::{GameflowSessionResponse, PreGameData}; + *pregame = Some(PreGameData { + session: Some(GameflowSessionResponse { + map: info.map_name.clone(), + game_mode: info.game_mode.clone(), + queue_id: info.queue_id.map(|id| id as u64), + ..Default::default() + }), + summoner: summoner_name.map(|name| { + use record_daemon::lqp::SummonerResponse; + SummonerResponse { + display_name: Some(name), + ..Default::default() + } + }), + ..Default::default() }); } } @@ -434,7 +408,7 @@ impl Daemon { } // Get pre-game metadata to pass to recording - let pregame = self.pregame_metadata.read().clone(); + let pregame = self.pregame_data.read().clone(); let champion_name = champion.or_else(|| { pregame.as_ref().and({ // Try to get champion name from metadata if not provided @@ -519,49 +493,16 @@ impl Daemon { let fetched_final_items = self.lqp_client.fetch_final_items().await.ok().flatten(); - // Convert GameEndInfo to GameEndMetadata if available - let game_end_metadata = - game_end_info.map(|info| record_daemon::lqp::GameEndMetadata { - victory: Some(info.victory), - match_id: None, - kills: info.stats.as_ref().map(|s| s.kills).unwrap_or(0), - deaths: info.stats.as_ref().map(|s| s.deaths).unwrap_or(0), - assists: info.stats.as_ref().map(|s| s.assists).unwrap_or(0), - creep_score: info - .stats - .as_ref() - .map(|s| s.minions_killed) - .unwrap_or(0), - gold_earned: info - .stats - .as_ref() - .map(|s| s.gold_earned) - .unwrap_or(0), - damage_dealt: info - .stats - .as_ref() - .map(|s| s.damage_dealt) - .unwrap_or(0), - damage_taken: info - .stats - .as_ref() - .map(|s| s.damage_taken) - .unwrap_or(0), - vision_score: info - .stats - .as_ref() - .map(|s| s.vision_score) - .unwrap_or(0.0), - game_duration: info.duration, - }); + // Fetch end-of-game stats from API + let game_end_stats = self.lqp_client.fetch_game_end_stats().await.ok(); info!( - "[EVENT_HANDLER] Game end metadata from event: {:?}", - game_end_metadata + "[EVENT_HANDLER] Game end stats from API: {:?}", + game_end_stats ); if let Err(e) = self - .stop_recording_with_metadata(game_end_metadata, fetched_final_items) + .stop_recording_with_metadata(game_end_stats, fetched_final_items) .await { error!("[EVENT_HANDLER] Failed to stop recording: {}", e); @@ -569,8 +510,8 @@ impl Daemon { // Don't propagate error - keep daemon running } - // Clear pre-game metadata - *self.pregame_metadata.write() = None; + // Clear pre-game data + *self.pregame_data.write() = None; } _ => {} } @@ -656,10 +597,10 @@ impl Daemon { self.stop_recording_with_metadata(None, None).await } - /// Stop recording with optional game end metadata. + /// Stop recording with optional game end stats. async fn stop_recording_with_metadata( &self, - game_end_metadata: Option, + game_end_stats: Option, final_items: Option, ) -> Result<()> { info!("Stopping recording"); @@ -671,7 +612,7 @@ 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_metadata = self.pregame_metadata.read().clone(); + let pregame_data = self.pregame_data.read().clone(); // Use spawn_blocking to avoid blocking the async runtime tokio::task::spawn_blocking(move || { @@ -698,35 +639,43 @@ impl Daemon { // Update metadata if we have it let mut update = record_daemon::timeline::MetadataUpdate::default(); - // Add pre-game metadata - if let Some(pregame) = pregame_metadata { + // 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); + 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; - update.queue_type = pregame.queue_type; - update.queue_id = pregame.queue_id; - update.game_mode = pregame.game_mode; - update.map_name = pregame.map_name; - update.team = pregame.team; + 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 metadata - if let Some(end_meta) = game_end_metadata { - update.match_id = end_meta.match_id; - update.victory = end_meta.victory; - update.final_stats = Some(record_daemon::timeline::GameFinalStats { - kills: end_meta.kills, - deaths: end_meta.deaths, - assists: end_meta.assists, - creep_score: end_meta.creep_score, - gold_earned: end_meta.gold_earned, - damage_dealt: end_meta.damage_dealt, - damage_taken: end_meta.damage_taken, - vision_score: end_meta.vision_score, - game_duration: end_meta.game_duration, - }); + // 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()); + + // 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), + }); + } + } } // Add final items diff --git a/record-daemon/src/recording/encoder.rs b/record-daemon/src/recording/encoder.rs index 5233f39..030b2d0 100644 --- a/record-daemon/src/recording/encoder.rs +++ b/record-daemon/src/recording/encoder.rs @@ -299,10 +299,16 @@ pub fn detect_hardware_encoders() -> Vec { info!("[ENCODER_DETECT] Starting hardware encoder detection..."); // Software encoding is always available= - let mut capabilities = vec![EncoderCapability::Software]; + let capabilities = vec![EncoderCapability::Software]; + + #[cfg(target_os = "linux")] + { + let _capabilities = [EncoderCapability::Software]; + } #[cfg(target_os = "windows")] { + let mut capabilities = vec![EncoderCapability::Software]; let gpu_vendors = detect_gpu_vendors(); if gpu_vendors.contains(&GpuVendor::Nvidia) {