borger/multiplayer_tradeoff.rs
1use crate::diff_ser::DiffSerializer;
2use crate::simulation_controller::GameContext;
3use std::mem;
4
5///Although all game logic code is meant to be interpreted as server authoritative and
6///from the server's top-down perspective controlling everything, you as the game
7///developer can choose how each of your game mechanics feel, on a spectrum between
8///"responsive+potentially incorrect" to "laggy+definitely correct". Game state is always
9///eventually consistent, but this macro lets you decide when and where to execute logic
10///in order to fine-tune game feel. This is possible because the engine executes each
11///simulation tick multiple times across the server and all connected clients, requiring
12///game logic to be **deterministic** depending on the chosen trade-off.
13///
14///multiplayer_tradeoff!() blocks can be nested inside each other, but only in order of
15///increasing latency:
16///
17///`Immediate` (outer) → `WaitForServer` → `WaitForConsensus` (inner)
18///
19///The implementation of the macro itself is very simple:
20///- Immediate is the default because it functionally does nothing other than declare
21///intent to the developer. Both the server and client call the simulation_tick, so they
22///will naturally both run the same code.
23///- WaitForServer removes the block of code from the client build, causing it to only run
24///on the server.
25///- WaitForConsensus also removes the code from the client, and additionally will prevent
26///the code from running during resimulation of the same tick until all client inputs have
27///arrived for that particular tick.
28///- The actual "magic" part, eventual consistency, is enforced as a consequence of the
29///engine's design.
30///
31///| |`Immediate` (default) |`WaitForServer` |`WaitForConsensus` |
32///|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
33///|**Where** |Code runs on both client and server. It is worth mentioning that use of Immediate mode does NOT give clients any cheating ability or authority over what everyone else sees - it's simply a local prediction. |Code runs only on the server |Code runs only on the server |
34///|**Latency/Responsiveness** |Instantaneous. Press a button on the client, see result on screen immediately without waiting for server reply. |Slower if used for processing client inputs/interactions (their individual RTT), otherwise feels smooth. The server executes this regardless of whether it has received inputs from all clients, and clients will render the result whenever they receive it.|Slowest. The server waits to receive all inputs from all clients (up to 1.5 seconds before timing out) before executing. Clients won’t see the result until RTT of the slowest client.|
35///|**Correctness** |May be wrong (“mispredicts”) due to not having access to all relevant state. The server may produce different results than the client. |If it is possible for a client's input to change the results (eg. someone stepping in front of an NPC), there is a risk of mispredicts. |Always correct. No mispredicts. |
36///|**State Visibility** |Can only access client-visible state. Accessing private state causes a compile error; out-of-scope access causes a runtime error. |Has full access to all game state, including private/hidden data. |Has full access to all game state, including private/hidden data. |
37///|**Determinism Requirements**|Must be deterministic across all devices running this block of code. Minor floating-point variations across CPU architectures are *usually* acceptable. Note that comparisons between nearly identical floating-point numbers may produce conflicting boolean results across devices, which could cause a jarring mispredict.|Must be locally deterministic (consistent results on the same device across multiple runs, but may vary between devices) |Determinism is not required because this code will only ever run once. Perfect for making backend calls, eg. a leaderboard update |
38///|**Example Use Cases** |Processing client inputs (movement, shooting, etc.) should happen here in Immediate as much as possible for best game feel. Controls are by far the most latency-sensitive aspect of gameplay. Any physics interactions should also be immediate whenever possible: damage-on-collision mechanics, moving platforms, etc. |Logic that affects entities seen by clients but are unable to be predicted by clients due to having some private state (NPC) |Large/important game state change events that would look horrible if rolled back/mispredicted (game over, level change) |
39///
40///## Parameters
41///- `tradeoff` - The trade-off level: `WaitForServer`, or `WaitForConsensus`
42///- `ctx` - The variable to rebind with the new context. Type can be either `&mut
43///GameContext` or `&mut DiffSerializer`. You can either pass just a variable name (no &
44///or .) or declare a new variable (eg. literally write out `let diff = &mut ctx.diff`)`
45///- `tick` - Expression that evaluates to a `&TickInfo` (required for `WaitForConsensus`
46///only)
47///- `code` - The code block to execute within the specified trade-off context. Can be
48///an expression or a bracketed block
49///
50///## Game logic example:
51///```rust
52///pub fn simulation_tick(ctx: &mut GameContext<Immediate>)
53///{
54/// for character in ctx.state.characters.values_mut()
55/// {
56/// match character.input_owner
57/// {
58/// //client-controlled - we're already in the immediate block, so client side
59/// //prediction is active
60/// InputOwner::Client =>
61/// {
62/// //this hypothetical get_input function only *optionally* returns an input:
63/// //clients can only access their own inputs. when the server also runs
64/// //this block of code, it can access all inputs
65/// if let Some(input) = get_input(character)
66/// {
67/// process_input(character, input, &mut ctx.diff);
68/// }
69/// },
70///
71/// //npc-controlled - in this example, npc input/decision-making state is
72/// //defined as private/server only (state schema shown below). Without WaitForServer,
73/// //the client build would not compile because the private struct fields are removed
74/// InputOwner::NPC =>
75/// {
76/// //safe to unwrap/assert here on get_input. server always has access to all state.
77/// //also take note of this nesting example - the inner (WaitForServer) block can
78/// //still access variables declared in the outer (Immediate) block
79/// multiplayer_tradeoff!(WaitForServer, let diff = &mut ctx.diff,
80/// process_input(character, get_input(character).unwrap(), diff));
81/// },
82/// }
83/// }
84///
85/// multiplayer_tradeoff!(WaitForConsensus, ctx, ctx.tick,
86/// {
87/// if collision_check_finish_line(state)
88/// {
89/// game_over(ctx);
90///
91/// //use WaitForConsensus here to avoid mispredicts/rollbacks,
92/// //at the cost of standing around waiting at the finish line
93/// //(should be 3 seconds absolute worst case). we want to avoid
94/// //someone seeing "you win!" then a second later their screen
95/// //changes to "you lose!" at all costs
96/// }
97/// });
98///}
99///
100/////process_input is called from both an Immediate block and a WaitForServer block
101///pub fn process_input(character: &mut Character, input: &Input, diff: &mut DiffSerializer<impl ImmediateOrWaitForServer>)
102///{
103/// let mut pos = character.get_pos();
104/// pos.x += input.omnidir.x * TickInfo::SIM_DT;
105/// character.set_pos(pos, diff);
106/// // ^ diff serializer records all state mutations, the key to engine's rollback implementation
107///}
108///```
109///
110///State schema example for the above game logic:
111///
112///(Note this has been cut down to illustrate only the shape of
113///the State object and how usage of netVisibility affects
114///the required multiplayer_tradeoff)
115///```typescript
116///import type { State } from "@borger/code_generator/state_schema.ts";
117///
118///export default {
119/// //represents a real person connected to a server
120/// clients: {
121/// netVisibility: "public",
122/// type: "SlotMap",
123/// content: {
124/// input: {
125/// netVisibility: "owner",
126/// type: "struct",
127/// content: {
128/// omnidir: { netVisibility: "owner", type: "Vec2" },
129/// },
130/// },
131/// character_id: { netVisibility: "public", type: "usize32" },
132/// },
133/// },
134/// //represents the internal thought process of a non-player character
135/// npcs: {
136/// netVisibility: "private",
137/// type: "SlotMap",
138/// content: {
139/// character_id: { netVisibility: "private", type: "usize32" },
140/// will_drop_rare_item: { netVisibility: "private", type: "bool" },
141/// //more secret stuff that clients shouldn't know about...
142/// },
143/// },
144/// //represents an entity being rendered, who may be controlled by either a client or npc
145/// characters: {
146/// netVisibility: "public",
147/// type: "SlotMap",
148/// content: {
149/// pos: { netVisibility: "public", type: "Vec3" },
150/// input_owner: { netVisibility: "public", type: "enum", content: ["Client", "NPC"] },
151/// },
152/// },
153///} satisfies State;
154///```
155#[macro_export]
156macro_rules! multiplayer_tradeoff
157{
158 //WaitForServer - adds server feature flag
159 (WaitForServer, $ctx:ident, $code:stmt) =>
160 {
161 #[cfg(feature = "server")]
162 {
163 let $ctx = unsafe { $ctx._to_server_unchecked() };
164 $code
165 }
166 };
167 (WaitForServer, let $rebind:ident = $ctx:expr, $code:stmt) =>
168 {
169 #[cfg(feature = "server")]
170 {
171 let $rebind = $ctx; //evaluate safely first
172 let $rebind = unsafe { $rebind._to_server_unchecked() };
173 $code
174 }
175 };
176 (WaitForServer, $ctx:ident, { $($code:tt)* }) =>
177 {
178 #[cfg(feature = "server")]
179 {
180 let $ctx = unsafe { $ctx._to_server_unchecked() };
181 $($code)*
182 }
183 };
184 (WaitForServer, let $rebind:ident = $ctx:expr, { $($code:tt)* }) =>
185 {
186 #[cfg(feature = "server")]
187 {
188 let $rebind = $ctx; //evaluate safely first
189 let $rebind = unsafe { $rebind._to_server_unchecked() };
190 $($code)*
191 }
192 };
193
194 //WaitForConsensus - adds server feature flag+wraps in has_consensus() if statement
195 (WaitForConsensus, $ctx:ident, $tick:expr, $code:stmt) =>
196 {
197 #[cfg(feature = "server")]
198 {
199 let _tick: &borger::tick::TickInfo = $tick;
200 if unsafe { _tick._has_consensus_tradeoff() }
201 {
202 let $ctx = unsafe { $ctx._to_consensus_unchecked() };
203 $code
204 }
205 }
206 };
207 (WaitForConsensus, let $rebind:ident = $ctx:expr, $tick:expr, $code:stmt) =>
208 {
209 #[cfg(feature = "server")]
210 {
211 let _tick: &borger::tick::TickInfo = $tick;
212 if unsafe { _tick._has_consensus_tradeoff() }
213 {
214 let $rebind = $ctx; //evaluate safely first
215 let $rebind = unsafe { $rebind._to_consensus_unchecked() };
216 $code
217 }
218 }
219 };
220 (WaitForConsensus, $ctx:ident, $tick:expr, { $($code:tt)* }) =>
221 {
222 #[cfg(feature = "server")]
223 {
224 let _tick: &borger::tick::TickInfo = $tick;
225 if unsafe { _tick._has_consensus_tradeoff() }
226 {
227 let $ctx = unsafe { $ctx._to_consensus_unchecked() };
228 $($code)*
229 }
230 }
231 };
232 (WaitForConsensus, let $rebind:ident = $ctx:expr, $tick:expr, { $($code:tt)* }) =>
233 {
234 #[cfg(feature = "server")]
235 {
236 let _tick: &borger::tick::TickInfo = $tick;
237 if unsafe { _tick._has_consensus_tradeoff() }
238 {
239 let $rebind = $ctx; //evaluate safely first
240 let $rebind = unsafe { $rebind._to_consensus_unchecked() };
241 $($code)*
242 }
243 }
244 };
245}
246
247pub struct Immediate; //to immediate/server/consensus
248pub struct WaitForServer; //to server/consensus
249pub struct WaitForConsensus; //to consensus
250
251#[derive(Default)]
252pub(crate) struct Impl; //default contextless state used internally
253
254pub trait AnyTradeOff {} //to consensus
255impl AnyTradeOff for Immediate {}
256impl AnyTradeOff for WaitForServer {}
257impl AnyTradeOff for WaitForConsensus {}
258impl AnyTradeOff for Impl {}
259
260//convenience: useful if an entity may be controlled either
261//by client or server (npc)
262pub trait ImmediateOrWaitForServer: AnyTradeOff {} //to server/consensus
263impl ImmediateOrWaitForServer for Immediate {}
264impl ImmediateOrWaitForServer for WaitForServer {}
265
266//transmutation between different AnyTradeOff is safe memory-wise
267//because the struct layout is not influenced by it (used by
268//phantom data only). however it is not safe multiplayer-wise and
269//so the game itself should not be calling these directly
270
271macro_rules! multiplayer_tradeoff_transitions {
272 ($type:ident) => {
273 impl $type<Immediate> {
274 #[doc(hidden)]
275 pub unsafe fn _to_immediate_unchecked(&mut self) -> &mut Self {
276 self
277 }
278 }
279
280 #[cfg(feature = "server")]
281 impl<TradeOff: ImmediateOrWaitForServer> $type<TradeOff> {
282 #[doc(hidden)]
283 pub unsafe fn _to_server_unchecked(&mut self) -> &mut $type<WaitForServer> {
284 unsafe { mem::transmute(self) }
285 }
286 }
287
288 #[cfg(feature = "server")]
289 impl<TradeOff: AnyTradeOff> $type<TradeOff> {
290 #[doc(hidden)]
291 pub unsafe fn _to_consensus_unchecked(&mut self) -> &mut $type<WaitForConsensus> {
292 unsafe { mem::transmute(self) }
293 }
294 }
295 };
296}
297
298multiplayer_tradeoff_transitions!(GameContext);
299multiplayer_tradeoff_transitions!(DiffSerializer);
300
301//internal only
302
303impl GameContext<Impl> {
304 pub(crate) fn to_immediate(&mut self) -> &mut GameContext<Immediate> {
305 unsafe { mem::transmute(self) }
306 }
307}
308
309#[cfg(feature = "server")]
310impl DiffSerializer<Impl> {
311 pub(crate) fn to_consensus(&mut self) -> &mut DiffSerializer<WaitForConsensus> {
312 unsafe { mem::transmute(self) }
313 }
314}
315
316impl<TradeOff: AnyTradeOff> DiffSerializer<TradeOff> {
317 pub(crate) fn to_impl(&mut self) -> &mut DiffSerializer<Impl> {
318 unsafe { mem::transmute(self) }
319 }
320}