use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
pub trait Clock: Send {
fn now(&self) -> Instant;
fn elapsed_since(&self, earlier: Instant) -> Duration {
self.now().duration_since(earlier)
}
}
#[derive(Debug, Clone, Copy)]
pub struct WallClock;
impl Clock for WallClock {
fn now(&self) -> Instant {
Instant::now()
}
}
pub struct TickClock {
anchor: Instant,
elapsed: Duration,
tick_size: Duration,
tick_count: u64,
}
impl TickClock {
#[must_use]
pub fn new(tick_size: Duration) -> Self {
Self {
anchor: Instant::now(),
elapsed: Duration::ZERO,
tick_size,
tick_count: 0,
}
}
#[must_use]
pub fn at_60fps() -> Self {
Self::new(Duration::from_micros(16_667))
}
#[must_use]
pub fn at_30fps() -> Self {
Self::new(Duration::from_micros(33_333))
}
pub fn tick(&mut self) {
self.elapsed += self.tick_size;
self.tick_count += 1;
}
pub fn tick_n(&mut self, n: u64) {
let mut remaining = n;
while remaining > 0 {
let batch = remaining.min(u64::from(u32::MAX));
#[allow(clippy::cast_possible_truncation)]
let batch_u32 = batch as u32;
self.elapsed += self.tick_size * batch_u32;
remaining -= batch;
}
self.tick_count += n;
}
#[must_use]
pub const fn tick_count(&self) -> u64 {
self.tick_count
}
#[must_use]
pub const fn tick_size(&self) -> Duration {
self.tick_size
}
#[must_use]
pub const fn virtual_elapsed(&self) -> Duration {
self.elapsed
}
}
impl Clock for TickClock {
fn now(&self) -> Instant {
self.anchor + self.elapsed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeterministicSeed(pub u64);
impl DeterministicSeed {
#[must_use]
pub const fn new(seed: u64) -> Self {
Self(seed)
}
#[must_use]
pub const fn value(self) -> u64 {
self.0
}
#[must_use]
pub fn derive(self, subsystem: &str) -> Self {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325 ^ self.0;
for byte in subsystem.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
Self(hash)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReplayMode {
Live,
Deterministic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayMetadata {
pub mode: ReplayMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tick_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame_seq: Option<u64>,
}
impl ReplayMetadata {
#[must_use]
pub const fn live() -> Self {
Self {
mode: ReplayMode::Live,
seed: None,
tick_ms: None,
frame_seq: None,
}
}
#[must_use]
pub const fn deterministic(seed: u64, tick_ms: u64, frame_seq: u64) -> Self {
Self {
mode: ReplayMode::Deterministic,
seed: Some(seed),
tick_ms: Some(tick_ms),
frame_seq: Some(frame_seq),
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn wall_clock_monotonic() {
let clock = WallClock;
let a = clock.now();
let b = clock.now();
assert!(b >= a);
}
#[test]
fn tick_clock_starts_at_zero() {
let clock = TickClock::new(Duration::from_millis(10));
assert_eq!(clock.tick_count(), 0);
assert_eq!(clock.virtual_elapsed(), Duration::ZERO);
}
#[test]
fn tick_clock_advances() {
let mut clock = TickClock::new(Duration::from_millis(10));
let start = clock.now();
clock.tick();
assert_eq!(clock.tick_count(), 1);
assert_eq!(clock.virtual_elapsed(), Duration::from_millis(10));
assert_eq!(clock.elapsed_since(start), Duration::from_millis(10));
clock.tick();
assert_eq!(clock.tick_count(), 2);
assert_eq!(clock.virtual_elapsed(), Duration::from_millis(20));
}
#[test]
fn tick_clock_tick_n() {
let mut clock = TickClock::new(Duration::from_millis(5));
clock.tick_n(10);
assert_eq!(clock.tick_count(), 10);
assert_eq!(clock.virtual_elapsed(), Duration::from_millis(50));
}
#[test]
fn tick_clock_60fps() {
let clock = TickClock::at_60fps();
assert_eq!(clock.tick_size(), Duration::from_micros(16_667));
}
#[test]
fn tick_clock_30fps() {
let clock = TickClock::at_30fps();
assert_eq!(clock.tick_size(), Duration::from_micros(33_333));
}
#[test]
fn deterministic_seed_derive() {
let seed = DeterministicSeed::new(42);
let sub_a = seed.derive("rrf");
let sub_b = seed.derive("blend");
assert_ne!(sub_a, sub_b);
assert_ne!(sub_a, seed);
assert_ne!(sub_b, seed);
assert_eq!(seed.derive("rrf"), sub_a);
assert_eq!(seed.derive("blend"), sub_b);
}
#[test]
fn deterministic_seed_value() {
let seed = DeterministicSeed::new(12345);
assert_eq!(seed.value(), 12345);
}
#[test]
fn deterministic_seed_serde_roundtrip() {
let seed = DeterministicSeed::new(999);
let json = serde_json::to_string(&seed).unwrap();
let decoded: DeterministicSeed = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, seed);
}
#[test]
fn replay_mode_serde_roundtrip() {
for mode in [ReplayMode::Live, ReplayMode::Deterministic] {
let json = serde_json::to_string(&mode).unwrap();
let decoded: ReplayMode = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, mode);
}
}
#[test]
fn replay_metadata_live() {
let meta = ReplayMetadata::live();
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("\"live\""));
assert!(!json.contains("seed"));
assert!(!json.contains("tick_ms"));
assert!(!json.contains("frame_seq"));
}
#[test]
fn replay_metadata_deterministic() {
let meta = ReplayMetadata::deterministic(42, 16, 100);
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("\"deterministic\""));
assert!(json.contains("\"seed\":42"));
assert!(json.contains("\"tick_ms\":16"));
assert!(json.contains("\"frame_seq\":100"));
}
#[test]
fn replay_metadata_serde_roundtrip() {
let meta = ReplayMetadata::deterministic(7, 33, 0);
let json = serde_json::to_string(&meta).unwrap();
let decoded: ReplayMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.seed, Some(7));
assert_eq!(decoded.tick_ms, Some(33));
assert_eq!(decoded.frame_seq, Some(0));
}
}