borger/tick.rs
1use num_enum::{IntoPrimitive, TryFromPrimitive};
2use std::fmt::{Debug, Error, Formatter};
3use std::time::Duration;
4use web_time::Instant;
5
6#[cfg(feature = "server")]
7use {crate::networked_types::primitive::usize32, crate::thread_comms::SimToClientChannel};
8
9//fun fact: tick id as u32 at a rate of 30hz gives a maximum of
10//~4.5 years of gameplay before overflow. not good enough i say.
11//the u64 loses some precision when casting to f64 later on but
12//should still give a lot more than 4.5 years.
13pub type TickID = u64;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
16#[repr(u8)]
17pub(crate) enum TickType {
18 ///If server events are triggered, a "server events" tick is
19 ///actually only the first half of a complete tick.
20 ///Non-deterministic in nature
21 ServerEvents,
22
23 ///Consensus tick is final. All inputs have been received
24 ///from all clients (or timeout occurred while waiting).
25 ///It will never be simulated again
26 Consensus,
27
28 ///Predicted tick has not received inputs from all clients yet.
29 ///It is guaranteed to simulate again when either the late input
30 ///arrives or the laggy client disconnects
31 Predicted,
32}
33
34pub struct TickInfo {
35 //this value is not network synchronized in any way and so
36 //isn't deterministic. it only measures how long the local
37 //simulation has been running
38 first: Instant,
39
40 //all of these id's are incremental
41
42 //oldest tick that can still be rolled back and (as in, rewind
43 //state to right before this tick happened). controls the amount
44 //of state history that must be stored for rollback. will only
45 //ever increase
46 pub(crate) id_consensus: TickID,
47
48 //same as id_consensus but includes pending unreconciled buffers
49 //held in pending_received_buffers
50 #[cfg(feature = "client")]
51 pub(crate) id_consensus_received: TickID,
52
53 //highest tick id received from the server so far. will only ever
54 //increase. used to block reconciliation until receiving an equal
55 //or higher tick id from the server. fixes severe visual glitching
56 //caused by receiving wildly fluctuating tick id's during
57 //consensus timeouts
58 #[cfg(feature = "client")]
59 pub(crate) id_cur_received: TickID,
60
61 //another way of looking at this number is "how many ticks have
62 //completed start to finish" or "tick.id_unfinished". may increase
63 //or decrease due to local rollbacks, causing old ticks to be
64 //resimulated
65 pub(crate) id_cur: TickID,
66
67 //by the end of the current scheduled tick, id_cur should be here
68 pub(crate) id_target: TickID,
69
70 //id_consensus <= id_consensus_received <= id_cur_received <= id_cur <= id_target
71 //the wider the gap between id's (particularly consensus+target),
72 //the worse the performance due to more rollbacks and
73 //retransmitting old ticks. this also means that the laggiest
74 //client will hurt performance for everyone, including the server.
75 //we don't like that guy
76
77 //server authoritative data that can't be reconciled yet due to
78 //none of the pending_received_buffers having a tick id higher than
79 //id_received
80 #[cfg(feature = "client")]
81 pub(crate) pending_received_buffers: Vec<Vec<u8>>,
82}
83
84impl TickInfo {
85 //simulation delta time/tick rate, in seconds/tick.
86 //can be higher or lower than vsync refresh rate.
87 //too low feels kinda floaty, too high hurts performance
88 pub const SIM_DT: f32 = 1.0 / 30.0;
89
90 pub(crate) fn new(id_start: TickID, fast_forward_ticks: TickID) -> Self {
91 TickInfo {
92 first: Instant::now()
93 - Duration::from_secs_f64((id_start + fast_forward_ticks) as f64 * Self::SIM_DT as f64),
94
95 id_consensus: id_start,
96
97 #[cfg(feature = "client")]
98 id_consensus_received: id_start,
99
100 #[cfg(feature = "client")]
101 id_cur_received: id_start,
102
103 id_cur: id_start,
104 id_target: id_start + fast_forward_ticks,
105
106 #[cfg(feature = "client")]
107 pending_received_buffers: Vec::new(),
108 }
109 }
110
111 pub fn id(&self) -> TickID {
112 self.id_cur
113 }
114
115 //if true, this tick is being simulated for the final time.
116 //non-deterministic code is allowed, and large transition events
117 //(objective complete, game end, etc.) are encouraged to happen now
118 #[cfg(feature = "server")]
119 pub(crate) fn has_consensus(&self) -> bool {
120 //seems counterintuitive that consensus can be higher
121 //than cur, but this is because id_consensus is
122 //incremented at the start of a tick while id_cur of
123 //a tick while id_cur is incremented at the end
124 self.id_consensus > self.id_cur
125 }
126
127 //used by multiplayer tradeoff macro and marked unsafe for similar
128 //reasoning: multiplayer safety, not memory safety
129 #[cfg(feature = "server")]
130 #[doc(hidden)]
131 pub unsafe fn _has_consensus_tradeoff(&self) -> bool {
132 self.has_consensus()
133 }
134
135 //analogous to InputAge::Fresh - true if this is the first time
136 //the current tick ID is being simulated, false on resimulation
137 #[cfg(feature = "client")]
138 pub(crate) fn is_fresh(&self) -> bool {
139 self.id_cur == self.id_target - 1
140 }
141
142 #[cfg(any(feature = "server", feature = "singlethreaded"))]
143 pub(crate) const fn get_ticks(dur: Duration) -> TickID {
144 f32::round(dur.as_secs_f32() / Self::SIM_DT) as TickID
145 }
146
147 pub(crate) fn get_duration(offset: TickID) -> Duration {
148 Duration::from_secs_f64(Self::SIM_DT as f64 * offset as f64)
149 }
150
151 pub(crate) fn get_instant_at(&self, id: TickID) -> Instant {
152 self.first + Self::get_duration(id)
153 }
154
155 #[cfg(any(feature = "server", feature = "singlethreaded"))]
156 pub(crate) fn get_tick_at(&self, instant: Instant) -> TickID {
157 let duration = instant - self.first;
158 Self::get_ticks(duration)
159 }
160
161 pub(crate) fn get_now(&self) -> Instant {
162 self.get_instant_at(self.id_cur)
163 }
164
165 //recalibration is needed when server and client have
166 //differing tick.id_target. this is unrelated to ping.
167 //it causes client to exist at the wrong time, either
168 //too far into the past or future
169 #[cfg(feature = "client")]
170 pub(crate) fn recalibrate(&mut self, offset_from_server: i16) {
171 if offset_from_server >= 0 {
172 //early relative to server
173 let offset_from_server = offset_from_server as TickID;
174 self.first += Self::get_duration(offset_from_server);
175 //do not modify id_target. rather, the adjustment of first
176 //causes the simulation to pause for that many ticks
177 } else {
178 //late relative to server
179 let offset_from_server = (-offset_from_server) as TickID;
180 self.first -= Self::get_duration(offset_from_server);
181 self.id_target += offset_from_server;
182 }
183 }
184}
185
186impl Debug for TickInfo {
187 fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
188 #[derive(Debug)]
189 #[allow(dead_code)]
190 struct TickInfo {
191 id_consensus: TickID,
192 id_cur: TickID,
193 }
194
195 Debug::fmt(
196 &TickInfo {
197 id_consensus: self.id_consensus,
198 id_cur: self.id_cur,
199 },
200 f,
201 )
202 }
203}
204
205//triggering an unrollbackable game logic event (aka
206//using multiplayer_tradeoff!(WaitForConsensus) is
207//normally delayed because it's caused by something
208//happening on a specific tick, so that tick needs
209//to finalize/reach consensus before triggering the
210//unrollbackable event. unrollbackable network events
211//on the other hand do not care about happening on a
212//specific tick, so in order to trigger them asap,
213//server rolls back as far as it can to the most recent
214//consensus point in history
215#[cfg(feature = "server")]
216pub(crate) enum UnrollbackableNetEvent {
217 ServerStart,
218 ClientConnect(SimToClientChannel),
219 ClientDisconnect(usize32),
220}