Skip to main content

borger/
simulation_controller.rs

1use crate::SimulationInitOptions;
2use crate::simulation::{Input, InputAge, State};
3use crate::snapshot_serdes::NewClientHeader;
4use crate::thread_comms::*;
5use crate::tick::TickInfo;
6use borger_plugin_sdk::diff_ser::DiffSerializer;
7use borger_plugin_sdk::multiplayer_tradeoff::{AnyTradeOff, Impl};
8use borger_plugin_sdk::primitive::usize32;
9use borger_plugin_sdk::traits::{ConstructCustomStruct, UntrackedState};
10use borger_plugin_sdk::{ClientKind, TickID};
11use log::debug;
12use std::collections::VecDeque;
13use std::rc::Rc;
14use std::sync::mpsc::channel as sync_unbounded_channel;
15use std::time::Duration;
16use web_time::Instant;
17
18#[cfg(feature = "server")]
19use {
20	crate::tick::UnrollbackableNetEvent,
21	std::collections::HashMap,
22	std::sync::mpsc::{Receiver as SyncReceiver, Sender as SyncSender},
23	tokio::sync::mpsc::UnboundedSender as AsyncSender,
24};
25
26#[cfg(feature = "client")]
27use {
28	crate::presentation_collect::PresentationCollectContext, crate::snapshot_serdes, atomicbox::AtomicOptionBox,
29	std::sync::Arc,
30};
31
32#[cfg(not(feature = "singlethreaded"))]
33use wasm_thread as thread;
34
35#[cfg(feature = "client")]
36mod client;
37mod seek;
38#[cfg(feature = "server")]
39mod server;
40
41//recommend increasing sim_dt with this feature on
42#[cfg(feature = "server")]
43const TRACE_TICK_ADVANCEMENT: bool = false;
44#[cfg(feature = "client")]
45const TRACE_TICK_ADVANCEMENT: bool = false;
46
47//communications between the simulation thread
48//and the owning parent thread
49pub struct SimControllerExternals {
50	pub internals: SimThreading,
51
52	#[cfg(feature = "server")]
53	pub new_connection_sender: SyncSender<AsyncSender<SimToClientCommand>>,
54	#[cfg(feature = "client")]
55	pub comms: PresentationToSimChannel,
56}
57
58//data that needs to be moved across threads
59//during initialization of simulation thread.
60//all fields must be Send
61#[cfg(not(feature = "singlethreaded"))]
62struct SimMoveAcrossThreads {
63	o: SimulationInitOptions,
64
65	#[cfg(feature = "client")]
66	new_client_snapshot: Vec<u8>,
67
68	#[cfg(feature = "server")]
69	new_connection_receiver: SyncReceiver<AsyncSender<SimToClientCommand>>,
70	#[cfg(feature = "client")]
71	comms: SimToPresentationChannel,
72}
73
74pub enum SimThreading {
75	#[cfg(not(feature = "singlethreaded"))]
76	Multithreaded(thread::JoinHandle<()>),
77
78	#[cfg(any(feature = "singlethreaded", feature = "session_replay"))]
79	#[allow(private_interfaces)]
80	Singlethreaded(SimControllerInternals),
81}
82
83//on client, owned by simulation thread
84struct SimControllerInternals {
85	ctx: GameContext<Impl>,
86	o: SimulationInitOptions,
87
88	//inputs associated with ticks that haven't reached consensus yet
89	//are stored here. when more info is received ticks will be
90	//resimulated using this input history. server is aware of all
91	//clients' inputs; client only stores its own
92	#[cfg(feature = "server")]
93	input_history: HashMap<usize32, InternalInputHistory>,
94	#[cfg(feature = "client")]
95	input_history: InternalInputHistory,
96
97	#[cfg(feature = "server")]
98	new_connection_receiver: SyncReceiver<AsyncSender<SimToClientCommand>>,
99	#[cfg(feature = "server")]
100	comms: HashMap<usize32, SimToClientChannel>,
101	#[cfg(feature = "client")]
102	comms: SimToPresentationChannel,
103
104	#[cfg(feature = "server")]
105	server_events: VecDeque<UnrollbackableNetEvent>,
106
107	#[cfg(feature = "client")]
108	local_client_id: usize32,
109	#[cfg(feature = "client")]
110	calibration_samples: VecDeque<i16>,
111	#[cfg(feature = "client")]
112	initial_calibration: bool,
113}
114
115pub struct GameContext<TradeOff: AnyTradeOff> {
116	pub state: State,
117	pub tick: TickInfo,
118	pub diff: DiffSerializer<TradeOff>,
119}
120
121#[derive(Default, Debug)]
122struct InternalInputHistory {
123	//element at index 0 corresponds to the most recently
124	//completed consensus tick, and each subsequent element
125	//is a progressively more recent tick. between each
126	//scheduled tick (during the sleep period) it is
127	//guaranteed that every client has at least one element
128	//in order to compare the new input to the previous.
129	//some logic asserts at least 2 elements when consensus
130	//is taking place, in order to guarantee pop_front will
131	//leave at least 1 left by the end of the scheduled tick
132	entries: VecDeque<InternalInputEntry>,
133
134	//how many ticks timed out and reached consensus while
135	//waiting for this client's inputs. used to prevent
136	//newly received inputs from pushing to the inputs
137	//buffer until there are no more missing
138	#[cfg(feature = "server")]
139	timed_out_ticks: TickID,
140
141	//the diff received from the client is always applied to
142	//this, regardless of whether there are .timed_out inputs
143	#[cfg(feature = "server")]
144	latest_received: Input,
145
146	//if true, server will not wait for this client before
147	//coming to consensus
148	#[cfg(feature = "server")]
149	is_timed_out: bool,
150}
151
152#[derive(Default, Debug, Clone)]
153struct InternalInputEntry {
154	input: Input,
155
156	#[cfg(feature = "server")]
157	age: InputAge,
158
159	//ping is measured in 2 different ways depending on
160	//whether this is the server or client:
161	//1. server: time in ticks between the tick that this entry
162	//is associated with and the server's scheduled tick.id_cur
163	//at the time of receiving. shipped to the corresponding
164	//client as part of its state diff packet header (see the
165	//signaling scheme). normally negative but can be positive
166	//if client is ahead
167	//2. client: rtt in ticks between the time the client sends
168	//this input and the time it receives the corresponding
169	//authoritative state diff from server. can only be
170	//positive
171	//together these 2 numbers are used to calibrate the
172	//client's tick.id_target to try to match the server's
173	//tick.id_target in real world time, in case they have
174	//become desynced with each other. the time between
175	//the client receiving the initial state snapshot and
176	//actually starting the simulation always causes a
177	//noticeable desync that this can fix
178	#[cfg(feature = "server")]
179	ping: Option<i16>, //offset
180	#[cfg(feature = "client")]
181	ping: bool, //whether waiting to be acked for the first time
182}
183
184#[cfg(feature = "client")]
185struct ClientComms {
186	sim_comms: SimToPresentationChannel,
187	presentation_comms: PresentationToSimChannel,
188}
189
190#[cfg(feature = "client")]
191fn make_client_comms() -> ClientComms {
192	let sim_out = Arc::new(AtomicOptionBox::none());
193	let (to_presentation, from_sim) = sync_unbounded_channel();
194	let (to_sim, from_presentation) = sync_unbounded_channel();
195
196	let sim_comms = SimToPresentationChannel {
197		to_presentation,
198		from_presentation,
199		sim_out: sim_out.clone(),
200	};
201	let presentation_comms = PresentationToSimChannel {
202		to_sim,
203		from_sim,
204		sim_out,
205	};
206
207	ClientComms {
208		sim_comms,
209		presentation_comms,
210	}
211}
212
213struct MakeContext {
214	ctx: GameContext<Impl>,
215
216	#[cfg(feature = "client")]
217	new_client_header: NewClientHeader,
218}
219
220fn make_context(
221	o: &SimulationInitOptions,
222	#[cfg(feature = "client")] new_client_snapshot: Vec<u8>,
223) -> MakeContext {
224	let mut state = State::construct(&Rc::default(), ClientKind::NA);
225	o.init_static_level_geom.map(|init_level| {
226		init_level(&mut state);
227		state.reset_untracked(); //in case init_static_level_geom did anything nefarious
228	});
229
230	#[cfg(feature = "client")]
231	let new_client_header = snapshot_serdes::des_new_client(&mut state, new_client_snapshot).unwrap();
232
233	#[cfg(feature = "server")]
234	let tick_info = TickInfo::new(o.sim_dt, 0, 0);
235	#[cfg(feature = "client")]
236	let tick_info = TickInfo::new(
237		o.sim_dt,
238		new_client_header.tick_id_snapshot,
239		new_client_header.fast_forward_ticks,
240	);
241
242	MakeContext {
243		ctx: GameContext {
244			state,
245			tick: tick_info,
246			diff: DiffSerializer::default(),
247		},
248
249		#[cfg(feature = "client")]
250		new_client_header,
251	}
252}
253
254#[cfg(not(feature = "singlethreaded"))]
255pub fn init_multithreaded(
256	o: SimulationInitOptions,
257	#[cfg(feature = "client")] new_client_snapshot: Vec<u8>,
258) -> SimControllerExternals {
259	#[cfg(feature = "server")]
260	let (new_connection_sender, new_connection_receiver) = sync_unbounded_channel();
261
262	#[cfg(feature = "client")]
263	let ClientComms {
264		sim_comms,
265		presentation_comms,
266	} = make_client_comms();
267
268	let move_me = SimMoveAcrossThreads {
269		o,
270
271		#[cfg(feature = "client")]
272		new_client_snapshot,
273
274		#[cfg(feature = "server")]
275		new_connection_receiver,
276		#[cfg(feature = "client")]
277		comms: sim_comms,
278	};
279
280	let thread = thread::spawn(move || loop_multithreaded(move_me));
281
282	SimControllerExternals {
283		internals: SimThreading::Multithreaded(thread),
284
285		#[cfg(feature = "server")]
286		new_connection_sender,
287		#[cfg(feature = "client")]
288		comms: presentation_comms,
289	}
290}
291
292#[cfg(not(feature = "singlethreaded"))]
293fn loop_multithreaded(moved_data: SimMoveAcrossThreads) {
294	let MakeContext {
295		ctx,
296		#[cfg(feature = "client")]
297		new_client_header,
298	} = make_context(
299		&moved_data.o,
300		#[cfg(feature = "client")]
301		moved_data.new_client_snapshot,
302	);
303
304	let mut sim = SimControllerInternals {
305		ctx,
306		o: moved_data.o,
307
308		#[cfg(feature = "server")]
309		input_history: HashMap::new(),
310		#[cfg(feature = "client")]
311		input_history: InternalInputHistory::default(),
312
313		#[cfg(feature = "server")]
314		new_connection_receiver: moved_data.new_connection_receiver,
315		#[cfg(feature = "server")]
316		comms: HashMap::new(),
317		#[cfg(feature = "client")]
318		comms: moved_data.comms,
319
320		#[cfg(feature = "server")]
321		server_events: VecDeque::from([UnrollbackableNetEvent::ServerStart]),
322
323		#[cfg(feature = "client")]
324		local_client_id: new_client_header.client_id,
325		#[cfg(feature = "client")]
326		calibration_samples: VecDeque::new(),
327		#[cfg(feature = "client")]
328		initial_calibration: true,
329	};
330
331	#[cfg(feature = "client")]
332	sim.initial_fast_forward(new_client_header.fast_forward_ticks);
333
334	loop {
335		if sim.scheduled_tick(
336			#[cfg(feature = "session_replay")]
337			false,
338		) == None
339		{
340			break;
341		}
342
343		let next_tick_time = sim.ctx.tick.get_now();
344		let now = Instant::now();
345
346		if next_tick_time > now {
347			//unfortunately this is blocking, so using repl console on this thread
348			//is probably a no go. not that you could do much anyway since it's
349			//written in rust. also seems to sleep too long+cause clock drift if
350			//tick rate is fast?
351			thread::sleep(next_tick_time - now);
352		} else if TRACE_TICK_ADVANCEMENT && now > next_tick_time {
353			//simulation tick is running behind. possible death spiral.
354			//intentionally not handling this because game is unplayable
355			//and player will just quit
356			debug!("simulation tick hiccuped");
357		}
358	}
359}
360
361#[cfg(any(feature = "singlethreaded", feature = "session_replay"))]
362pub fn init_singlethreaded(o: SimulationInitOptions, new_client_snapshot: Vec<u8>) -> SimControllerExternals {
363	let ClientComms {
364		sim_comms,
365		presentation_comms,
366	} = make_client_comms();
367
368	let MakeContext {
369		ctx,
370		new_client_header,
371	} = make_context(&o, new_client_snapshot);
372
373	let mut internals = SimControllerInternals {
374		ctx,
375		o,
376		input_history: InternalInputHistory::default(),
377		comms: sim_comms,
378		local_client_id: new_client_header.client_id,
379		calibration_samples: VecDeque::new(),
380		initial_calibration: true,
381	};
382
383	internals.initial_fast_forward(new_client_header.fast_forward_ticks);
384
385	SimControllerExternals {
386		internals: SimThreading::Singlethreaded(internals),
387		comms: presentation_comms,
388	}
389}
390
391#[cfg(feature = "singlethreaded")]
392impl SimControllerExternals {
393	pub fn loop_singlethreaded(&mut self) {
394		let SimThreading::Singlethreaded(sim) = &mut self.internals;
395
396		let tick_id_accumulator = sim.ctx.tick.get_tick_at(Instant::now());
397		while sim.ctx.tick.id_cur < tick_id_accumulator {
398			if sim.scheduled_tick(
399				#[cfg(feature = "session_replay")]
400				false,
401			) == None
402			{
403				break;
404			}
405		}
406	}
407}
408
409#[cfg(feature = "session_replay")]
410pub fn replay_session(o: SimulationInitOptions, actions: Vec<SessionReplayAction>) -> Result<(), ()> {
411	let mut actions = actions.into_iter();
412	let Some(SessionReplayAction::Init(new_client_snapshot)) = actions.next() else {
413		return Err(());
414	};
415
416	let mut externals = init_singlethreaded(o, new_client_snapshot);
417	#[cfg_attr(feature = "singlethreaded", allow(irrefutable_let_patterns))]
418	let SimThreading::Singlethreaded(sim) = &mut externals.internals else {
419		unreachable!()
420	};
421
422	//ok to unwrap scheduled_tick() in here. no risk of disconnection
423	for action in actions {
424		match action {
425			SessionReplayAction::ScheduledTick => sim.scheduled_tick(true).unwrap(),
426			SessionReplayAction::ReceiveComm(msg) => externals.comms.to_sim.send(msg).map_err(|_| ())?,
427			SessionReplayAction::Init(_) => return Err(()),
428		};
429
430		//play the part of presentation thread to avoid mem leak
431		while let Ok(_) = externals.comms.from_sim.try_recv() {}
432	}
433
434	//one more time. in the event of a crash, the final
435	//ReplayAction::ScheduledTick is not recorded
436	sim.scheduled_tick(true).unwrap();
437
438	Ok(())
439}
440
441impl SimControllerInternals {
442	#[must_use]
443	//on a client, this returns none upon disconnection
444	fn scheduled_tick(&mut self, #[cfg(feature = "session_replay")] is_replay: bool) -> Option<()> {
445		self.ctx.tick.id_target += 1;
446		if TRACE_TICK_ADVANCEMENT {
447			debug!("begin scheduled tick @{:?}", self.ctx.tick);
448		}
449
450		/*
451		server to client tick signaling protocol:
452		- when server receives a client's input, as long as it hasn't timed out yet (INPUT_TOO_LATE) it rolls back to the tick associated with it and resimulates
453		- send separate state diff packets for simulation-driven vs. server events
454		- when server executes server events, the associated diffs are considered to happen between id_consensus and the predicted tick after it
455		- first value written to every state diff packet is the type of tick that this buffer contains state diffs for:
456		  TickType::ServerEvents -> client does not increment either of the tick id's. this diff is applied to the end of the most recent consensus tick to avoid rollback
457		  TickType::Consensus -> client increments both tick id's. pop the oldest element from input_history
458		  TickType::Predicted -> client increments id_cur only
459		- second value written depends on tick type:
460		  TickType::ServerEvents -> nothing. no client inputs are associated with server events
461		  TickType::Consensus -> whether the receiving client's inputs are acked (true) or a timeout occurred (false)
462		  TickType::Predicted -> the associated tick id. clients who receive predicted ticks are guaranteed to be acked in this packet
463		- the first time a client's inputs are acked, a third value is written:
464		  it is the number of ticks between server's tick.id_cur at reception time and the acked input's associated tick id
465		  TickType::ServerEvents -> n/a, server events don't have associated inputs
466		  TickType::Consensus -> only write if acked for the first time (implies previous value was true)
467		  TickType::Predicted -> only write if acked for the first time
468		- client will roll back to the correct id upon arrival. for ServerEvents and Consensus, this means rolling back as far as possible (until the rollback buffer is empty)
469		- after all state diff packets are processed, client then locally simulates/predicts up to id_target
470		*/
471
472		//remember tick.id_cur is the number of completed ticks. the
473		//target/goal of this iteration of the loop is to simulate 1
474		//more tick than has currently finished simulating
475		self.scheduled_tick_impl()?;
476
477		if TRACE_TICK_ADVANCEMENT {
478			debug!("end scheduled tick");
479		}
480
481		#[cfg(feature = "session_replay")]
482		if !is_replay {
483			self.comms
484				.to_presentation
485				.send(SimToPresentationCommand::SessionReplayAction(
486					SessionReplayAction::ScheduledTick,
487				))
488				.unwrap();
489		}
490
491		Some(())
492	}
493}
494
495impl InternalInputHistory {
496	//new client will attempt to rapidly fast forward
497	//from id_consensus to id_cur, so it won't have time
498	//to populate real input states. generate a bunch of
499	//bogus ones locally without transmitting to avoid
500	//wasting bandwidth. it is expected to start sending
501	//inputs at id_cur. the +1 is so that there is always
502	//at least 1 last known input, in the event that a
503	//client hasn't sent anything at all since the last
504	//consensus tick
505	fn generate_bogus_inputs(&mut self, amount: TickID) {
506		self.entries
507			.extend((0..amount + 1).map(|_| InternalInputEntry::default()));
508	}
509}