Skip to main content

borger/
tick.rs

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