record-daemon: add record raw events, subscribe lp change events
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m0s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m0s
This commit is contained in:
@@ -14,7 +14,7 @@ use super::endpoints;
|
||||
use super::events::GameEvent;
|
||||
use super::state::{ClientState, GameflowPhase};
|
||||
use super::tls::create_insecure_tls_config;
|
||||
use super::websocket::parse_websocket_message;
|
||||
use super::websocket::{parse_websocket_message, ParsedEvent};
|
||||
use crate::error::{LqpError, Result};
|
||||
|
||||
/// LQP Client for League Client communication.
|
||||
@@ -24,7 +24,7 @@ pub struct LqpClient {
|
||||
/// Current client state.
|
||||
state: Arc<RwLock<ClientState>>,
|
||||
/// Event broadcaster.
|
||||
event_sender: broadcast::Sender<GameEvent>,
|
||||
event_sender: broadcast::Sender<ParsedEvent>,
|
||||
/// HTTP client for REST API.
|
||||
http_client: reqwest::Client,
|
||||
/// Shutdown signal.
|
||||
@@ -54,7 +54,7 @@ impl LqpClient {
|
||||
}
|
||||
|
||||
/// Get a subscriber for game events.
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<GameEvent> {
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<ParsedEvent> {
|
||||
self.event_sender.subscribe()
|
||||
}
|
||||
|
||||
@@ -184,12 +184,12 @@ impl LqpClient {
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(event) = parse_websocket_message(&text) {
|
||||
if let Some(parsed) = parse_websocket_message(&text) {
|
||||
// Update state based on event
|
||||
Self::update_state_from_event(&state, &event).await;
|
||||
Self::update_state_from_event(&state, &parsed.event).await;
|
||||
|
||||
// Check for duplicate GameStart events
|
||||
if let GameEvent::GameStart(ref info) = event {
|
||||
if let GameEvent::GameStart(ref info) = parsed.event {
|
||||
let mut last_game_id = last_emitted_game_id.write().await;
|
||||
if *last_game_id == Some(info.game_id) {
|
||||
info!(
|
||||
@@ -202,12 +202,12 @@ impl LqpClient {
|
||||
}
|
||||
|
||||
// Reset last_emitted_game_id on GameEnd to allow new game starts
|
||||
if let GameEvent::GameEnd(_) = &event {
|
||||
if let GameEvent::GameEnd(_) = &parsed.event {
|
||||
*last_emitted_game_id.write().await = None;
|
||||
}
|
||||
|
||||
// Broadcast event
|
||||
if event_sender.send(event.clone()).is_err() {
|
||||
if event_sender.send(parsed).is_err() {
|
||||
trace!("No event subscribers");
|
||||
}
|
||||
}
|
||||
@@ -217,12 +217,12 @@ impl LqpClient {
|
||||
// Try to parse as UTF-8
|
||||
if let Ok(text) = String::from_utf8(data) {
|
||||
if !text.is_empty() {
|
||||
if let Some(event) = parse_websocket_message(&text) {
|
||||
if let Some(parsed) = parse_websocket_message(&text) {
|
||||
// Update state based on event
|
||||
Self::update_state_from_event(&state, &event).await;
|
||||
Self::update_state_from_event(&state, &parsed.event).await;
|
||||
|
||||
// Check for duplicate GameStart events
|
||||
if let GameEvent::GameStart(ref info) = event {
|
||||
if let GameEvent::GameStart(ref info) = parsed.event {
|
||||
let mut last_game_id = last_emitted_game_id.write().await;
|
||||
if *last_game_id == Some(info.game_id) {
|
||||
info!(
|
||||
@@ -235,12 +235,12 @@ impl LqpClient {
|
||||
}
|
||||
|
||||
// Reset last_emitted_game_id on GameEnd to allow new game starts
|
||||
if let GameEvent::GameEnd(_) = &event {
|
||||
if let GameEvent::GameEnd(_) = &parsed.event {
|
||||
*last_emitted_game_id.write().await = None;
|
||||
}
|
||||
|
||||
// Broadcast event
|
||||
if event_sender.send(event.clone()).is_err() {
|
||||
if event_sender.send(parsed).is_err() {
|
||||
trace!("No event subscribers");
|
||||
}
|
||||
}
|
||||
@@ -448,6 +448,16 @@ impl LqpClient {
|
||||
pub async fn fetch_raw_end_game_stats(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::GAME_STATS).await
|
||||
}
|
||||
|
||||
/// Fetch ranked stats as JSON (for LP tracking).
|
||||
pub async fn fetch_ranked_stats(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::RANKED_STATS).await
|
||||
}
|
||||
|
||||
/// Fetch current ranked stats as JSON.
|
||||
pub async fn fetch_current_ranked_stats(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::CURRENT_RANKED_STATS).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LqpClient {
|
||||
|
||||
@@ -22,6 +22,12 @@ pub const ALL_RUNE_PAGES: &str = "/lol-perks/v1/pages";
|
||||
pub const MATCH_HISTORY: &str = "/lol-match-history/v1/products/lol/current-summoner/matches";
|
||||
/// Live client data endpoint (all game data).
|
||||
pub const LIVE_CLIENT_DATA: &str = "/liveclientdata/allgamedata";
|
||||
/// Ranked stats endpoint (for LP tracking).
|
||||
pub const RANKED_STATS: &str = "/lol-ranked/v1/ranked-stats";
|
||||
/// Current ranked stats for the local player.
|
||||
pub const CURRENT_RANKED_STATS: &str = "/lol-ranked/v1/current-ranked-stats";
|
||||
/// LP change notification endpoint.
|
||||
pub const LP_CHANGE_NOTIFICATION: &str = "/lol-ranked/v1/current-lp-change-notification";
|
||||
/// Live client active player endpoint.
|
||||
pub const LIVE_CLIENT_DATA_ACTIVE_PLAYER: &str = "/liveclientdata/activeplayer";
|
||||
/// Live client player list endpoint.
|
||||
@@ -38,6 +44,8 @@ pub const SUBSCRIBE_ENDPOINTS: &[&str] = &[
|
||||
CHAMPION_SELECT,
|
||||
"/lol-lobby/v2/lobby",
|
||||
GAME_STATS,
|
||||
RANKED_STATS,
|
||||
LP_CHANGE_NOTIFICATION,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -49,6 +49,10 @@ pub enum GameEvent {
|
||||
#[serde(rename = "lcu-phase-change")]
|
||||
PhaseChange(PhaseChangeInfo),
|
||||
|
||||
/// LP changed after a ranked game.
|
||||
#[serde(rename = "lcu-lp-change")]
|
||||
LpChange(LpChangeInfo),
|
||||
|
||||
/// Unknown event type.
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
@@ -66,6 +70,66 @@ pub struct PhaseChangeInfo {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// LP change event data after a ranked game.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LpChangeInfo {
|
||||
/// Queue type (RANKED_SOLO_5x5, RANKED_FLEX_SR, etc.).
|
||||
pub queue_type: String,
|
||||
|
||||
/// LP change amount (positive for gain, negative for loss).
|
||||
pub lp_change: i32,
|
||||
|
||||
/// LP before the game.
|
||||
pub lp_before: i32,
|
||||
|
||||
/// LP after the game.
|
||||
pub lp_after: i32,
|
||||
|
||||
/// Current tier (IRON, BRONZE, SILVER, GOLD, PLATINUM, DIAMOND, MASTER, GRANDMASTER, CHALLENGER).
|
||||
pub tier: String,
|
||||
|
||||
/// Current division (I, II, III, IV).
|
||||
#[serde(default)]
|
||||
pub division: Option<String>,
|
||||
|
||||
/// Current league points.
|
||||
#[serde(default)]
|
||||
pub league_points: Option<i32>,
|
||||
|
||||
/// Whether the player is in a promotional series.
|
||||
#[serde(default)]
|
||||
pub in_promos: bool,
|
||||
|
||||
/// Promotional series progress (e.g., "W:2 L:1").
|
||||
#[serde(default)]
|
||||
pub promo_progress: Option<String>,
|
||||
|
||||
/// Number of wins in promo series.
|
||||
#[serde(default)]
|
||||
pub promo_wins: Option<u32>,
|
||||
|
||||
/// Number of losses in promo series.
|
||||
#[serde(default)]
|
||||
pub promo_losses: Option<u32>,
|
||||
|
||||
/// Total games played in this queue.
|
||||
#[serde(default)]
|
||||
pub total_games: Option<u32>,
|
||||
|
||||
/// Total wins in this queue.
|
||||
#[serde(default)]
|
||||
pub total_wins: Option<u32>,
|
||||
|
||||
/// Total losses in this queue.
|
||||
#[serde(default)]
|
||||
pub total_losses: Option<u32>,
|
||||
|
||||
/// Timestamp when LP change occurred.
|
||||
#[serde(default = "Utc::now")]
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Match found event data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -462,6 +526,18 @@ impl GameEvent {
|
||||
GameEvent::PhaseChange(info) => {
|
||||
format!("Phase changed to: {}", info.phase)
|
||||
}
|
||||
GameEvent::LpChange(lp) => {
|
||||
let sign = if lp.lp_change >= 0 { "+" } else { "" };
|
||||
format!(
|
||||
"LP Change: {}{} LP ({} {} {} -> {} LP)",
|
||||
sign,
|
||||
lp.lp_change,
|
||||
lp.tier,
|
||||
lp.division.as_deref().unwrap_or(""),
|
||||
lp.queue_type,
|
||||
lp.lp_after
|
||||
)
|
||||
}
|
||||
GameEvent::Unknown => "Unknown event".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -479,6 +555,7 @@ impl GameEvent {
|
||||
GameEvent::StatsUpdate(_) => "stats_update",
|
||||
GameEvent::GameEnd(_) => "game_end",
|
||||
GameEvent::PhaseChange(_) => "phase_change",
|
||||
GameEvent::LpChange(_) => "lp_change",
|
||||
GameEvent::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,4 @@ pub use events::{
|
||||
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
|
||||
};
|
||||
pub use state::{ClientState, GameflowPhase};
|
||||
pub use websocket::{parse_event_from_uri, parse_websocket_message};
|
||||
pub use websocket::{parse_event_from_uri, parse_websocket_message, ParsedEvent};
|
||||
|
||||
@@ -7,8 +7,19 @@ use tracing::{debug, info, warn};
|
||||
|
||||
use super::events::{GameEvent, GameflowSession};
|
||||
|
||||
/// Parse a WebSocket message into a game event.
|
||||
pub fn parse_websocket_message(text: &str) -> Option<GameEvent> {
|
||||
/// Parsed event with raw data preserved.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedEvent {
|
||||
/// The parsed game event.
|
||||
pub event: GameEvent,
|
||||
/// The raw JSON data from the API.
|
||||
pub raw_data: serde_json::Value,
|
||||
/// The URI of the endpoint that triggered this event.
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
/// Parse a WebSocket message into a parsed event with raw data.
|
||||
pub fn parse_websocket_message(text: &str) -> Option<ParsedEvent> {
|
||||
// Parse the message array format: [type, callback, data]
|
||||
let value: serde_json::Value = match serde_json::from_str(text) {
|
||||
Ok(v) => v,
|
||||
@@ -37,22 +48,33 @@ pub fn parse_websocket_message(text: &str) -> Option<GameEvent> {
|
||||
.get("eventType")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("Update");
|
||||
return parse_event_from_uri(
|
||||
&raw_event.uri,
|
||||
event_type,
|
||||
&serde_json::to_value(raw_event.data).unwrap_or_default(),
|
||||
);
|
||||
let raw_data =
|
||||
serde_json::to_value(raw_event.data.clone()).unwrap_or_default();
|
||||
let uri = raw_event.uri.clone();
|
||||
if let Some(event) = parse_event_from_uri(&uri, event_type, &raw_data) {
|
||||
return Some(ParsedEvent {
|
||||
event,
|
||||
raw_data,
|
||||
uri,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to manual extraction
|
||||
let uri = event_data.get("uri")?.as_str()?;
|
||||
let data = event_data.get("data")?;
|
||||
let uri = event_data.get("uri")?.as_str()?.to_string();
|
||||
let data = event_data.get("data")?.clone();
|
||||
let event_type = event_data
|
||||
.get("eventType")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("Update");
|
||||
|
||||
return parse_event_from_uri(uri, event_type, data);
|
||||
if let Some(event) = parse_event_from_uri(&uri, event_type, &data) {
|
||||
return Some(ParsedEvent {
|
||||
event,
|
||||
raw_data: data,
|
||||
uri,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
debug!("Unknown callback: {}", callback);
|
||||
}
|
||||
@@ -113,6 +135,16 @@ pub fn parse_event_from_uri(
|
||||
return parse_end_of_game_stats(data);
|
||||
}
|
||||
|
||||
// Handle LP change notifications
|
||||
if uri == "/lol-ranked/v1/current-lp-change-notification" {
|
||||
return parse_lp_change_notification(data);
|
||||
}
|
||||
|
||||
// Handle ranked stats updates (with UUID suffix)
|
||||
if uri.starts_with("/lol-ranked/v1/ranked-stats/") {
|
||||
return parse_ranked_stats_event(data);
|
||||
}
|
||||
|
||||
// Handle lobby
|
||||
if uri.starts_with("/lol-lobby") {
|
||||
debug!("Lobby event: {}", uri);
|
||||
@@ -423,6 +455,215 @@ fn parse_end_of_game_stats(data: &serde_json::Value) -> Option<GameEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse LP change notification event.
|
||||
///
|
||||
/// This is the primary source for LP changes after a ranked game.
|
||||
/// The notification contains the LP delta and current rank info.
|
||||
fn parse_lp_change_notification(data: &serde_json::Value) -> Option<GameEvent> {
|
||||
info!("LP change notification received: {:?}", data);
|
||||
|
||||
// Extract queue type
|
||||
let queue_type = data
|
||||
.get("queueType")
|
||||
.and_then(|q| q.as_str())
|
||||
.unwrap_or("UNKNOWN")
|
||||
.to_string();
|
||||
|
||||
// Extract LP change amount
|
||||
let lp_change = data.get("lpChange").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32;
|
||||
|
||||
// Extract LP before and after
|
||||
let lp_before = data.get("lpBefore").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32;
|
||||
|
||||
let lp_after = data.get("lpAfter").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32;
|
||||
|
||||
// Extract tier and division
|
||||
let tier = data
|
||||
.get("tier")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("UNRANKED")
|
||||
.to_string();
|
||||
|
||||
let division = data
|
||||
.get("division")
|
||||
.and_then(|d| d.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Check for promotional series
|
||||
let in_promos = data.get("miniSeries").is_some();
|
||||
|
||||
let promo_progress = if in_promos {
|
||||
data.get("miniSeries")
|
||||
.and_then(|ms| ms.get("progress"))
|
||||
.and_then(|p| p.as_str())
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let promo_wins = data
|
||||
.get("miniSeries")
|
||||
.and_then(|ms| ms.get("wins"))
|
||||
.and_then(|w| w.as_u64())
|
||||
.map(|w| w as u32);
|
||||
|
||||
let promo_losses = data
|
||||
.get("miniSeries")
|
||||
.and_then(|ms| ms.get("losses"))
|
||||
.and_then(|l| l.as_u64())
|
||||
.map(|l| l as u32);
|
||||
|
||||
// Extract total games stats if available
|
||||
let total_wins = data.get("wins").and_then(|w| w.as_u64()).unwrap_or(0) as u32;
|
||||
|
||||
let total_losses = data.get("losses").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
|
||||
|
||||
let total_games = total_wins + total_losses;
|
||||
|
||||
info!(
|
||||
"LP change notification: {} {} LP change: {} ({} -> {})",
|
||||
queue_type, tier, lp_change, lp_before, lp_after
|
||||
);
|
||||
|
||||
let event_json = serde_json::json!({
|
||||
"eventType": "lcu-lp-change",
|
||||
"queueType": queue_type,
|
||||
"lpChange": lp_change,
|
||||
"lpBefore": lp_before,
|
||||
"lpAfter": lp_after,
|
||||
"tier": tier,
|
||||
"division": division,
|
||||
"leaguePoints": lp_after,
|
||||
"inPromos": in_promos,
|
||||
"promoProgress": promo_progress,
|
||||
"promoWins": promo_wins,
|
||||
"promoLosses": promo_losses,
|
||||
"totalGames": total_games,
|
||||
"totalWins": total_wins,
|
||||
"totalLosses": total_losses
|
||||
});
|
||||
|
||||
GameEvent::from_json(&event_json)
|
||||
}
|
||||
|
||||
/// Parse ranked stats event for LP changes.
|
||||
///
|
||||
/// The ranked stats endpoint provides updates when LP changes occur.
|
||||
/// We extract the queue-specific data and emit an LpChange event.
|
||||
fn parse_ranked_stats_event(data: &serde_json::Value) -> Option<GameEvent> {
|
||||
info!("Ranked stats event received: {:?}", data);
|
||||
|
||||
// The ranked stats data contains queue-specific stats
|
||||
// We look for RANKED_SOLO_5x5 and RANKED_FLEX_SR queues
|
||||
let queues = data.get("queues")?.as_array()?;
|
||||
|
||||
for queue_data in queues {
|
||||
let queue_type = queue_data
|
||||
.get("queueType")
|
||||
.and_then(|q| q.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Only process ranked queues
|
||||
if queue_type != "RANKED_SOLO_5x5" && queue_type != "RANKED_FLEX_SR" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract tier and division
|
||||
let tier = queue_data
|
||||
.get("tier")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("UNRANKED")
|
||||
.to_string();
|
||||
|
||||
let division = queue_data
|
||||
.get("division")
|
||||
.and_then(|d| d.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Extract LP
|
||||
let league_points = queue_data
|
||||
.get("leaguePoints")
|
||||
.and_then(|lp| lp.as_i64())
|
||||
.unwrap_or(0) as i32;
|
||||
|
||||
// Extract previous LP if available (for calculating change)
|
||||
let previous_lp = queue_data
|
||||
.get("previousLeaguePoints")
|
||||
.and_then(|lp| lp.as_i64())
|
||||
.unwrap_or(league_points as i64) as i32;
|
||||
|
||||
// Calculate LP change
|
||||
let lp_change = league_points - previous_lp;
|
||||
|
||||
// Only emit event if there was an actual change
|
||||
if lp_change == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for promotional series
|
||||
let in_promos = queue_data.get("miniSeries").is_some();
|
||||
|
||||
let promo_progress = if in_promos {
|
||||
queue_data
|
||||
.get("miniSeries")
|
||||
.and_then(|ms| ms.get("progress"))
|
||||
.and_then(|p| p.as_str())
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let promo_wins = queue_data
|
||||
.get("miniSeries")
|
||||
.and_then(|ms| ms.get("wins"))
|
||||
.and_then(|w| w.as_u64())
|
||||
.map(|w| w as u32);
|
||||
|
||||
let promo_losses = queue_data
|
||||
.get("miniSeries")
|
||||
.and_then(|ms| ms.get("losses"))
|
||||
.and_then(|l| l.as_u64())
|
||||
.map(|l| l as u32);
|
||||
|
||||
// Extract total games stats
|
||||
let total_wins = queue_data.get("wins").and_then(|w| w.as_u64()).unwrap_or(0) as u32;
|
||||
|
||||
let total_losses = queue_data
|
||||
.get("losses")
|
||||
.and_then(|l| l.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
let total_games = total_wins + total_losses;
|
||||
|
||||
info!(
|
||||
"LP change detected: {} {} LP change: {} ({} -> {})",
|
||||
queue_type, tier, lp_change, previous_lp, league_points
|
||||
);
|
||||
|
||||
let event_json = serde_json::json!({
|
||||
"eventType": "lcu-lp-change",
|
||||
"queueType": queue_type,
|
||||
"lpChange": lp_change,
|
||||
"lpBefore": previous_lp,
|
||||
"lpAfter": league_points,
|
||||
"tier": tier,
|
||||
"division": division,
|
||||
"leaguePoints": league_points,
|
||||
"inPromos": in_promos,
|
||||
"promoProgress": promo_progress,
|
||||
"promoWins": promo_wins,
|
||||
"promoLosses": promo_losses,
|
||||
"totalGames": total_games,
|
||||
"totalWins": total_wins,
|
||||
"totalLosses": total_losses
|
||||
});
|
||||
|
||||
return GameEvent::from_json(&event_json);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -223,11 +223,12 @@ impl Daemon {
|
||||
}
|
||||
|
||||
/// Handle a game event.
|
||||
async fn handle_game_event(&self, event: GameEvent) -> Result<()> {
|
||||
async fn handle_game_event(&self, parsed: record_daemon::lqp::ParsedEvent) -> Result<()> {
|
||||
let event = &parsed.event;
|
||||
info!("[EVENT_HANDLER] Game event received: {:?}", event);
|
||||
|
||||
// Handle pre-game data collection
|
||||
match &event {
|
||||
match event {
|
||||
GameEvent::PhaseChange(info) if info.phase == "ChampSelect" => {
|
||||
info!("[EVENT_HANDLER] Champion select started");
|
||||
}
|
||||
@@ -249,10 +250,10 @@ impl Daemon {
|
||||
// Record event to timeline if recording (BEFORE state transition for GameEnd)
|
||||
// This ensures GameEnd events are recorded while still in recording state
|
||||
if self.state_machine.is_recording() {
|
||||
if let Some((video_ts, game_ts)) = self.event_mapper.write().handle_event(&event) {
|
||||
if let Some((video_ts, game_ts)) = self.event_mapper.write().handle_event(event) {
|
||||
// Get the current recording ID
|
||||
if let Some(recording_id) = *self.current_recording_id.read() {
|
||||
// Create a timestamped event
|
||||
// Create a timestamped event with raw data
|
||||
let timestamped_event = TimestampedEvent {
|
||||
video_timestamp: video_ts,
|
||||
game_timestamp: game_ts,
|
||||
@@ -260,6 +261,8 @@ impl Daemon {
|
||||
event_type: event.event_type_name().to_string(),
|
||||
description: event.description(),
|
||||
event: event.clone(),
|
||||
raw_data: Some(parsed.raw_data.clone()),
|
||||
uri: Some(parsed.uri.clone()),
|
||||
};
|
||||
|
||||
// Add the event to the timeline store
|
||||
@@ -284,7 +287,7 @@ impl Daemon {
|
||||
}
|
||||
|
||||
// Process state transitions
|
||||
if let Some(transition) = self.state_machine.process_event(&event) {
|
||||
if let Some(transition) = self.state_machine.process_event(event) {
|
||||
info!("[EVENT_HANDLER] State transition: {:?}", transition);
|
||||
|
||||
// Only process the transition if it's valid
|
||||
|
||||
@@ -89,6 +89,31 @@ impl Timeline {
|
||||
event_type: event_type_name(&event),
|
||||
description: event.description(),
|
||||
event,
|
||||
raw_data: None,
|
||||
uri: None,
|
||||
};
|
||||
|
||||
self.events.push(timestamped);
|
||||
}
|
||||
|
||||
/// Add an event to the timeline with raw data.
|
||||
pub fn add_event_with_raw(
|
||||
&mut self,
|
||||
event: GameEvent,
|
||||
video_timestamp: Duration,
|
||||
game_timestamp: Option<Duration>,
|
||||
raw_data: serde_json::Value,
|
||||
uri: String,
|
||||
) {
|
||||
let timestamped = TimestampedEvent {
|
||||
video_timestamp,
|
||||
game_timestamp,
|
||||
timestamp: Utc::now(),
|
||||
event_type: event_type_name(&event),
|
||||
description: event.description(),
|
||||
event,
|
||||
raw_data: Some(raw_data),
|
||||
uri: Some(uri),
|
||||
};
|
||||
|
||||
self.events.push(timestamped);
|
||||
@@ -176,6 +201,7 @@ fn event_type_name(event: &GameEvent) -> String {
|
||||
GameEvent::StatsUpdate(_) => "stats_update",
|
||||
GameEvent::GameEnd(_) => "game_end",
|
||||
GameEvent::PhaseChange(_) => "phase_change",
|
||||
GameEvent::LpChange(_) => "lp_change",
|
||||
GameEvent::Unknown => "unknown",
|
||||
}
|
||||
.to_string()
|
||||
|
||||
@@ -23,12 +23,18 @@ pub struct TimestampedEvent {
|
||||
pub game_timestamp: Option<Duration>,
|
||||
/// Real-world timestamp.
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Event type name.
|
||||
/// Event type name (derived from URI).
|
||||
pub event_type: String,
|
||||
/// Human-readable description.
|
||||
pub description: String,
|
||||
/// The actual event data.
|
||||
pub event: GameEvent,
|
||||
/// Raw JSON data from the API (for flexibility).
|
||||
#[serde(default)]
|
||||
pub raw_data: Option<serde_json::Value>,
|
||||
/// URI of the endpoint that triggered this event.
|
||||
#[serde(default)]
|
||||
pub uri: Option<String>,
|
||||
}
|
||||
|
||||
/// Metadata for a recording.
|
||||
|
||||
Reference in New Issue
Block a user