1use std::time::{Duration, Instant};
9
10use parking_lot::RwLock;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ServerTimeSampleFacts {
14 pub server_time_secs: Option<u64>,
15 pub server_time_usec: Option<u64>,
16 pub round_trip_micros: u64,
17 pub local_receive_unix_micros: Option<u64>,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ServerTimeProjection {
22 pub server_at_receive_unix_micros: i64,
23 pub offset_micros: i64,
24}
25
26#[derive(Debug, Clone, Copy)]
32pub struct ServerTimeAnchorUpdate {
33 pub projection: ServerTimeProjection,
34 pub captured_at: Instant,
35}
36
37#[must_use]
43pub fn project_server_time_sample_like_cpp(
44 facts: ServerTimeSampleFacts,
45) -> Option<ServerTimeProjection> {
46 let server_secs = i128::from(facts.server_time_secs?);
47 let server_usec = i128::from(facts.server_time_usec?);
48 let local_receive = i128::from(facts.local_receive_unix_micros?);
49 let half_rtt = i128::from(facts.round_trip_micros / 2);
50 let server_at_receive = server_secs
51 .checked_mul(1_000_000)?
52 .checked_add(server_usec)?
53 .checked_add(half_rtt)?;
54 let offset = server_at_receive.checked_sub(local_receive)?;
55
56 Some(ServerTimeProjection {
57 server_at_receive_unix_micros: i64::try_from(server_at_receive).ok()?,
58 offset_micros: i64::try_from(offset).ok()?,
59 })
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct DelayCalibrationFacts {
64 pub server_time_offset_micros: i64,
65 pub icmp_round_trip_micros: u64,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct DelayCalibrationProjection {
70 pub s2c_time_diff_us: i64,
71 pub net_delay_us: i64,
72}
73
74#[must_use]
76pub fn project_delay_calibration_like_cpp(
77 facts: DelayCalibrationFacts,
78) -> Option<DelayCalibrationProjection> {
79 Some(DelayCalibrationProjection {
80 s2c_time_diff_us: facts.server_time_offset_micros,
81 net_delay_us: i64::try_from(facts.icmp_round_trip_micros / 2).ok()?,
82 })
83}
84
85#[derive(Debug, Clone, Copy)]
86enum AnchorState {
87 OffsetOnly {
88 offset_micros: i64,
89 },
90 Precise {
91 server_at_capture_unix_micros: i64,
92 captured_at: Instant,
93 },
94}
95
96#[derive(Debug)]
102pub struct ServerTimeAnchorStore {
103 state: RwLock<AnchorState>,
104}
105
106impl ServerTimeAnchorStore {
107 #[must_use]
108 pub fn new() -> Self {
109 Self {
110 state: RwLock::new(AnchorState::OffsetOnly { offset_micros: 0 }),
111 }
112 }
113
114 pub fn store_offset_micros(&self, offset_micros: i64) {
115 *self.state.write() = AnchorState::OffsetOnly { offset_micros };
116 }
117
118 pub fn store_projection(&self, projection: ServerTimeProjection, captured_at: Instant) {
119 *self.state.write() = AnchorState::Precise {
120 server_at_capture_unix_micros: projection.server_at_receive_unix_micros,
121 captured_at,
122 };
123 }
124
125 pub fn store_update(&self, update: ServerTimeAnchorUpdate) {
126 self.store_projection(update.projection, update.captured_at);
127 }
128
129 #[must_use]
130 pub fn now_unix_micros_at(&self, local_now_unix_micros: i64, now: Instant) -> i64 {
131 match *self.state.read() {
132 AnchorState::OffsetOnly { offset_micros } => {
133 local_now_unix_micros.saturating_add(offset_micros)
134 }
135 AnchorState::Precise {
136 server_at_capture_unix_micros,
137 captured_at,
138 } => server_at_capture_unix_micros.saturating_add(duration_micros_i64(
139 now.saturating_duration_since(captured_at),
140 )),
141 }
142 }
143
144 #[must_use]
145 pub fn offset_micros_at(&self, local_now_unix_micros: i64, now: Instant) -> i64 {
146 self.now_unix_micros_at(local_now_unix_micros, now)
147 .saturating_sub(local_now_unix_micros)
148 }
149}
150
151impl Default for ServerTimeAnchorStore {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157fn duration_micros_i64(duration: Duration) -> i64 {
158 duration.as_micros().min(i64::MAX as u128) as i64
159}