Skip to main content

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}