record-daemon: remove data in state machine
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m11s

This commit is contained in:
2026-03-26 14:00:04 +01:00
parent b94101c4d2
commit 179678f95e
2 changed files with 101 additions and 168 deletions

View File

@@ -353,26 +353,25 @@ impl Daemon {
if let Some(_new_state) = self.state_machine.transition(transition.clone()) { if let Some(_new_state) = self.state_machine.transition(transition.clone()) {
// Handle recording start/stop // Handle recording start/stop
match transition { match transition {
StateTransition::GameStarted { StateTransition::GameStarted => {
game_id, // Extract data from the GameStart event that was stored in the timeline
champion, let (game_id, queue_type, queue_id, game_mode, map_name, session) =
queue_type, if let GameEvent::GameStart(ref info) = event {
queue_id, (
game_mode, info.game_id,
map_name, info.queue_type.clone(),
team, info.queue_id,
summoner_name, info.game_mode.clone(),
puuid: transition_puuid, info.map_name.clone(),
runes: transition_runes, info.session.clone(),
summoner_spells: transition_summoner_spells, )
} => { } else {
(0, None, None, None, None, None)
};
info!( info!(
"[EVENT_HANDLER] GameStarted transition - game_id: {}, champion: {:?}, queue_type: {:?}, game_mode: {:?}", "[EVENT_HANDLER] GameStarted transition - game_id: {}, queue_type: {:?}, game_mode: {:?}",
game_id, champion, queue_type, game_mode game_id, queue_type, game_mode
);
info!(
"[EVENT_HANDLER] Transition provided: puuid={:?}, runes={:?}, summoner_spells={:?}",
transition_puuid, transition_runes, transition_summoner_spells
); );
// If already recording, stop the current recording first // If already recording, stop the current recording first
@@ -388,51 +387,56 @@ impl Daemon {
info!("[EVENT_HANDLER] Calling start_recording..."); info!("[EVENT_HANDLER] Calling start_recording...");
// Wrap the start_recording call to catch any panics // Fetch player game metadata (puuid, runes, summoner spells)
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
let player_metadata = let player_metadata =
self.lqp_client.fetch_player_game_metadata().await.ok(); self.lqp_client.fetch_player_game_metadata().await.ok();
let (fetched_puuid, fetched_runes, fetched_summoner_spells) = let (puuid, runes, summoner_spells, champion_id, team, summoner_name) =
player_metadata.map_or((None, None, None), |m| { player_metadata.map_or((None, None, None, None, None, None), |m| {
(m.puuid, m.runes, m.summoner_spells) (
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 // If we have puuid and session, extract player-specific data from session
let final_puuid = transition_puuid.or(fetched_puuid); let (champion, final_team, final_summoner_name) = if let Some(ref p) = puuid
let final_runes = transition_runes.or(fetched_runes); {
let final_summoner_spells = if let Some(ref sess) = session {
transition_summoner_spells.or(fetched_summoner_spells); 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!( info!(
"[EVENT_HANDLER] Final values: puuid={:?}, runes={:?}, summoner_spells={:?}", "[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 // Fetch all players identities for puuid mapping
@@ -452,15 +456,15 @@ impl Daemon {
// Build game metadata for timeline // Build game metadata for timeline
let metadata_update = record_daemon::timeline::MetadataUpdate { let metadata_update = record_daemon::timeline::MetadataUpdate {
queue_type: queue_type.clone(), queue_type,
queue_id, queue_id,
game_mode: game_mode.clone(), game_mode,
map_name: map_name.clone(), map_name,
team, team: final_team,
summoner_name: summoner_name.clone(), summoner_name: final_summoner_name,
puuid: final_puuid, puuid,
runes: final_runes, runes,
summoner_spells: final_summoner_spells, summoner_spells,
all_players: all_players_info, all_players: all_players_info,
..Default::default() ..Default::default()
}; };
@@ -468,7 +472,7 @@ impl Daemon {
if let Err(e) = self if let Err(e) = self
.start_recording_with_metadata( .start_recording_with_metadata(
game_id, game_id,
champion_name.as_deref(), champion.as_deref(),
metadata_update, metadata_update,
) )
.await .await
@@ -480,14 +484,8 @@ impl Daemon {
info!("[EVENT_HANDLER] start_recording completed successfully"); info!("[EVENT_HANDLER] start_recording completed successfully");
} }
} }
StateTransition::GameEnded { StateTransition::GameEnded => {
game_end_info, info!("[EVENT_HANDLER] GameEnded transition");
final_items: _,
} => {
info!(
"[EVENT_HANDLER] GameEnded transition with info: {:?}",
game_end_info
);
// Fetch final items before stopping // Fetch final items before stopping
let fetched_final_items = let fetched_final_items =

View File

@@ -1,4 +1,7 @@
//! Daemon state machine implementation. //! Daemon state machine implementation.
//!
//! Only tracks state
//! All game data is stored in events and processed at game end.
use std::sync::Arc; use std::sync::Arc;
@@ -6,7 +9,7 @@ use parking_lot::RwLock;
use tracing::{info, trace, warn}; use tracing::{info, trace, warn};
use super::DaemonStatus; use super::DaemonStatus;
use crate::lqp::{GameEndInfo, GameEvent, GameflowPhase, ItemBuild, RunePage, SummonerSpells}; use crate::lqp::{GameEvent, GameflowPhase};
/// Internal daemon state. /// Internal daemon state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -36,36 +39,19 @@ impl From<DaemonState> for DaemonStatus {
} }
/// State transition event. /// State transition event.
///
/// Simplified: Only tracks state changes, no data passing.
/// Data is stored in events and processed at game end.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum StateTransition { pub enum StateTransition {
/// League Client started. /// League Client started.
ClientStarted, ClientStarted,
/// League Client stopped. /// League Client stopped.
ClientStopped, ClientStopped,
/// Game started. /// Game started
GameStarted { GameStarted,
game_id: u64, /// Game ended
champion: Option<String>, GameEnded,
queue_type: Option<String>,
queue_id: Option<u32>,
game_mode: Option<String>,
map_name: Option<String>,
team: Option<u32>,
summoner_name: Option<String>,
/// Player's PUUID
puuid: Option<String>,
/// Rune page at game start
runes: Option<RunePage>,
/// Summoner spells
summoner_spells: Option<SummonerSpells>,
},
/// Game ended.
GameEnded {
/// Game end info with stats from WebSocket.
game_end_info: Option<GameEndInfo>,
/// Final items
final_items: Option<ItemBuild>,
},
/// Error occurred. /// Error occurred.
Error(String), Error(String),
/// Error recovered. /// Error recovered.
@@ -153,13 +139,11 @@ impl DaemonStateMachine {
// Update related state // Update related state
match &transition { match &transition {
StateTransition::GameStarted { StateTransition::GameStarted => {
game_id, champion, .. // Game ID and champion are now tracked in events, not state
} => {
*self.current_game_id.write() = Some(*game_id);
*self.current_champion.write() = champion.clone();
} }
StateTransition::GameEnded { .. } => { StateTransition::GameEnded => {
// Clear state tracking
*self.current_game_id.write() = None; *self.current_game_id.write() = None;
*self.current_champion.write() = None; *self.current_champion.write() = None;
} }
@@ -189,20 +173,14 @@ impl DaemonStateMachine {
// From Monitoring // From Monitoring
(DaemonState::Monitoring, StateTransition::ClientStopped) => Some(DaemonState::Idle), (DaemonState::Monitoring, StateTransition::ClientStopped) => Some(DaemonState::Idle),
(DaemonState::Monitoring, StateTransition::GameStarted { .. }) => { (DaemonState::Monitoring, StateTransition::GameStarted) => Some(DaemonState::Recording),
Some(DaemonState::Recording)
}
(DaemonState::Monitoring, StateTransition::Error(_)) => Some(DaemonState::Error), (DaemonState::Monitoring, StateTransition::Error(_)) => Some(DaemonState::Error),
(DaemonState::Monitoring, StateTransition::Shutdown) => Some(DaemonState::ShuttingDown), (DaemonState::Monitoring, StateTransition::Shutdown) => Some(DaemonState::ShuttingDown),
// From Recording // From Recording
(DaemonState::Recording, StateTransition::GameEnded { .. }) => { (DaemonState::Recording, StateTransition::GameEnded) => Some(DaemonState::Monitoring),
Some(DaemonState::Monitoring)
}
// Allow GameStarted from Recording (handles case where GameEnded wasn't received) // Allow GameStarted from Recording (handles case where GameEnded wasn't received)
(DaemonState::Recording, StateTransition::GameStarted { .. }) => { (DaemonState::Recording, StateTransition::GameStarted) => Some(DaemonState::Recording),
Some(DaemonState::Recording)
}
(DaemonState::Recording, StateTransition::ClientStopped) => Some(DaemonState::Idle), (DaemonState::Recording, StateTransition::ClientStopped) => Some(DaemonState::Idle),
(DaemonState::Recording, StateTransition::Error(_)) => Some(DaemonState::Error), (DaemonState::Recording, StateTransition::Error(_)) => Some(DaemonState::Error),
(DaemonState::Recording, StateTransition::Shutdown) => Some(DaemonState::ShuttingDown), (DaemonState::Recording, StateTransition::Shutdown) => Some(DaemonState::ShuttingDown),
@@ -224,6 +202,9 @@ impl DaemonStateMachine {
} }
/// Process a game event and potentially trigger a transition. /// 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<StateTransition> { pub fn process_event(&self, event: &GameEvent) -> Option<StateTransition> {
trace!( trace!(
"Processing event in state {:?}: {:?}", "Processing event in state {:?}: {:?}",
@@ -232,33 +213,13 @@ impl DaemonStateMachine {
); );
match event { match event {
GameEvent::GameStart(info) => { GameEvent::GameStart(_) => Some(StateTransition::GameStarted),
Some(StateTransition::GameStarted { GameEvent::GameEnd(_) => Some(StateTransition::GameEnded),
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::PhaseChange(info) => { GameEvent::PhaseChange(info) => {
// Only trigger GameEnded on EndOfGame phase (stats are available by then) // 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 // The actual GameEnd event with stats comes from /lol-end-of-game/v1/eog-stats-block
if info.phase == "EndOfGame" && self.is_recording() { if info.phase == "EndOfGame" && self.is_recording() {
Some(StateTransition::GameEnded { Some(StateTransition::GameEnded)
game_end_info: None,
final_items: None,
})
} else { } else {
None None
} }
@@ -303,23 +264,9 @@ mod tests {
let machine = DaemonStateMachine::new(); let machine = DaemonStateMachine::new();
machine.transition(StateTransition::ClientStarted); machine.transition(StateTransition::ClientStarted);
let new_state = machine.transition(StateTransition::GameStarted { 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,
});
assert_eq!(new_state, Some(DaemonState::Recording)); 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] #[test]
@@ -327,19 +274,7 @@ mod tests {
let machine = DaemonStateMachine::new(); let machine = DaemonStateMachine::new();
// Can't start recording from Idle // Can't start recording from Idle
let result = machine.transition(StateTransition::GameStarted { 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,
});
assert_eq!(result, None); assert_eq!(result, None);
assert_eq!(machine.current_state(), DaemonState::Idle); assert_eq!(machine.current_state(), DaemonState::Idle);