1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![cfg_attr(feature = "fail-on-warnings", deny(clippy::all))]
3
4mod config;
5
6use chrono::{DateTime, Utc};
7pub use config::*;
8use std::{
9 sync::{
10 atomic::{AtomicU64, Ordering},
11 Arc, OnceLock,
12 },
13 time::Duration,
14};
15
16static INSTANCE: OnceLock<Time> = OnceLock::new();
17
18#[derive(Clone)]
19struct Time {
20 config: TimeConfig,
21 elapsed_ms: Arc<AtomicU64>,
22 ticker_task: Arc<OnceLock<()>>,
23}
24
25impl Time {
26 fn new(config: TimeConfig) -> Self {
27 let time = Self {
28 config,
29 elapsed_ms: Arc::new(AtomicU64::new(0)),
30 ticker_task: Arc::new(OnceLock::new()),
31 };
32 if !time.config.realtime {
33 time.spawn_ticker();
34 }
35 time
36 }
37
38 fn spawn_ticker(&self) {
39 let elapsed_ms = self.elapsed_ms.clone();
40 let sim_config = self
41 .config
42 .sim_time
43 .as_ref()
44 .expect("sim_time required when realtime is false");
45 let tick_interval_ms = sim_config.tick_interval_ms;
46 let tick_duration = sim_config.tick_duration_secs;
47 self.ticker_task.get_or_init(|| {
48 tokio::spawn(async move {
49 let mut interval =
50 tokio::time::interval(tokio::time::Duration::from_millis(tick_interval_ms));
51 loop {
52 interval.tick().await;
53 elapsed_ms.fetch_add(tick_duration.as_millis() as u64, Ordering::Relaxed);
54 }
55 });
56 });
57 }
58
59 fn now(&self) -> DateTime<Utc> {
60 if self.config.realtime {
61 Utc::now()
62 } else {
63 let sim_config = self
64 .config
65 .sim_time
66 .as_ref()
67 .expect("sim_time required when realtime is false");
68 let elapsed_ms = self.elapsed_ms.load(Ordering::Relaxed);
69
70 let simulated_time =
71 sim_config.start_at + chrono::Duration::milliseconds(elapsed_ms as i64);
72
73 if sim_config.transform_to_realtime && simulated_time >= Utc::now() {
74 Utc::now()
75 } else {
76 simulated_time
77 }
78 }
79 }
80
81 async fn sleep(&self, duration: Duration) {
82 if self.config.realtime {
83 tokio::time::sleep(duration).await
84 } else {
85 let sim_config = self
86 .config
87 .sim_time
88 .as_ref()
89 .expect("sim_time required when realtime is false");
90
91 let current_time = self.now();
92 let real_now = Utc::now();
93
94 if sim_config.transform_to_realtime && current_time >= real_now {
95 tokio::time::sleep(duration).await;
96 return;
97 }
98
99 let sim_ms_per_real_ms = sim_config.tick_duration_secs.as_millis() as f64
100 / sim_config.tick_interval_ms as f64;
101
102 let real_ms = (duration.as_millis() as f64 / sim_ms_per_real_ms).ceil() as u64;
103
104 tokio::time::sleep(Duration::from_millis(real_ms)).await
105 }
106 }
107
108 pub async fn wait_until_realtime(&self) {
109 if self.config.realtime {
110 return;
111 }
112
113 let current = self.now();
114 let real_now = Utc::now();
115
116 if current >= real_now {
117 return;
118 }
119
120 let wait_duration =
121 std::time::Duration::from_millis((real_now - current).num_milliseconds() as u64);
122
123 self.sleep(wait_duration).await;
124 }
125}
126
127pub async fn wait_until_realtime() {
128 INSTANCE
129 .get_or_init(|| Time::new(TimeConfig::default()))
130 .wait_until_realtime()
131 .await
132}
133
134pub fn init(config: TimeConfig) {
135 INSTANCE.get_or_init(|| Time::new(config));
136}
137
138pub fn now() -> DateTime<Utc> {
139 INSTANCE
140 .get_or_init(|| Time::new(TimeConfig::default()))
141 .now()
142}
143
144pub async fn sleep(duration: Duration) {
145 INSTANCE
146 .get_or_init(|| Time::new(TimeConfig::default()))
147 .sleep(duration)
148 .await
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use chrono::Duration as ChronoDuration;
155 use std::time::Duration as StdDuration;
156
157 #[tokio::test]
158 async fn test_simulated_time() {
159 let config = TimeConfig {
161 realtime: false,
162 sim_time: Some(SimTimeConfig {
163 start_at: Utc::now(),
164 tick_interval_ms: 10,
165 tick_duration_secs: StdDuration::from_secs(10 * 24 * 60 * 60), transform_to_realtime: false,
167 }),
168 };
169
170 init(config);
171 let start = now();
172 tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
173 let end = now();
174 let elapsed = end - start;
175
176 assert!(
177 elapsed >= ChronoDuration::days(19) && elapsed <= ChronoDuration::days(21),
178 "Expected ~20 days to pass, but got {} days",
179 elapsed.num_days()
180 );
181 }
182
183 #[test]
184 fn test_default_realtime() {
185 let t1 = now();
186 let t2 = Utc::now();
187 assert!(t2 - t1 < ChronoDuration::seconds(1));
188 }
189}