195 lines
5.3 KiB
Rust
195 lines
5.3 KiB
Rust
//! 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<Utc>,
|
|
/// Recording end time.
|
|
pub end_time: Option<DateTime<Utc>>,
|
|
/// Total duration in seconds.
|
|
pub duration_secs: i64,
|
|
/// Events in the timeline.
|
|
pub events: Vec<TimestampedEvent>,
|
|
}
|
|
|
|
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<Duration>,
|
|
) {
|
|
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<String, serde_json::Error> {
|
|
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");
|
|
}
|
|
}
|