//! Timeline module for storing game events and mapping them to video timestamps. mod mapper; mod store; pub use mapper::EventMapper; pub use store::{RecordingMetadata, TimelineStore, TimestampedEvent}; use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::lqp::GameEvent; /// A timeline of events for a recording. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Timeline { /// Recording ID. pub recording_id: Uuid, /// Recording start time. pub start_time: DateTime, /// Recording end time. pub end_time: Option>, /// Total duration in seconds. pub duration_secs: i64, /// Events in the timeline. pub events: Vec, } impl Timeline { /// Get the duration as chrono Duration. pub fn duration(&self) -> Duration { Duration::seconds(self.duration_secs) } } impl Timeline { /// Create a new timeline for a recording. pub fn new(recording_id: Uuid) -> Self { Self { recording_id, start_time: Utc::now(), end_time: None, duration_secs: 0, events: Vec::new(), } } /// Add an event to the timeline. pub fn add_event( &mut self, event: GameEvent, video_timestamp: Duration, game_timestamp: Option, ) { let timestamped = TimestampedEvent { video_timestamp, game_timestamp, timestamp: Utc::now(), event_type: event_type_name(&event), description: event.description(), event, }; self.events.push(timestamped); } /// Finalize the timeline. pub fn finalize(&mut self) { self.end_time = Some(Utc::now()); self.duration_secs = (self.end_time.unwrap() - self.start_time).num_seconds(); } /// Get events within a time range. pub fn events_in_range(&self, start: Duration, end: Duration) -> Vec<&TimestampedEvent> { self.events .iter() .filter(|e| e.video_timestamp >= start && e.video_timestamp <= end) .collect() } /// Get events of a specific type. pub fn events_of_type(&self, event_type: &str) -> Vec<&TimestampedEvent> { self.events .iter() .filter(|e| e.event_type == event_type) .collect() } /// Get the number of events. pub fn event_count(&self) -> usize { self.events.len() } /// Export timeline to JSON. pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self) } /// Export timeline to CSV. pub fn to_csv(&self) -> String { let mut csv = String::from("video_timestamp,game_timestamp,event_type,description,timestamp\n"); for event in &self.events { let video_ts = format_timestamp(event.video_timestamp); let game_ts = event .game_timestamp .map(format_timestamp) .unwrap_or_default(); csv.push_str(&format!( "{},{},{},{},{}\n", video_ts, game_ts, event.event_type, event.description, event.timestamp.to_rfc3339(), )); } csv } } /// Format a duration as HH:MM:SS.mmm fn format_timestamp(duration: Duration) -> String { let total_ms = duration.num_milliseconds(); let hours = total_ms / 3_600_000; let minutes = (total_ms % 3_600_000) / 60_000; let seconds = (total_ms % 60_000) / 1000; let millis = total_ms % 1000; format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis) } /// Get the event type name. fn event_type_name(event: &GameEvent) -> String { match event { GameEvent::MatchFound(_) => "match_found", GameEvent::GameStart(_) => "game_start", GameEvent::Kill(_) => "kill", GameEvent::Death(_) => "death", GameEvent::Objective(_) => "objective", GameEvent::GameEnd(_) => "game_end", GameEvent::PhaseChange(_) => "phase_change", GameEvent::Unknown => "unknown", } .to_string() } #[cfg(test)] mod tests { use super::*; use crate::lqp::KillEvent; #[test] fn test_timeline_creation() { let id = Uuid::new_v4(); let timeline = Timeline::new(id); assert_eq!(timeline.recording_id, id); assert!(timeline.events.is_empty()); } #[test] fn test_add_event() { let id = Uuid::new_v4(); let mut timeline = Timeline::new(id); let event = GameEvent::Kill(KillEvent { killer: "Player1".to_string(), killer_champion: Some("Ahri".to_string()), victim: "Player2".to_string(), victim_champion: Some("Lux".to_string()), solo_kill: true, assists: 0, position: None, game_time: Some(120.0), timestamp: Utc::now(), }); timeline.add_event(event, Duration::seconds(5), Some(Duration::seconds(120))); assert_eq!(timeline.event_count(), 1); } #[test] fn test_format_timestamp() { let duration = Duration::milliseconds(3723456); // 1:02:03.456 let formatted = format_timestamp(duration); assert_eq!(formatted, "01:02:03.456"); } }