Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Edited 4/18/2026

Trade-Offs

The trade-offs annotation system is the heart of what makes Borger unique. All simulation logic grapples with the tension between latency, state visibility, and misprediction risk. Trade-offs allow you to explicitly choose how this tension affects your code (and thus the overall game feel), on a spectrum between “responsive+potentially incorrect” to “laggy+definitely correct”. They are applied as wrappers around functions and other blocks of code, so a quick glance at the start of a block tells you everything you need to know about the multiplayer characteristics of any game mechanic.

Comparison Table

Rule of ThumbWhereLatency/ResponsivenessCorrectnessState VisibilityDeterminism RequirementsExample Use Cases
Immediate (default)Immediate response without waiting for the serverCode runs on both server and client. Use of Immediate mode does NOT give clients any cheating ability or authority over what everyone else sees - it’s simply a local prediction.Instantaneous. Press a button on the client, see result on screen immediately without waiting for server reply.Highest risk of mispredction due to not having access to all relevant state. The server may produce different results than the client.Can only access client-visible state. Accessing private state causes a compile error; out-of-scope access causes a runtime error.Must be deterministic between the server and any clients executing it. They should independently arrive at the same result.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.
WaitForServerHide sensitive, private data from clientsCode runs only on the serverSlower 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.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 mispredction.Has full access to all game state, including private/hidden data.Must only be locally deterministic (consistent results on the same device across multiple runs, but may vary between devices)Logic that affects entities seen by clients but are unable to be predicted by clients due to having some private state (NPC)
WaitForConsensusGuaranteed correctness at all costsCode runs only on the serverSlowest. 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.Always correct. No mispredction.Has full access to all game state, including private/hidden data.Determinism is not required because this code will only ever run once.Backend calls (updating the leaderboard in the database), large/important game state change events that would look horrible if rolled back/mispredicted (game over, level change)

Identification

Trade-offs are implemented as a no-op generic parameter on the DiffSerializer struct (and GameContext by extension), which forces any function that mutates output state to specify under which trade-off context it can execute. This is what’s meant by “annotations”: you see trade-offs at the top of every function signature and every multiplayer_tradeoff!() call, lending an understanding to the multiplayer implications when it runs.

In this example, note the WaitForConsensus in the signature. It immediately (pun intended) tells you that this function is non-deterministic, never rolls back, and only happens on the server (from the information in the above table)

pub fn on_client_connect
(
	state: &mut SimulationState,
	client_id: usize32,
	tick_id: TickID,
	diff: &mut DiffSerializer<WaitForConsensus>,
)

And the simulation loop of course is tagged with the most flexible trade-off, Immediate.

fn simulation_loop(ctx: &mut GameContext<Immediate>)

Sometimes it can make sense for a function to be called under multiple contexts:

//a character may be client-controlled (Immediate) or an NPC (WaitForServer)
fn update_character(ctx: &mut GameContext<impl ImmediateOrWaitForServer>)

multiplayer_tradeoff!()

This macro is what allows toggling between trade-offs. Notably, trade-offs can only be nested in order of increasing latency:

fn simulation_loop(ctx: &mut GameContext<Immediate>)
{
	multiplayer_tradeoff!(WaitForServer,
	{
		multiplayer_tradeoff!(WaitForConsensus,
		{
			//game logic here

			//(you can skip directly from Immediate to WaitForConsensus
			//if needed. this is demonstration only)
		});
	});
}

#[server]

Functions who are meant to only be called in WaitForServer and WaitForConsensus contexts must be tagged with this attribute macro, in order to strip the code from the client build. Ideally the compiler could flag this automatically through static analysis, but for now it must be manually inserted in this early release of Borger. Forgetting to include it could result in a failed client build, a client-sided runtime crash if unwrap()ing on out-of-scope state, or it may just work normally if the function doesn’t attempt to read private state.