diff --git a/record-daemon/src/main.rs b/record-daemon/src/main.rs index e26a385..e1944fa 100644 --- a/record-daemon/src/main.rs +++ b/record-daemon/src/main.rs @@ -353,33 +353,32 @@ impl Daemon { if let Some(_new_state) = self.state_machine.transition(transition.clone()) { // Handle recording start/stop match transition { - StateTransition::GameStarted { - game_id, - champion, - queue_type, - queue_id, - game_mode, - map_name, - team, - summoner_name, - puuid: transition_puuid, - runes: transition_runes, - summoner_spells: transition_summoner_spells, - } => { + 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) + }; + info!( - "[EVENT_HANDLER] GameStarted transition - game_id: {}, champion: {:?}, queue_type: {:?}, game_mode: {:?}", - game_id, champion, queue_type, game_mode - ); - info!( - "[EVENT_HANDLER] Transition provided: puuid={:?}, runes={:?}, summoner_spells={:?}", - transition_puuid, transition_runes, transition_summoner_spells - ); + "[EVENT_HANDLER] GameStarted transition - game_id: {}, queue_type: {:?}, game_mode: {:?}", + game_id, queue_type, game_mode + ); // If already recording, stop the current recording first if self.state_machine.is_recording() { info!( - "[EVENT_HANDLER] Stopping previous recording before starting new one" - ); + "[EVENT_HANDLER] Stopping previous recording before starting new one" + ); if let Err(e) = self.stop_recording().await { warn!("[EVENT_HANDLER] Failed to stop previous recording: {}", e); @@ -388,51 +387,56 @@ impl Daemon { info!("[EVENT_HANDLER] Calling start_recording..."); - // Wrap the start_recording call to catch any panics - let start_result = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - // We need to use a blocking approach here since we're in catch_unwind - // The actual async call happens outside - })); - - if let Err(panic_info) = start_result { - error!( - "[EVENT_HANDLER] PANIC before start_recording: {:?}", - panic_info - ); - - eprintln!( - "[EVENT_HANDLER] PANIC before start_recording: {:?}", - panic_info - ); - } - - // Get pre-game metadata to pass to recording - 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 - None // We only have champion_id, not name - }) - }); - - // Fetch player game metadata (runes, summoner spells, puuid) as fallback + // Fetch player game metadata (puuid, runes, summoner spells) let player_metadata = self.lqp_client.fetch_player_game_metadata().await.ok(); - let (fetched_puuid, fetched_runes, fetched_summoner_spells) = - player_metadata.map_or((None, None, None), |m| { - (m.puuid, m.runes, m.summoner_spells) + 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, + ) }); - // Use transition values first, then fall back to fetched values - let final_puuid = transition_puuid.or(fetched_puuid); - let final_runes = transition_runes.or(fetched_runes); - let final_summoner_spells = - transition_summoner_spells.or(fetched_summoner_spells); + // If we have puuid and session, extract player-specific data from session + let (champion, final_team, final_summoner_name) = 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()); + ( + champ_name.or_else(|| { + champion_id + .and_then(record_daemon::lqp::champion_id_to_name) + }), + team_id.or(team), + summoner.or(summoner_name), + ) + } else { + ( + champion_id.and_then(record_daemon::lqp::champion_id_to_name), + team, + summoner_name, + ) + } + } else { + ( + champion_id.and_then(record_daemon::lqp::champion_id_to_name), + team, + summoner_name, + ) + }; info!( "[EVENT_HANDLER] Final values: puuid={:?}, runes={:?}, summoner_spells={:?}", - final_puuid, final_runes, final_summoner_spells + puuid, runes, summoner_spells ); // Fetch all players identities for puuid mapping @@ -452,15 +456,15 @@ impl Daemon { // Build game metadata for timeline let metadata_update = record_daemon::timeline::MetadataUpdate { - queue_type: queue_type.clone(), + queue_type, queue_id, - game_mode: game_mode.clone(), - map_name: map_name.clone(), - team, - summoner_name: summoner_name.clone(), - puuid: final_puuid, - runes: final_runes, - summoner_spells: final_summoner_spells, + game_mode, + map_name, + team: final_team, + summoner_name: final_summoner_name, + puuid, + runes, + summoner_spells, all_players: all_players_info, ..Default::default() }; @@ -468,7 +472,7 @@ impl Daemon { if let Err(e) = self .start_recording_with_metadata( game_id, - champion_name.as_deref(), + champion.as_deref(), metadata_update, ) .await @@ -480,14 +484,8 @@ impl Daemon { info!("[EVENT_HANDLER] start_recording completed successfully"); } } - StateTransition::GameEnded { - game_end_info, - final_items: _, - } => { - info!( - "[EVENT_HANDLER] GameEnded transition with info: {:?}", - game_end_info - ); + StateTransition::GameEnded => { + info!("[EVENT_HANDLER] GameEnded transition"); // Fetch final items before stopping let fetched_final_items = diff --git a/record-daemon/src/state/machine.rs b/record-daemon/src/state/machine.rs index 41fc7c0..c00683e 100644 --- a/record-daemon/src/state/machine.rs +++ b/record-daemon/src/state/machine.rs @@ -1,4 +1,7 @@ //! Daemon state machine implementation. +//! +//! Only tracks state +//! All game data is stored in events and processed at game end. use std::sync::Arc; @@ -6,7 +9,7 @@ use parking_lot::RwLock; use tracing::{info, trace, warn}; use super::DaemonStatus; -use crate::lqp::{GameEndInfo, GameEvent, GameflowPhase, ItemBuild, RunePage, SummonerSpells}; +use crate::lqp::{GameEvent, GameflowPhase}; /// Internal daemon state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -36,36 +39,19 @@ impl From for DaemonStatus { } /// State transition event. +/// +/// Simplified: Only tracks state changes, no data passing. +/// Data is stored in events and processed at game end. #[derive(Debug, Clone)] pub enum StateTransition { /// League Client started. ClientStarted, /// League Client stopped. ClientStopped, - /// Game started. - GameStarted { - game_id: u64, - champion: Option, - queue_type: Option, - queue_id: Option, - game_mode: Option, - map_name: Option, - team: Option, - summoner_name: Option, - /// Player's PUUID - puuid: Option, - /// Rune page at game start - runes: Option, - /// Summoner spells - summoner_spells: Option, - }, - /// Game ended. - GameEnded { - /// Game end info with stats from WebSocket. - game_end_info: Option, - /// Final items - final_items: Option, - }, + /// Game started + GameStarted, + /// Game ended + GameEnded, /// Error occurred. Error(String), /// Error recovered. @@ -153,13 +139,11 @@ impl DaemonStateMachine { // Update related state match &transition { - StateTransition::GameStarted { - game_id, champion, .. - } => { - *self.current_game_id.write() = Some(*game_id); - *self.current_champion.write() = champion.clone(); + StateTransition::GameStarted => { + // Game ID and champion are now tracked in events, not state } - StateTransition::GameEnded { .. } => { + StateTransition::GameEnded => { + // Clear state tracking *self.current_game_id.write() = None; *self.current_champion.write() = None; } @@ -189,20 +173,14 @@ impl DaemonStateMachine { // From Monitoring (DaemonState::Monitoring, StateTransition::ClientStopped) => Some(DaemonState::Idle), - (DaemonState::Monitoring, StateTransition::GameStarted { .. }) => { - Some(DaemonState::Recording) - } + (DaemonState::Monitoring, StateTransition::GameStarted) => Some(DaemonState::Recording), (DaemonState::Monitoring, StateTransition::Error(_)) => Some(DaemonState::Error), (DaemonState::Monitoring, StateTransition::Shutdown) => Some(DaemonState::ShuttingDown), // From Recording - (DaemonState::Recording, StateTransition::GameEnded { .. }) => { - Some(DaemonState::Monitoring) - } + (DaemonState::Recording, StateTransition::GameEnded) => Some(DaemonState::Monitoring), // Allow GameStarted from Recording (handles case where GameEnded wasn't received) - (DaemonState::Recording, StateTransition::GameStarted { .. }) => { - Some(DaemonState::Recording) - } + (DaemonState::Recording, StateTransition::GameStarted) => Some(DaemonState::Recording), (DaemonState::Recording, StateTransition::ClientStopped) => Some(DaemonState::Idle), (DaemonState::Recording, StateTransition::Error(_)) => Some(DaemonState::Error), (DaemonState::Recording, StateTransition::Shutdown) => Some(DaemonState::ShuttingDown), @@ -224,6 +202,9 @@ impl DaemonStateMachine { } /// Process a game event and potentially trigger a transition. + /// + /// Only returns state transitions + /// All data is stored in events in the timeline and processed at game end. pub fn process_event(&self, event: &GameEvent) -> Option { trace!( "Processing event in state {:?}: {:?}", @@ -232,33 +213,13 @@ impl DaemonStateMachine { ); match event { - GameEvent::GameStart(info) => { - Some(StateTransition::GameStarted { - game_id: info.game_id, - champion: info.champion.clone(), - queue_type: info.queue_type.clone(), - queue_id: info.queue_id, - game_mode: info.game_mode.clone(), - map_name: info.map_name.clone(), - team: info.team, - summoner_name: info.summoner_name.clone(), - puuid: None, // Will be populated from client.fetch_player_game_metadata() - runes: None, // Will be populated from client.fetch_player_game_metadata() - summoner_spells: None, // Will be populated from client.fetch_player_game_metadata() - }) - } - GameEvent::GameEnd(info) => Some(StateTransition::GameEnded { - game_end_info: Some(info.clone()), - final_items: None, // Will be populated from client.fetch_final_items() - }), + GameEvent::GameStart(_) => Some(StateTransition::GameStarted), + GameEvent::GameEnd(_) => Some(StateTransition::GameEnded), GameEvent::PhaseChange(info) => { // Only trigger GameEnded on EndOfGame phase (stats are available by then) // The actual GameEnd event with stats comes from /lol-end-of-game/v1/eog-stats-block if info.phase == "EndOfGame" && self.is_recording() { - Some(StateTransition::GameEnded { - game_end_info: None, - final_items: None, - }) + Some(StateTransition::GameEnded) } else { None } @@ -303,23 +264,9 @@ mod tests { let machine = DaemonStateMachine::new(); machine.transition(StateTransition::ClientStarted); - let new_state = machine.transition(StateTransition::GameStarted { - game_id: 12345, - champion: Some("Ahri".to_string()), - queue_type: None, - queue_id: None, - game_mode: None, - map_name: None, - team: None, - summoner_name: None, - puuid: None, - runes: None, - summoner_spells: None, - }); + let new_state = machine.transition(StateTransition::GameStarted); assert_eq!(new_state, Some(DaemonState::Recording)); - assert_eq!(machine.current_game_id(), Some(12345)); - assert_eq!(machine.current_champion(), Some("Ahri".to_string())); } #[test] @@ -327,19 +274,7 @@ mod tests { let machine = DaemonStateMachine::new(); // Can't start recording from Idle - let result = machine.transition(StateTransition::GameStarted { - game_id: 12345, - champion: None, - queue_type: None, - queue_id: None, - game_mode: None, - map_name: None, - team: None, - summoner_name: None, - puuid: None, - runes: None, - summoner_spells: None, - }); + let result = machine.transition(StateTransition::GameStarted); assert_eq!(result, None); assert_eq!(machine.current_state(), DaemonState::Idle);