Skip to content

Networking: assemble the server-authoritative multiplayer loop + playable 2-client demo #636

Closed

Description

@drsnuggles8

Problem

OloEngine's networking subsystem contains ~20 replication/prediction/interest/lag-comp classes and a working GameNetworkingSockets transport — all unit-tested in isolation — but they are never assembled into a running server-authoritative loop. The pieces exist; the game does not.

Verified dead seams (grep + read, 2026-07-11):

  • NetworkManager::TickSnapshots() has zero call sites. NetworkThread::ThreadFunc (NetworkThread.cpp:78) only runs GNS callbacks + named-thread tasks. The header comment claiming it "invokes TickSnapshots()" is false. The server-side capture+delta+broadcast never runs.
  • NetworkManager::SetActiveScene() is never called in production. OloServer/src/OloServerApp.cpp loads a scene and ticks OnUpdateRuntimeFixed, but never registers it with networking. s_ActiveScene is always null, so TickSnapshots would early-out anyway.
  • SnapshotInterpolator::Interpolate(scene, dt) is called only from tests. Received snapshots pile into the buffer and are never applied to the client ECS — remote entities never move.
  • Prediction/reconciliation never fires end-to-end. No InputApplyCallback is registered in production; the server never sends InputAck, so ClientPrediction::Reconcile is dead.
  • RPC is a bare ENetworkMessageType enum value (NetworkMessage.h:21). No registry, marshalling, authority routing, or script bindings exist (RegisterRPC/InvokeRPC/RpcDispatcher = no matches anywhere).
  • Interest management is wired only into the separate MMO ZoneServer, not the core loop — TickSnapshots broadcasts one shared delta to all clients with no per-connection baseline or relevance culling.
  • LagCompensator is never invoked by the input/hit path (ServerInputHandler::ProcessInput does no rewind).
  • EntitySpawn/EntityDespawn are enum tokens with no lifecycle replication — snapshots assume a static entity set; server-spawned entities never appear on clients.

No open GitHub issue tracks this.

Why it matters

The engine advertises networking (dedicated OloServer target, MMO/lobby/P2P classes, Lua Network.* table) but cannot run a single authoritative multiplayer match. This is the highest-decay area in the codebase: a large, well-tested stack that produces nothing playable because the last-mile assembly and several keystone systems (RPC, spawn/despawn, interest-scoped replication, lag-comp integration) were never built. Bringing it to life turns a dead stack into the engine's flagship differentiator.

What EXISTS today

  • Transport: NetworkServer/NetworkClient/NetworkConnection (GNS), live-tested by tests/Networking/NetworkIntegrationTest.cpp.
  • Replication: EntitySnapshot (Capture/CaptureDelta/Apply), ComponentReplicator, ComponentInterpolationRegistry, SnapshotBuffer, SnapshotInterpolator.
  • Prediction: ClientPrediction, ServerInputHandler, NetworkInputBuffer.
  • Lag comp: LagCompensator (rewind primitive).
  • Interest: NetworkInterestManager, SpatialGrid, NetworkPriorityQueue, RelevanceTier.
  • Components: NetworkIdentityComponent + ENetworkAuthority{Server,Client,Shared} (Components.h:3509), NetworkInterestComponent, NetworkLODComponent.
  • Facade: NetworkManager (server capture+broadcast in TickSnapshots, SendInput, reconcile handlers in Connect).
  • Lua: Network.isServer/connect/startServer/… (LuaScriptGlue.cpp:2385).

Proposed approach

  1. Core loop driver — drive TickSnapshots() from the NetworkThread (or a dedicated fixed-rate replication tick); register the active scene via SetActiveScene() from OloServerApp and the editor host; drive SnapshotInterpolator::Interpolate() per client frame.
  2. Authoritative ECS replication + entity lifecycle — replicate spawn/despawn (wire the existing tokens), server-UUID→client-entity mapping, authority-gated writes.
  3. Client prediction + reconciliation end-to-end — real InputApplyCallback, per-tick input sampling, server InputAck, replay-on-divergence.
  4. Registered RPC system — typed marshalling over FArchive, authority routing (Server/Client/Multicast/target), reliability, dispatch over the RPC message type.
  5. RPC script bindings — RegisterRPC/InvokeRPC in Lua (LuaScriptGlue.cpp) and C# (ScriptGlue.cpp).
  6. Interest-scoped core replication — per-connection scoping via NetworkInterestManager/SpatialGrid/RelevanceTier + per-client delta baselines.
  7. Lag compensation integration — rewind to the client's RTT-adjusted tick before authoritative hit checks, then restore.
  8. Dedicated-server integration — OloServerApp registers its scene, spawns/despawns a player entity per connection, drives the replication tick.
  9. Playable 2-client demo + functional tests — a SandboxProject scene running the full loop, plus Functional-axis convergence/authority/RPC/interest/lag-comp tests.

