# Easy Multiplayer — Semantic Rollback Networking Architecture

## Overview

Easy Multiplayer is a multiplayer networking system designed around a central goal:

> Developers should be able to write multiplayer games almost exactly like local multiplayer games.

The system transparently provides:

- Rollback netcode
- Deterministic simulation
- Prediction
- Sparse networking
- Peer-to-peer OR server-client networking
- Large-scale passive participation
- Semantic rollback elimination

However, the redesign evolved far beyond “rollback with optimizations”.

The architecture is now fundamentally based around:

> semantic simulation relevance.

Meaning:

- only semantically relevant inputs matter
- only semantically relevant corrections cause rollback
- only semantically relevant knowledge must synchronize
- silence is meaningful

This document describes the redesigned architecture in its current best-understood form.

---

# The Core Problem With Traditional Rollback

Traditional rollback netcode assumes:

```text
Every player sends inputs every tick
Every input mismatch may affect simulation
Therefore every mismatch can trigger rollback
```

This works well for:

- small player counts
- tightly interactive games
- highly synchronized gameplay

But it scales poorly.

Even if:

- players are idle
- players are spectators
- players are far away
- queried game logic could not possibly change

traditional rollback still:

- transmits inputs
- predicts inputs
- compares inputs
- potentially rolls back

The redesign fundamentally rejects this model.

---

# Core Architectural Principles

The redesign is based around several key principles.

---

## 1. Silence Is Meaningful

No packet does NOT mean:

```text
peer disconnected
```

It means:

```text
nothing changed
```

This drastically reduces unnecessary traffic.

---

## 2. Inputs Only Matter If Queried

If game logic never queried an input:

```text
that input cannot affect simulation
```

Therefore:

```text
corrections to that input cannot require rollback
```

---

## 3. Query Results Matter, Not Raw Inputs

Rollback is not triggered by:

```text
predictedInput !== actualInput
```

Rollback is triggered only if:

```text
the semantic result of a previously executed query changes
```

This is one of the most important innovations in the system.

---

## 4. Inputs Represent Intent, Not Raw Hardware State

The network should generally carry:

```text
jump
```

NOT:

```text
A button pressed
```

Inputs are interpreted locally according to locally visible game state before entering the rollback simulation.

This prevents severe rollback UX problems.

---

## 5. Networking And Simulation Are Separate Layers

The rollback system should not care:

- how packets arrived
- whether networking is peer-to-peer
- whether relay servers exist
- whether players are geographically close

The transport layer should not care:

- what inputs mean
- what causes rollback
- game rules
- deterministic simulation

This separation is essential.

---

# High-Level Architecture

The system is best understood as three distinct layers.

---

# Layer 1 — Transport Layer

Responsible for:

- Connections
- Routing
- NAT traversal
- Reliability
- Packet resend strategy
- Attendance
- Peer discovery
- Clock synchronization
- Relay support
- Connection health
- Delivery ordering

The transport layer ONLY moves data.

It does NOT understand:

- gameplay
- rollback
- game state
- simulation correctness
- input semantics

This makes the transport layer fully replaceable.

Possible transports:

- decentralized mesh
- server-client
- relay-assisted P2P
- hybrid systems

All should work with the same rollback layer.

---

# Layer 2 — Rollback Simulation Layer

Responsible for:

- Deterministic simulation
- Prediction
- Query logging
- Rollback
- Sparse input history
- State snapshots
- Hash checkpoints
- Desync detection
- Re-simulation

This layer understands:

- ticks
- inputs
- queries
- simulation state

But it should NOT know:

- transport topology
- packet routes
- relay structure
- NAT traversal
- sockets

---

# Layer 3 — Game Layer

Responsible for:

- Game rules
- Rendering
- Audio
- Local input interpretation
- Defining game semantics
- Defining query conditions

The game should feel very close to writing a local multiplayer game.

---

# Semantic Query-Based Rollback

This is the central innovation of the system.

---

# Traditional Rollback

Traditional rollback typically works like:

```js
const jump = input.jump;
```

If predicted input differs from actual input:

```text
rollback immediately
```

Even if:

- player was mid-air
- jump could not matter
- gameplay result would remain identical

This causes enormous unnecessary rollback.

---

# Easy Multiplayer Query Model

Instead:

```js
if(player.grounded)
{
    if(query(playerId, input => input.jump))
    {
        player.velocityY = 10;
    }
}
```

The rollback layer records:

```js
{
    tick: 120,
    previousResult: false,
    predicateFunction,
    capturedContext
}
```

---

# Critical Insight

If the query never executes:

```text
the input is irrelevant to that tick
```

Therefore:

```text
corrections to that input cannot require rollback
```

Example:

```js
if(player.grounded)
```

If grounded is false:

- jump was never queried
- jump cannot affect simulation
- jump corrections cannot invalidate tick

This dramatically reduces rollback frequency.

---

# Rollback Decision Logic

Traditional rollback:

```text
predicted input != actual input
=> rollback
```

Easy Multiplayer:

```text
predicted input != actual input
=> re-run affected queries
=> rollback ONLY if query result changes
```

Example:

```text
Predicted stickX = 0.67
Actual stickX = 0.69
Predicate = x > 0.5
Result remains true
=> no rollback
```

Rollback is now based on:

```text
semantic outcome changes
```

NOT:

```text
raw input equality
```

This is one of the key conceptual shifts in the entire system.

---

# Predicates Are Local Rollback Invalidation Functions

Predicates are NOT:

- serialized
- networked
- protocol-level objects
- globally identified

They are purely local rollback invalidation logic.

Example:

```js
query(playerId, input => input.jump > 0.5)
```

The system stores:

```js
{
    predicateFunction,
    previousResult,
    capturedContext
}
```

Then later:

```js
newResult = predicate(correctedInput)

if(newResult !== previousResult)
{
    rollback()
}
```

No predicate registration is necessary.

This significantly simplifies the architecture.

---

# Predicate Context Capture

Predicates may capture game state.

Example:

```js
const threshold = player.position.x;

query(playerId, input => input.x > threshold)
```

However:

the captured values must remain frozen.

Re-evaluation must NOT dynamically fetch current game state.

Otherwise rollback invalidation becomes nondeterministic.

So internally the system stores:

```js
{
    capturedThreshold: 5.2
}
```

rather than reading:

```js
player.position.x
```

again later.

---

# Inputs Represent Local Intent

This is another major conceptual shift.

Traditional rollback often treats:

```text
physical controller state
```

as the fundamental network truth.

Easy Multiplayer instead treats:

```text
locally interpreted player intent
```

as the network truth.

---

# Why This Matters

Consider:

```text
A button means Jump while in gameplay
A button means Confirm while dialog is open
```

Player presses A while locally seeing gameplay.

Locally:

```text
A => Jump
```

Later rollback reveals:

```text
dialog was actually open
```

Traditional rollback could reinterpret:

```text
A => Confirm
```

causing completely unintended behavior.

Easy Multiplayer prevents this.

The local visible state at input time determines meaning.

The network transmits:

```text
jump
```

NOT:

```text
A pressed
```

This preserves player intent.

---

# Context-Aware Input Construction

This process is NOT merely “input filtering”.

It is:

> local intent construction

Example:

```js
getLocalInputs(localGameState)
{
    if(localGameState.state === "inGame")
    {
        return {
            jump: buttonA,
            moveX: joystickX
        };
    }

    if(localGameState.state === "dialogOpen")
    {
        return {
            confirm: buttonA
        };
    }

    return null;
}
```

This means:

- controls are context-sensitive
- input meaning is stable
- rollback cannot reinterpret player intent incorrectly

---

# Passive Participation

An important consequence of sparse inputs.

If a player is:

```js
if(localGameState.state !== "inGame")
{
    return null;
}
```

then:

- no gameplay inputs are generated
- no gameplay inputs are transmitted
- no rollback pressure is created
- the player can still remain connected

This is extremely important for scalability.

The protocol does NOT need explicit:

- spectator mode
- inactive mode
- dormant mode

Participation emerges naturally from whether meaningful inputs exist.

---

# Sparse Input History

Traditional rollback often stores:

```text
tick1 = left
tick2 = left
tick3 = left
tick4 = left
```

Easy Multiplayer instead stores only changes:

```js
[
    { tick: 1, input: "left" }
]
```

Meaning:

```text
continue previous input until changed
```

This dramatically reduces bandwidth and memory usage.

---

# Silence-As-Default Networking

No input packet means:

```text
input state unchanged
```

NOT:

```text
disconnect
```

This distinction is critical.

Connection liveness is handled separately by the transport layer.

---

# Disconnects As Simulation Events

Disconnects themselves may affect gameplay.

Therefore:

disconnects are queryable simulation events.

Example:

```js
queryDisconnected(playerId)
```

This means disconnects participate in:

- rollback
- determinism
- synchronization

---

# Important Distinction

Transport layer detects:

```text
peer unreachable
```

Simulation layer decides:

```text
disconnect becomes canonical at tick X
```

This is a very important separation.

---

# The Hard Problem

All peers must eventually agree on:

```text
which tick the disconnect occurred
```

while still minimizing communication.

This remains one of the more difficult open areas.

---

# Hash Checkpoints

Traditional rollback systems often assume:

```text
all inputs known up to tick X
```

This assumption no longer scales well under sparse semantic synchronization.

Easy Multiplayer instead synchronizes:

- state hashes
- relevant input knowledge

---

# Rolling Hash Windows

Peers periodically send:

```js
{
    oldestTick,
    interval,

    stateHashes: [
        hash0,
        hash100,
        hash200,
        hash300
    ],

    usedInputs: [...]
}
```

Where:

- all hashes within grace window are sent
- oldestTick corresponds to stateHashes[0]
- usedInputs contains all queried relevant inputs since oldestTick

This enables:

- delayed convergence
- partition healing
- decentralized synchronization
- uncertainty-aware desync detection

## Finalized vs Non-Finalized Frame Hashes

Hashes are computed only at CHECKPOINTS (the fixed `interval` grid), never every
frame. This has a deliberate consequence for what counts as "the finalized
frame" used by severe-desync recovery (the completeness score below):

- The recovery-relevant **finalized frame** is the latest CHECKPOINT that has
  crossed the grace horizon — NOT every frame past grace. A frame can be past
  the grace window (technically immutable) yet never be hashed or broadcast as
  a finalized frame because it is not on the checkpoint grid. Only checkpoints
  that finalize advance the "most recent finalized frame" each peer reports.
  This keeps the finalized-hash stream sparse (one hash per interval, not per
  tick) while remaining sufficient to score completeness.

- **Finalized** checkpoint hashes (checkpoint ≤ grace horizon) are ALWAYS
  broadcast — they are what catastrophic-desync detection and the completeness
  score consume.

- **Non-finalized** checkpoint hashes (checkpoints still inside the grace
  window) are broadcast only when an OPT-IN flag is enabled. They can only ever
  reveal a *non-finalized* divergence — i.e. a rollback-able disagreement or
  game-step non-determinism — which a state transfer cannot remedy (see *Desync
  Detection*), so their practical value is unproven. They are gated behind a
  flag rather than sent by default until that value is demonstrated.

---

# Connected-Network Delta Sync

Easy Multiplayer does NOT assume a complete graph. Peers form a CONNECTED
network: every pair is reachable, but not necessarily directly, and messages
may be relayed, delayed, duplicated, or lost.

To synchronize knowledge over such a network with minimal traffic, every
message carries an identity and an acknowledgement:

```js
{ id, ackId, /* payload */ }
```

- `id`    — monotonic per-sender message id
- `ackId` — the highest id this sender has received FROM the recipient

A sender transmits only the DELTA the recipient has not yet acknowledged.
Silence still means "unchanged"; an ack collapses everything up to `ackId`
into "already known", so steady state costs nothing beyond genuinely new
changes.

## Global Knowledge-Diff Store

Each node keeps ONE global watermark, not a per-peer history:

```text
{ id -> knowledge-diff }
```

A `knowledge-diff` is what changed at that message id (new/changed inputs, an
advanced finalized frame, new frame-hash updates). To bring any peer up to
date, replay every diff with `id > peerAckId`. This bounds memory to the
unacknowledged tail rather than full history, and keeps per-peer bookkeeping
to a single integer (their `ackId`).

---

# Desync Detection

Traditional rollback often assumes:

```text
different state = desync
```

Easy Multiplayer rejects this.

Different states may simply mean:

```text
peers possess different relevant knowledge
```

A desync is only REAL if:

```text
there exists no unresolved relevant input uncertainty
between the last agreed checkpoint
and the divergent checkpoint
```

Or more concretely:

- both peers agree on prior checkpoint
- next checkpoint differs
- neither peer possesses mutually exclusive unresolved relevant inputs

Only then is the mismatch considered:

```text
true deterministic divergence
```

This is one of the most important conceptual improvements in the redesign.

## Three Outcomes

Compare at the LOWEST finalized frame the two peers share. The earliest
differing checkpoint falls into exactly one class:

1. **Knowledge gap** — the checkpoints differ but one peer holds relevant
   inputs the other has not yet seen (unresolved uncertainty in the interval).
   NOT a desync; resolve by delta-syncing the missing inputs (above).
   Convergence is automatic once knowledge equalizes.

2. **Non-determinism** — a NON-finalized checkpoint differs while both peers
   used the SAME relevant inputs. The simulation itself is non-deterministic
   between those frames. State transfer cannot fix this: re-running identical
   inputs re-diverges on the same frame. Only finalization (which freezes one
   history) resolves it. **PARKED:** regular non-finalized desync *healing* may
   be dropped entirely for this reason — detection still has diagnostic value,
   but a transfer is not a remedy. (Open — see `KNOWN_ISSUES.md`.)

3. **Catastrophic desync** — a FINALIZED checkpoint differs. History below the
   grace window can no longer be reconciled by accepting inputs, so the network
   must converge by transferring one peer's full finalized state. Resolved by
   the completeness score (see *Severe Desync Recovery*).

---

# Acceptance Window And Grace Window

To enable decentralized convergence:

---

## Acceptance Window

Peers directly accept late inputs up to approximately:

```text
~200ms into the past
```

---

## Grace Window

Peers additionally accept relayed already-accepted inputs for somewhat longer.

Example:

```text
~300ms into the past
```

This enables:

```text
Peer B accepted input earlier
Peer A temporarily missed it
Peer A later converges
```

This is extremely important for decentralized networking.

---

# Finalization

Eventually ticks become immutable.

After grace window expiration:

- corrections rejected
- rollback impossible
- history finalized
- old query logs removable
- old snapshots removable
- old sparse inputs removable

This bounds memory growth.

Memory usage depends primarily on:

- rollback horizon
- query density
- active participant count

NOT total runtime.

---

# Authority Model

The system does NOT have:

- a permanent master peer
- globally authoritative ownership
- a single source of truth

Authority exists only:

> as a local tie-breaking mechanism for convergence

Typically:

- older simulations preferred
- lower IDs break ties

But authority only matters when:

- histories conflict
- convergence becomes necessary

This is intentionally lightweight. In severe-desync recovery authority is NOT
the primary decider — it is only the TIEBREAKER for an even completeness score
(see *Severe Desync Recovery*).

---

# Severe Desync Recovery

When a FINALIZED checkpoint diverges (catastrophic), the network must collapse
to a single history. Pure authority ("older simulation wins") is too blunt: a
peer that was briefly isolated could force a whole agreeing group to adopt ITS
state merely because it is older. Instead each peer computes a COMPLETENESS
SCORE for the contested finalized frame, and the more-complete history wins;
authority is only the tiebreaker.

## Completeness Score

Evaluated at `min(myFinalizedFrame, theirFinalizedFrame)`, summing over
players:

```text
+1   a player I have finalized that they do not
-1   a player they have finalized that I do not
+1   a shared player whose most-recent finalizing input I hold is NEWER
-1   a shared player whose most-recent finalizing input THEY hold is newer
 0   a player both finalize identically (or known to have no input)
```

Because inputs are accepted only IN ORDER (no gaps — you cannot accept an input
that skips the one before it), the most-recent input before the finalized frame
is a SUFFICIENT summary of a shared player's completeness: there is nothing
"between" two most-recent inputs to have missed.

Verdict:

```text
score > 0   I WIN   -> do nothing
score < 0   I LOSE  -> request the winner's full state
score = 0   tie     -> authority tiebreaker (older simulation, then lower id)
```

## Zero-Handshake

Both peers compute the SAME verdict from data they already share, so no
agreement round is needed. The LOSER is the only actor: it requests full state
from the exact peer it lost to — not a broadcast, not a poll of others. The
winner simply serves when asked and otherwise does nothing; it discovers it won
implicitly by never being asked to yield.

## Lost-Request Liveness

The request/serve exchange must not WEDGE if a request (or its reply) is lost.
A single in-flight guard with no timeout lets one dropped request permanently
suppress recovery — pinned RED by `tests/recovery-lost-request.test.js`. The
loser therefore RE-REQUESTS on a rate-limited timeout until the transfer lands.
(Exact cadence is open — "something that makes sense"; see `KNOWN_ISSUES.md`.)

Eventually the network should converge back toward identical simulation.

---

# Waking / Lagging Peers

Peers that become heavily delayed:

```text
> several seconds behind
```

may reset their local “simulation age” for authority comparison purposes.

This prevents:

- stale isolated peers
- outdated authority dominance
- broken recovery behavior

The goal is stabilization rather than permanent hierarchy.

---

# Peer-To-Peer vs Server-Client

The architecture supports both.

---

# Peer-To-Peer

Advantages:

- decentralized
- lower infrastructure cost
- resilient
- scalable passive participation

Challenges:

- trust
- cheating
- convergence complexity
- disconnect agreement

---

# Server-Client

Advantages:

- authoritative execution
- anti-cheat
- simpler trust model
- easier validation

Easy Multiplayer can still use all semantic rollback optimizations under server authority.

This flexibility is one of the architecture’s strengths.

---

# Security Model

Current assumption:

```text
cooperative peers by default
```

For trusted execution:

server-authoritative architecture is recommended.

However:

cryptographic validation of:

- inputs
- hashes
- accepted histories

is possible in the future.

This remains an open exploration area.

---

# Dynamic Interest Areas

One of the most promising future directions.

Example:

- thousands of users connected
- only nearby entities matter
- only nearby interactions queried

The difficult problem is NOT:

```text
what DID matter
```

It is:

```text
what COULD HAVE mattered under rollback divergence
```

This requires semantic understanding of game possibility space.

The protocol alone cannot fully solve this.

Game-level relevance knowledge is still necessary.

Currently this is handled primarily through:

- contextual input construction
- developer-controlled relevance shaping

This remains an important future research direction.

---

# Recommended Engine API Direction

Current best direction:

```js
const mp = new EasyMultiplayer({

    initialState,

    tick(state, query)
    {
        for(const playerId of state.players)
        {
            const player = state.players[playerId];

            if(player.grounded)
            {
                if(query(playerId, input => input.jump))
                {
                    player.velocityY = 10;
                }
            }
        }
    },

    getLocalInputs(localGameState)
    {
        if(localGameState.state !== "inGame")
        {
            return null;
        }

        return {
            jump: buttonA,
            moveX: joystick.x
        };
    }
});
```

---

# Most Important Novel Aspects Of The Design

The redesign introduces several genuinely novel concepts.

---

## 1. Semantic Rollback Invalidation

Rollback depends on:

```text
query outcome changes
```

NOT:

```text
raw input mismatches
```

This is arguably the central innovation of the architecture.

---

## 2. Query-Aware Simulation Relevance

Inputs only matter if:

```text
game logic actually queried them
```

This enables:

- rollback elimination
- sparse synchronization
- scalability improvements

Traditional rollback systems generally do not operate this way.

---

## 3. Contextual Intent Construction

Inputs are interpreted locally into:

```text
semantic player intent
```

before entering rollback simulation.

This avoids:

- rollback reinterpretation bugs
- unintended contextual action changes

and preserves:

```text
what the player MEANT to do
```

This is an extremely important UX improvement.

---

## 4. Sparse Relevant Knowledge Synchronization

The system synchronizes:

```text
relevant semantic knowledge
```

rather than:

```text
complete raw input history
```

This fundamentally changes desync semantics.

---

## 5. Uncertainty-Aware Desync Detection

Different state hashes are NOT automatically desyncs.

Peers may simply possess:

```text
different unresolved relevant knowledge
```

This is a major conceptual departure from traditional rollback assumptions.

---

## 6. Emergent Participation

The protocol does not require explicit:

- spectator states
- inactive modes
- participation classes

Participation naturally emerges from:

```text
whether semantically meaningful inputs exist
```

This is highly scalable.

---

# Final Conclusion

Easy Multiplayer is no longer simply:

```text
rollback netcode with optimizations
```

The redesign is evolving toward:

```text
a semantic distributed simulation architecture
```

where:

- silence is meaningful
- intent matters more than raw hardware state
- rollback depends on semantic query outcomes
- synchronization depends on relevant knowledge
- desyncs are uncertainty-aware
- participation emerges naturally

The system combines:

- rollback networking
- sparse synchronization
- semantic invalidation
- contextual intent interpretation
- decentralized convergence

into something substantially different from traditional GGPO-style rollback.

The most genuinely novel parts appear to be:

- semantic rollback invalidation
- contextual intent networking
- uncertainty-aware desync determination
- query-based simulation relevance

Together these ideas form the real conceptual core of the architecture.

