borger/multiplayer_tradeoff.rs
1use crate::simulation_controller::GameContext;
2use borger_plugin_sdk::multiplayer_tradeoff::{Immediate, Impl};
3use borger_plugin_sdk::multiplayer_tradeoff_transitions;
4use std::mem;
5
6///Although all game logic code is meant to be interpreted as server authoritative and
7///from the server's top-down perspective controlling everything, you as the game
8///developer can choose how each of your game mechanics feel, on a spectrum between
9///"responsive+potentially incorrect" to "laggy+definitely correct". Game state is always
10///eventually consistent, but this macro lets you decide when and where to execute logic
11///in order to fine-tune game feel. This is possible because the engine executes each
12///simulation tick multiple times across the server and all connected clients, requiring
13///game logic to be **deterministic** depending on the chosen trade-off.
14///
15///multiplayer_tradeoff!() blocks can be nested inside each other, but only in order of
16///increasing latency:
17///
18///`Immediate` (outer) → `WaitForServer` → `WaitForConsensus` (inner)
19///
20///The implementation of the macro itself is very simple:
21///- Immediate is the default because it functionally does nothing other than declare
22///intent to the developer. Both the server and client call the simulation_tick, so they
23///will naturally both run the same code.
24///- WaitForServer removes the block of code from the client build, causing it to only run
25///on the server.
26///- WaitForConsensus also removes the code from the client, and additionally will prevent
27///the code from running during resimulation of the same tick until all client inputs have
28///arrived for that particular tick.
29///- The actual "magic" part, eventual consistency, is enforced as a consequence of the
30///engine's design.
31///
32///| |`Immediate` (default) |`WaitForServer` |`WaitForConsensus` |
33///|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
34///|**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 |
35///|**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.|
36///|**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. |
37///|**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. |
38///|**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 |
39///|**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) |
40///
41///## Parameters
42///- `tradeoff` - The trade-off level: `WaitForServer`, or `WaitForConsensus`
43///- `ctx` - The variable to rebind with the new context. Type can be either `&mut
44///GameContext` or `&mut DiffSerializer`. You can either pass just a variable name (no &
45///or .) or declare a new variable (eg. literally write out `let diff = &mut ctx.diff`)`
46///- `tick` - Expression that evaluates to a `&TickInfo` (required for `WaitForConsensus`
47///only)
48///- `code` - The code block to execute within the specified trade-off context. Can be
49///an expression or a bracketed block
50///
51///## Game logic example:
52///```rust
53///pub fn simulation_tick(ctx: &mut GameContext<Immediate>)
54///{
55/// for character in ctx.state.characters.values_mut()
56/// {
57/// match character.input_owner
58/// {
59/// //client-controlled - we're already in the immediate block, so client side
60/// //prediction is active
61/// InputOwner::Client =>
62/// {
63/// //this hypothetical get_input function only *optionally* returns an input:
64/// //clients can only access their own inputs. when the server also runs
65/// //this block of code, it can access all inputs
66/// if let Some(input) = get_input(character)
67/// {
68/// process_input(character, input, &mut ctx.diff);
69/// }
70/// },
71///
72/// //npc-controlled - in this example, npc input/decision-making state is
73/// //defined as private/server only (state schema shown below). Without WaitForServer,
74/// //the client build would not compile because the private struct fields are removed
75/// InputOwner::NPC =>
76/// {
77/// //safe to unwrap/assert here on get_input. server always has access to all state.
78/// //also take note of this nesting example - the inner (WaitForServer) block can
79/// //still access variables declared in the outer (Immediate) block
80/// multiplayer_tradeoff!(WaitForServer, let diff = &mut ctx.diff,
81/// process_input(character, get_input(character).unwrap(), diff));
82/// },
83/// }
84/// }
85///
86/// multiplayer_tradeoff!(WaitForConsensus, ctx, ctx.tick,
87/// {
88/// if collision_check_finish_line(state)
89/// {
90/// game_over(ctx);
91///
92/// //use WaitForConsensus here to avoid mispredicts/rollbacks,
93/// //at the cost of standing around waiting at the finish line
94/// //(should be 3 seconds absolute worst case). we want to avoid
95/// //someone seeing "you win!" then a second later their screen
96/// //changes to "you lose!" at all costs
97/// }
98/// });
99///}
100///
101/////process_input is called from both an Immediate block and a WaitForServer block
102///pub fn process_input(character: &mut Character, input: &Input, diff: &mut DiffSerializer<impl ImmediateOrWaitForServer>)
103///{
104/// let mut pos = character.get_pos();
105/// pos.x += input.omnidir.x * SIM_DT;
106/// character.set_pos(pos, diff);
107/// // ^ diff serializer records all state mutations, the key to engine's rollback implementation
108///}
109///```
110///
111///State schema example for the above game logic:
112///
113///(Note this has been cut down to illustrate only the shape of
114///the State object and how usage of netVisibility affects
115///the required multiplayer_tradeoff)
116///```typescript
117///import type { State } from "@borger/code_generator/state_schema.ts";
118///
119///export default {
120/// //represents a real person connected to a server
121/// clients: {
122/// netVisibility: "public",
123/// type: "SlotMap",
124/// content: {
125/// input: {
126/// netVisibility: "owner",
127/// type: "struct",
128/// content: {
129/// omnidir: { netVisibility: "owner", type: "Vec2" },
130/// },
131/// },
132/// character_id: { netVisibility: "public", type: "usize32" },
133/// },
134/// },
135/// //represents the internal thought process of a non-player character
136/// npcs: {
137/// netVisibility: "private",
138/// type: "SlotMap",
139/// content: {
140/// character_id: { netVisibility: "private", type: "usize32" },
141/// will_drop_rare_item: { netVisibility: "private", type: "bool" },
142/// //more secret stuff that clients shouldn't know about...
143/// },
144/// },
145/// //represents an entity being rendered, who may be controlled by either a client or npc
146/// characters: {
147/// netVisibility: "public",
148/// type: "SlotMap",
149/// content: {
150/// pos: { netVisibility: "public", type: "Vec3" },
151/// input_owner: { netVisibility: "public", type: "enum", content: ["Client", "NPC"] },
152/// },
153/// },
154///} satisfies State;
155///```
156#[macro_export]
157#[doc(hidden)]
158macro_rules! multiplayer_tradeoff
159{
160 //WaitForServer - adds server feature flag
161 (WaitForServer, $ctx:ident, $code:stmt) =>
162 {
163 #[cfg(feature = "server_internal_only_dont_use")]
164 {
165 let $ctx = unsafe { $ctx._to_server_unchecked() };
166 $code
167 }
168 };
169 (WaitForServer, let $rebind:ident = $ctx:expr, $code:stmt) =>
170 {
171 #[cfg(feature = "server_internal_only_dont_use")]
172 {
173 let $rebind = $ctx; //evaluate safely first
174 let $rebind = unsafe { $rebind._to_server_unchecked() };
175 $code
176 }
177 };
178 (WaitForServer, $ctx:ident, { $($code:tt)* }) =>
179 {
180 #[cfg(feature = "server_internal_only_dont_use")]
181 {
182 let $ctx = unsafe { $ctx._to_server_unchecked() };
183 $($code)*
184 }
185 };
186 (WaitForServer, let $rebind:ident = $ctx:expr, { $($code:tt)* }) =>
187 {
188 #[cfg(feature = "server_internal_only_dont_use")]
189 {
190 let $rebind = $ctx; //evaluate safely first
191 let $rebind = unsafe { $rebind._to_server_unchecked() };
192 $($code)*
193 }
194 };
195
196 //WaitForConsensus - adds server feature flag+wraps in has_consensus() if statement
197 (WaitForConsensus, $ctx:ident, $tick:expr, $code:stmt) =>
198 {
199 #[cfg(feature = "server_internal_only_dont_use")]
200 {
201 let _tick: &borger::tick::TickInfo = $tick;
202 if unsafe { _tick._has_consensus_tradeoff() }
203 {
204 let $ctx = unsafe { $ctx._to_consensus_unchecked() };
205 $code
206 }
207 }
208 };
209 (WaitForConsensus, let $rebind:ident = $ctx:expr, $tick:expr, $code:stmt) =>
210 {
211 #[cfg(feature = "server_internal_only_dont_use")]
212 {
213 let _tick: &borger::tick::TickInfo = $tick;
214 if unsafe { _tick._has_consensus_tradeoff() }
215 {
216 let $rebind = $ctx; //evaluate safely first
217 let $rebind = unsafe { $rebind._to_consensus_unchecked() };
218 $code
219 }
220 }
221 };
222 (WaitForConsensus, $ctx:ident, $tick:expr, { $($code:tt)* }) =>
223 {
224 #[cfg(feature = "server_internal_only_dont_use")]
225 {
226 let _tick: &borger::tick::TickInfo = $tick;
227 if unsafe { _tick._has_consensus_tradeoff() }
228 {
229 let $ctx = unsafe { $ctx._to_consensus_unchecked() };
230 $($code)*
231 }
232 }
233 };
234 (WaitForConsensus, let $rebind:ident = $ctx:expr, $tick:expr, { $($code:tt)* }) =>
235 {
236 #[cfg(feature = "server_internal_only_dont_use")]
237 {
238 let _tick: &borger::tick::TickInfo = $tick;
239 if unsafe { _tick._has_consensus_tradeoff() }
240 {
241 let $rebind = $ctx; //evaluate safely first
242 let $rebind = unsafe { $rebind._to_consensus_unchecked() };
243 $($code)*
244 }
245 }
246 };
247}
248
249multiplayer_tradeoff_transitions!(GameContext);
250
251//internal only
252impl GameContext<Impl> {
253 pub(crate) fn to_immediate(&mut self) -> &mut GameContext<Immediate> {
254 unsafe { mem::transmute(self) }
255 }
256}