Acceptance criteria

  • TickSnapshots() is driven and SetActiveScene() is wired from both OloServerApp and the editor host; a connected client's remote entities move via SnapshotInterpolator::Interpolate().
  • Server spawns/despawns replicate to clients with a stable server-UUID→client-entity mapping; client writes to server-authoritative entities are rejected.
  • Client prediction + server InputAck + reconciliation converge end-to-end (replay of unacknowledged inputs on divergence).
  • A registered RPC system marshals typed args, routes by authority (Server/Client/Multicast/target), and selects reliability — no longer a bare enum.
  • RPCs are callable from Lua and C# scripts.
  • The core replication loop scopes snapshots per connection with per-client baselines and relevance culling (not one shared broadcast).
  • LagCompensator is invoked from the server input/hit path (rewind → check → restore).
  • OloServer runs the full authoritative loop: player-per-connection lifecycle + replication tick.
  • A playable 2-client demo scene exists under SandboxProject, plus Functional tests asserting cross-client convergence, authority rejection under the live loop, RPC round-trips, interest culling, and lag-comp hit registration.
capability: 5
craft: 4
stability: 2
decay: 5
effort: 13
confidence: 0.92
learning: 4
fun: 5
kano: delighter
blocked_by: []
blocks: []

Activity

  1. coderabbitai commented on Jul 11, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #162 - Feature/networking [closed]
    #334 - MCP: per-worktree discovery-file isolation + skill auto-attach (#316 … [merged]
    #382 - feat(network): spatial-grid prefilter + relevance evaluation for NetworkInterestManager [merged]
    #561 - fix(networking): correct prediction/reconciliation and state-lifecycle bugs [merged]
    #743 - feat(networking): assemble the server-authoritative multiplayer loop (#636) [open]


    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. drsnuggles8 commented on Aug 3, 2026

    @drsnuggles8
    OwnerAuthor

    Implemented in #743.

    Acceptance criteria — evidence

    # Criterion Evidence
    1 Loop driven; SetActiveScene() wired from OloServerApp and the editor host; remote entities move via Interpolate() NetworkManager::Tick(dt) is the single game-thread entry point, called from OloServerApp, OloRuntimeApp and EditorLayer (which registers only the Play scene, never m_EditorScene). ServerAuthoritativeLoopTest.ClientInputMovesItsPawnOnTheServerAndOnTheOtherClient asserts client 1 sees client 0's pawn move — which travels through the interpolator.
    2 Spawn/despawn replicate with a stable server-UUID→client-entity mapping; client writes to server-authoritative entities rejected EntityLifecycle + NetworkSpawnRegistry. EachConnectionGetsAPawnThatBothClientsSee, DisconnectDespawnsThePawnOnTheRemainingClient, ClientOnlyDestroysEntitiesTheSpawnPathCreated, ServerRejectsInputForAServerAuthoritativeEntity, plus 10 EntityLifecycleTest codec cases.
    3 Prediction + InputAck + reconciliation converge end-to-end PredictionAndReconciliationConvergeOnTheServerPosition — asserts convergence, not merely "it moved".
    4 Registered RPC: typed marshalling, authority routing, reliability RpcRegistry / RpcDispatcher; 12 RpcMarshallingTest cases + 4 live-loop RPC tests.
    5 RPCs callable from Lua and C# Lua verified live: OloServer runs the demo's Network.useMovementInput and both Network.registerRPC calls at startup. C# surface (OloEngine.Network) compiles into OloEngine-ScriptCore.dll.
    6 Per-connection scoping, per-client baselines, relevance culling Per-connection relevance set, known-entity set and acknowledged baseline (new SnapshotAck). InterestScopingWithholdsADistantEntity plus its control InterestScopingDisabledReplicatesEverything.
    7 LagCompensator invoked from the server path ServerReplicationDriver::PerformLagCompensatedCheck rewinds by half-RTT plus the client's render delay. LagCompensationRewindsToWhereTheClientSawTheWorld asserts the check saw the past and that the world was restored.
    8 OloServer runs the full loop Live: mounts the project, loads the scene, runs its Lua, replication tick at 20 Hz.
    9 Playable 2-client demo + Functional tests SandboxProject/Assets/Scenes/NetworkDemo.olo + NetworkDemoController.lua; ServerAuthoritativeLoopTest stands up a real server and two real clients over loopback (15 cases).

    Test totals: 37 new networking tests pass; a 580-test regression sweep across networking, replication, components, MMO, lockstep, P2P and persistence is green (1 unrelated skip).

    Honest caveats

    • The C# RPC path is compile-verified, not runtime-verified. Lua is the one exercised live.
    • The GUI client was not driven end-to-end: connecting needs interactive input this session cannot inject. The editor was confirmed to launch and render cleanly with the new wiring, and the two-client loop is proven over real sockets by the Functional test rather than by a screenshot.

    Pre-existing bugs surfaced

    Three were in code that had never executed — which is the pathology this issue was about:

    1. NetworkClient::PollMessages dispatched handlers inside its own mutex, so any handler that replied self-deadlocked. NetworkServer::PollMessages already deferred dispatch outside the lock and documents why.
    2. GetClientPingMs re-acquires the mutex ForEachConnection holds. Three shipped call sites had that shape — including the editor's Network Debug panel, so opening it against a live server froze the editor.
    3. OloServer mounted no Project, so scene-referenced Lua never resolved: the dedicated server could host a world but run none of its server-side gameplay.

    Lessons captured in docs/agent-rules/server-authoritative-networking-loop.md (and an mspdbsrv build-infra note in build-trees-and-windows-asan.md).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requesthelp wantedExtra attention is neededrobustnessProduction hardening / shipping robustness

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions