GregOrigin - Rollback Core Pro: Accessible Netcode Framework for Fighting & Platform Games

Initial video (v0.2) | ✍🏼Read the manual. | 💬 Discord support. | 🎁 Get the Core version on GitHub: a functional open-source variety that I recommend you try before buying the Pro version. | 👨‍💼 Contact for custom integration / implementation.

🔥Say goodbye to input lag and latency in Unreal Engine! Introducing Rollback Core (Pro), the ultimate deterministic GGPO-style rollback netcode framework built specifically for UE5.

Whether you're building a fast-paced fighting game, a platform brawler, or a tight co-op experience, standard server-client networking just doesn't cut it. Rollback Core flips the script: simulate locally, predict remote inputs, and seamlessly re-simulate history when the truth arrives late. The result? Zero perceived input lag for your players. ⚡️🎮

✨ Why Choose Rollback Core?

🔹 Auto State-Saving (No Code Required): Forget writing complex C++ serializers. Just check the standard SaveGame flag on any Blueprint variable, and we'll automatically handle snapshots, transforms, and velocities via reflection. 💾

📖 Custom UDP Transport: Highly-optimized netcode featuring per-frame input redundancy, reliable ACKs, and built-in latency/packet-loss simulation for effortless stress testing. 📡

💬 Deterministic Fixed-Step Logic: Decouple your visual framerate from your game logic! Runs a perfectly rigid, predictable simulation under the hood. ⏱️ 👀 Visual Frame Debugger: Spawn our intuitive debugger right into your world to scrub through frames, view prediction ghost trails, and analyze rollback markers in real-time. 👻

👇 Seamless Matchmaking: Full OnlineSubsystem support so your players can effortlessly advertise, query, and join sessions out-of-the-box. 🤝

👾 3 Playable Demos Included: Learn by doing! We've included a fully playable 2D Viking Duel (with hitboxes & blockstun), a Network Packet-loss stress test, and a Basic Prediction environment. ⚔️

🛡️ Guided Editor Wizard: Get set up in 5 minutes with the onboarding Wizard. It automatically fixes project settings and injects the necessary rollback components directly into your existing Pawn. 🪄

🚀 Ready to upgrade your netcode?


Disclaimer: this plugin assumes a working knowledge of UE5 workflows, editor tooling, asset setup and so on. Custom integration, extension or modification may also require comfort reading and writing C++. If you are new to UE5 or plugin-based development, expect a learning curve. Before purchasing the Fab version, it is recommended that you explore the open-source GitHub version and the online manual first to understand the architecture, setup requirements, and integration style. This will help you determine whether the plugin fits your project and experience level.

Launch video:

Can it be used for a 3D fighting game in the likes of Tekken?

Hi there. It can, in fact 2 of the basic demos that come with it are fully 3D. And I’ll make more demos in the future.

1 Like

0.2.0, 2026-06-16 :wireless:

Networking correctness pass for peer identity and disconnect handling,
driven by reported issues with the Rollback.Net* console
workflow. Contains a UDP wire-protocol change (packet
version 1 → 2): all participants must run this build to interoperate.

Changed

  • :shield: Wire protocol v2. Every UDP packet header now carries a
    SenderPlayerId field and PacketVersion is bumped 1 → 2. Mismatched
    versions are rejected (never misparsed), so v2 peers cannot talk to v1
    peers. Ship all participants on this version.
  • Rollback.NetClient argument semantics. The 4th arg is
    now this client’s own PlayerId, announced to the host, instead of
    the remote peer’s id. The host now identifies the client by it, so
    Rollback.NetDisconnect <id> / Rollback.NetPeers can reference it.
    Rollback.NetConnect still takes the remote peer’s id. Help and
    usage text updated.
  • Peers now learn each other’s PlayerId from the handshake header on
    any packet (including Hello), instead of only from received input
    frames. An id set explicitly via ConnectToPeer is never overridden.
  • Rollback.NetHost now announces a host identity (LocalPlayerId = 0).
  • Rollback.NetClient now opens a single connection. It previously both
    auto-connected (via StartUdpPeer with RemoteHost set) and called
    ConnectToPeer, creating two peer records for the same endpoint.
  • URollbackNetSubsystem::ConnectToPeer is now idempotent per endpoint:
    reconnecting to an already-connected endpoint refreshes that peer (and
    promotes its id) instead of adding a duplicate record.
  • URollbackNetSubsystem::DisconnectPeer now returns bool (was
    void): true when a matching peer was found and removed.
  • URollbackNetworkBlueprintLibrary::DisconnectRemotePeer Blueprint node
    now returns bool. This adds an output pin; existing graphs that
    ignore it are unaffected.
  • Rollback.NetDisconnect logging is now accurate: it reports remaining
    peers from GetAllPeerInfo() (consistent with Rollback.NetPeers) and
    prints “No connected peer with PlayerId=N” when nothing matched,
    instead of always claiming success.

Added

  • FRollbackTransportConfig::LocalPlayerId (default -1): this peer’s
    own id, announced in every packet header. -1 preserves the previous
    behaviour of learning ids from input frames.
  • FRollbackTransportConfig::DisconnectCooldownSeconds (default 3.0):
    after an explicit DisconnectPeer, the endpoint cannot be
    auto-re-accepted for this window.
  • Automation tests: RollbackCorePro.Network.PeerDisconnectStaysGone,
    .PeerIdentityHandshake, .ConnectEndpointDedup, .DisconnectReturn.

Fixed

  • Disconnected peer immediately reappearing. DisconnectPeer removed
    the peer, but the still-running remote’s next packet hit the
    auto-accept path and re-created it with reset counters. A per-endpoint
    cooldown now prevents resurrection.
  • Rollback.NetDisconnect <id> doing nothing yet reporting success
    for console-connected peers. The host never learned the peer’s id (it
    stayed -1), so the id never matched. Header identity + accurate
    logging fix this.
  • Duplicate peer record on the connecting client. Connecting with an
    explicit id produced two records for the host endpoint (one -1, one
    the id); the -1 record received all traffic and showed real
    RTT/counters while the id record sat idle. Single-connect + endpoint
    dedup fix this.
  • Rollback.NetDisconnect “Remaining peers” disagreeing with
    Rollback.NetPeers
    for unassigned (-1) peers: the two commands
    counted different collections. Both now read GetAllPeerInfo().
  • Flaky RollbackCorePro.Network.PeerDisconnect automation test (it
    asserted connectivity before the receiving subsystem had pumped the
    handshake) made deterministic with a pump loop.

Build / verification

  • Compile-validated clean on UE 5.5, 5.6, and 5.7 via rcp_assemble.ps1; full suite green on 5.7 (15/15).
  • Per-engine staging regenerated for 5.5 / 5.6 / 5.7. The Blueprint DisconnectRemotePeer return-bool change landed afterward and is source-only.

0.2.0a update, 2026-06-16

Added

  • URollbackNetSubsystem::OnPeerReconnected (BlueprintAssignable): fired when a previously-known endpoint is auto-accepted again after a timeout, carrying the restored PlayerId and endpoint. Additive; lets UI distinguish a transient reconnect from a brand-new connection.
  • Rollback.NetJoinNull [Index=0] [LocalPlayerId=1] [LocalPort=0] and Rollback.NetSessions console commands. These complete the console-driven Null-LAN matchmaking loop in a packaged build: a host advertises (Rollback.NetHost), a second instance discovers (Rollback.NetFindNull), lists results (Rollback.NetSessions), and joins (Rollback.NetJoinNull), which brings up the UDP transport (announcing its own id) and connects to the advertised host endpoint. Previously JoinPlatformSessionByIndex was only reachable from the editor Setup Wizard.

Fixed

  • Timed-out peer reconnecting as a fresh PlayerId=-1. When a peer
    went quiet long enough to time out (CheckPeerTimeouts) and then
    resumed sending, the auto-accept path re-created it as a brand-new
    -1 record with reset counters, so OnPeerDisconnected (“Peer timed
    out”) was immediately followed by Rollback.NetPeers showing a
    connected -1 peer with recent activity, which looked contradictory.
    A timeout (unlike an explicit kick) now records the endpoint’s
    PlayerId; the returning peer is re-accepted as the same peer with
    that id restored and fires OnPeerReconnected instead. The explicit
    DisconnectPeer cooldown path is unchanged (kicked peers stay gone).
  • Disconnected peer immediately reappearing. DisconnectPeer removed
    the peer, but the still-running remote’s next packet hit the
    auto-accept path and re-created it with reset counters. A per-endpoint
    cooldown now prevents resurrection.
  • Rollback.NetDisconnect <id> doing nothing yet reporting success
    for console-connected peers. The host never learned the peer’s id (it
    stayed -1), so the id never matched. Header identity + accurate
    logging fix this.
  • Duplicate peer record on the connecting client. Connecting with an
    explicit id produced two records for the host endpoint (one -1, one
    the id); the -1 record received all traffic and showed real
    RTT/counters while the id record sat idle. Single-connect + endpoint
    dedup fix this.
  • Rollback.NetDisconnect “Remaining peers” disagreeing with
    Rollback.NetPeers
    for unassigned (-1) peers — the two commands
    counted different collections. Both now read GetAllPeerInfo().

Optional map (Content/Maps/RollbackNetDemo) for the available 2-Peer UDP Demo:

RollbackNetDemo.umap (20.8 KB)
DefaultEngine.ini (601 Bytes)

Please note that the plugin is still 0.2.0: not recommended for live production games yet. It’s robust, but still “early access” till indicated otherwise.
As always, liberal experimentation with the open source core is recommended at this point. :microscope:

0.2.2 update, 2026-06-18 :chequered_flag:

:video_game: Matchmaking — async Blueprint nodes, configurable search timeout & examples

A Blueprint-first matchmaking pass: the previously hidden session-search timeout is now configurable and readable, and the Find / Host / Join flow is exposed as Advanced-Sessions-style latent nodes with example assets. Additive with no API breaks.

Legend: :sparkles: new Blueprint node · :plus: new API · :counterclockwise_arrows_button: changed · :white_check_mark: tests · :memo: docs · :wrench: tooling · :snake: Python · :puzzle_piece: example asset

:stopwatch: Configurable session-search timeout

  • :counterclockwise_arrows_button: URollbackNetSubsystem::FindPlatformSessions no longer hard-codes a
    10.0f search timeout; it reads a configurable value, so callers can size
    their own “Searching…” watchdog/UI to the real search instead of guessing.
  • :plus: SessionSearchTimeoutSeconds (EditAnywhere, default 10s, ClampMin 0.1)
    plus GetSessionSearchTimeoutSeconds() (BlueprintPure) and
    SetSessionSearchTimeoutSeconds() (BlueprintCallable, clamps to 0.1).

:satellite_antenna: Results-carrying completion delegates

  • :plus: OnFindSessionsComplete(bool, TArray<FRollbackMatchmakingResult>, FString); hands back the whole session list in one shot when a search finishes.
  • :plus: OnCreateSessionComplete(bool, FName SessionName, FString).
  • :plus: OnJoinSessionComplete(bool, FString); fires after the UDP transport
    actually connects on a successful join (kept in lock-step with the generic
    OnMatchmakingComplete via an internal BroadcastJoinComplete helper).

:sparkles: Async / latent matchmaking nodes (the Find / Host / Join trio)

  • :sparkles: Find Rollback Sessions (Async); URollbackFindSessionsAsync: runs a
    search, waits for completion (bounded by the timeout), delivers the list on
    On Found / On Failed. Optional Timeout Seconds pin (-1 = use configured).
  • :sparkles: Host Rollback Session (Async); URollbackCreateSessionAsync: brings up
    the UDP transport on Host Port if needed, advertises the session →
    On Hosted (SessionName) / On Failed.
  • :sparkles: Join Rollback Session (Async); URollbackJoinSessionAsync: joins a Find
    result and waits until the transport connects → On Joined / On Failed.
  • :plus: Pure helpers FindBestRollbackSession (lowest ping with an open slot) and
    FormatRollbackSession (one-line summary) on URollbackNetworkBlueprintLibrary.

:puzzle_piece: Example actor & Blueprint generator

  • :puzzle_piece: ARollbackMatchmakingExampleActor (Blueprintable) wires all three nodes
    into one actor with editable defaults (Match Id, LAN, port, timeout, auto-find)
    and OnSessionsFound / OnHosted / OnJoined / OnError events for UI.
  • :snake: Content/Python/generate_matchmaking_examples.py; run in-editor to create
    /RollbackCorePro/Examples/BP_RollbackMatchmakingExample (a ready-to-drop
    subclass of the example actor).

:white_check_mark: Tests

  • :white_check_mark: New automation tests: RollbackCorePro.Matchmaking.FindSessionsCompleteDelegate,
    .FindSessionsAsyncNode, .FindSessionsAsyncNodeFailure,
    .HostSessionAsyncNodeFailure, .JoinSessionAsyncNodeFailure.
  • :white_check_mark: Added URollbackMatchmakingTestSink, a reflected UObject that captures the
    dynamic-delegate / node-pin payloads tests assert on.
  • :counterclockwise_arrows_button: RollbackCorePro.Network.NullMatchmakingConnect now derives its poll window
    from GetSessionSearchTimeoutSeconds() instead of a magic 12.0 constant.

:memo: Docs

  • :memo: HTML manual: new delegate rows, an async-node table (Find/Host/Join), the
    pure helpers, the example-asset + Python-generator instructions, and an updated
    client-flow snippet showing the obtainable timeout.

:wrench: Project / tooling

  • :wrench: Enabled PythonScriptPlugin in RollbackCorePro.uproject (required to run
    the example generator).

:hammer_and_wrench: Build / verification

  • :hammer_and_wrench: Clean on UE 5.5 - 5.7; RollbackCorePro.Matchmaking automation suite 6/6
    green
    ; the Python generator verified to produce the example Blueprint asset.

A 2-peer demo in action, version 0.2.2+

0.2.2a update, 2026-06-20 :up_arrow:

🎮 UMG matchmaking menu example

  • 🧩 URollbackMatchmakingMenuWidget (with row widget URollbackMatchmakingMenuRow): a fully native UMG menu (no .uasset required): Host / Find buttons and a scrollable list of found sessions, each with a Join button. It builds its own tree in RebuildWidget, drives the Find / Host / Join async nodes (mirroring ARollbackMatchmakingExampleActor), and exposes config (Match Id, LAN, port, timeout, …) plus OnHosted / OnJoined / OnSessionsFound / OnError events. Subclass it as a Widget Blueprint to restyle: the behaviour stays in C++.
  • Rollback.ShowMatchmakingMenu: creates the menu and adds it to the viewport (running game / PIE).
  • 🧩 The example generator now also emits /RollbackCorePro/Examples/WBP_RollbackMatchmakingMenu (a Widget Blueprint subclass of the menu widget).

🖥️ Console-command parity for the async Host/Find/Join flow

  • Rollback.Host [MatchId] [Port=7777] [Slots=2] [LAN=1], Rollback.Find [MatchId] [Max=20] [LAN=1] [Timeout=-1], and Rollback.Join [Index=0]: drive the same URollbackCreateSessionAsync / URollbackFindSessionsAsync / URollbackJoinSessionAsync nodes the Blueprint flow uses, for console/headless testing parity. Rollback.Join joins a result cached by a prior Rollback.Find. (Previously the async flow was Blueprint-only; the existing Rollback.Net* commands call the subsystem directly.)
  • URollbackNetConsoleSink: a small UObject that binds the async nodes' dynamic result pins (which cannot bind lambdas), logs each outcome, and self-releases.

🔧 Editor: one-click example generation

  • 🔧 Generate Example Content on the Rollback toolbar combo (new Examples section) and Generate Example Blueprints on the Setup Wizard's Integration page run the bundled generator via IPythonScriptPlugin (FRollbackCoreProEditorModule::GenerateExampleContent() → returns whether it ran), degrading gracefully with an on-screen note when the Python Editor Script Plugin is unavailable. The wizard reflects the real outcome in its toast and Finish-page summary.
  • 🔧 RollbackCoreProEditor now privately depends on PythonScriptPlugin, and the plugin descriptor enables it.
  • 🐍 generatematchmakingexamples.py extended to also author the WBP menu via WidgetBlueprintFactory.

✅ Tests & verification

  • ✅ New automation tests: RollbackCorePro.Matchmaking.ConsoleSinkHostFailure, .ConsoleCommandsRegistered, RollbackCorePro.UI.MatchmakingMenuRowJoin, .ShowMatchmakingMenuRegistered.

Rollback P2P demo. Edit the file to use your own paths. (Powershell scripts are not allowed in Fab builds.)

README.md (4.3 KB)
RunRollbackP2PDemo.ps1 (3.7 KB)

0.3.0 update, 2026-06-23 :roller_skate:

Rollback correctness & netcode pass: fixes the core replay off-by-one, completes
state restore (velocity / angular velocity / CharacterMovement), and upgrades desync
detection from a local replay check to genuine cross-peer checksum exchange
(auto-wired). Contains a breaking UDP wire-protocol change (packet version 2 → 3):
all peers must run this build to interoperate.

Legend: :bullseye: correctness · :shield: hardening · :globe_with_meridians: networking · :ice: physics/CMC · :plus: new API · :sparkles: new component · :counterclockwise_arrows_button: changed · :warning: breaking · :white_check_mark: tests · :hammer_and_wrench: build/verify · :memo: docs

:bullseye: Rollback correctness

  • :bullseye: Off-by-one double-application fixed. SimulateFrame now snapshots the
    start-of-frame state before ticking, so RollbackToFrame(F) restores
    StateBuffer[F] and replays from F applying the target frame’s input exactly
    once
    . Previously the snapshot was taken after the tick, so every rollback
    re-applied the corrected frame twice and drifted by one frame of its motion. (The
    prior tests used all-neutral input, where the double-apply was invisible.)
  • :white_check_mark: RollbackCorePro.Rollback.NoOpReplayIsIdentity: a no-op rollback (identical
    recorded inputs) must reproduce the exact pre-rollback state.

:globe_with_meridians: Cross-peer desync detection: was local-only

  • :warning: BREAKING: wire protocol v3. New StateChecksum packet type; PacketVersion
    bumped 2 → 3. Mismatched versions are rejected (never misparsed), so v3 peers
    cannot talk to ≤ 0.2.2 (v2) peers. Ship all participants on this build.
  • :plus: Genuine cross-peer detection: peers exchange per-frame state checksums and
    compare each against their own; a true mismatch calls RecordDesync() and broadcasts
    OnDesyncDetected(Frame, peer, delta). Previously the “desync detector” only compared
    an entity’s own pre/post-rollback checksums locally (a replay-determinism check) and
    could never observe two machines diverging.
  • :plus: Auto-wired: URollbackManager records and (on an interval) sends the
    confirmed-frame checksum automatically while transport is connected — the demos
    inherit it for free. New API: RecordLocalStateChecksum, SendStateChecksum,
    ComputeStateChecksum (order-independent XOR aggregate), GetConfirmedFrame /
    GetConfirmedFrameForPlayer.
  • :counterclockwise_arrows_button: Confirmed-frame policy: only frames whose inputs are all in from every peer
    (highest contiguous received input) are checksummed, so the state is final and a
    mismatch is a real desync: no false positives from in-flight prediction.

:ice: State-complete physics / CharacterMovement rollback

  • :wrench: Velocity is now restored on rollback (it was saved but never re-applied).
    Physics roots get SetPhysicsLinearVelocity; every entity also surfaces it via
    LastRestoredVelocity and the new OnRollbackStateLoaded(Frame, Velocity) event so
    custom/kinematic movement can re-apply it.
  • :ice: Full rigid-body + CharacterMovement state captured/restored. The snapshot gains
    angular velocity and CMC MovementMode/CustomMovementMode; save/load now restore
    linear + angular physics velocity and CMC velocity/movement mode. Surfaced via
    LastRestoredAngularVelocity.
  • :sparkles: URollbackCharacterMovementComponent: a UCharacterMovementComponent subclass
    driven by the rollback loop (its own engine tick disabled); the state component calls
    its RollbackStep(dt) once per fixed frame for both live and replayed frames.
  • :warning: Determinism boundary unchanged: restore is now correct and complete, but
    cross-machine forward-sim determinism (collision / Chaos) is not guaranteed: use
    flat/kinematic movement or a deterministic mover where that matters.

:shield: Robustness

  • :shield: Rollback to an evicted/missing frame now aborts cleanly instead of replaying
    forward from the live state and corrupting the buffer. RollbackToFrame no-ops (no
    state change, no rollback counted) when no entity holds the target frame’s snapshot;
    usable rollback depth is bounded to MaxBufferSize snapshots per entity.

:white_check_mark: Tests & verification

  • :white_check_mark: 12 new automation tests (Rollback.NoOpReplayIsIdentity,
    Rollback.MissingFrameAborts, State.VelocityRestored,
    State.AngularVelocityRestored, State.CharacterMovementRestored,
    State.ChecksumDeterministic, State.ChecksumOrderIndependent,
    Network.CrossPeerChecksumDesync, Network.ConfirmedFrameTracking,
    Network.ManagerAutoChecksum, Movement.RollbackStepSafe,
    Movement.CMCDrivenByRollbackTick). Full RollbackCorePro suite 40/41 green (the
    two-process-only Network.NullMatchmakingConnect fails single-process by design).
  • :hammer_and_wrench: Two-process packaged P2P session verified: Null OSS matchmaking + UDP
    transport + the new v3 StateChecksum traffic ran ~1440 frames with both peers
    connected, no desync false positives, and no transport/parse errors.

Updating the manual to 0.3.0 (online & PDF) will take a few days, until then these features are undocumented. Thanks for your patience. :folded_hands:

1 Like

[0.5.0 update] - 2026-08-07 :roller_coaster:

Breaking changes: all peers must upgrade together :palms_up_together:

  • The wire protocol moves from v5 to v6. Every packet header now carries the sender’s own frame
    advantage, which is what lets pacing act on the difference between two peers instead of on the
    local confirmation age. A 0.4.0 peer and a 0.5.0 peer cannot play together. The failure is at
    connect time rather than a mid-match desync: through matchmaking the protocol string is a search
    filter, so a 0.4.0 peer never discovers a 0.5.0 session; connecting directly, version-mismatched
    packets are discarded on receipt — including the handshake — so it presents as a connection
    timeout with no explicit version error. Upgrade every participant, including packaged demos.
  • Max Frame Advantage changes meaning, and its default moves 8 → 4. It used to bound how far
    the local clock ran ahead of its own newest confirmed frame; it now bounds the smoothed
    difference between the two peers’ self-reported advantages. The difference counts each peer’s
    contribution once, so the new default of 4 tolerates 8 frames of real divergence. Projects that
    raised the old value should not carry that number across — start from the new default.

Behaviour changes: read before upgrading :coral:

  • Input delay now defaults to 2 frames (Simulation | Latency → Input Delay). Input polled
    while simulating frame F now drives frame F+2. This is the right default for netplay and
    removes the large majority of rollbacks, but it does change gameplay timing. Offline-only
    projects that want zero added latency should set it to 0. It must be identical on every peer.
  • The rollback subsystems are no longer created in editor worlds (Game and PIE only). The
    Rollback Stats panel now reports “inactive” outside PIE instead of showing an editor-world
    frame counter.

Fixed

  • Peers’ fixed-step clocks no longer free-run apart. Nothing previously kept two peers in
    step: the faster machine pulled ahead until incoming corrections were older than the rollback
    window, at which point RollbackToFrame rejected them outright and the peers diverged for good.
    URollbackManager now paces the local clock against the peer, clamped to Max Rollback Depth so
    a correction is always still shallow enough to apply. Exposed via GetFrameAdvantage(),
    GetFrameAdvantageDelta(), ShouldStallForFrameAdvantage() and StalledFrameCount. Regression
    test: RollbackCorePro.Production.FrameAdvantagePacing (a peer previously reached a 58-frame
    advantage against a 12-frame window in 60 ticks).
  • A high-latency match no longer runs in permanent slow motion. Pacing originally stalled on
    confirmation age: how old the newest confirmed frame was — which latency alone drives past the
    bound even when both clocks are perfectly in step, so every peer on a slow link stalled
    continuously. The pre-fix soak measured both peers pinned at the bound with 1183 stalls out of
    ~1199 frames and an effective rate of 0.80 Hz. Pacing now acts on the smoothed difference
    between the two peers’ self-reported advantages, so latency shared by both sides cancels to zero
    and only genuine divergence stalls anything. Regression test:
    RollbackCorePro.Production.SymmetricLatencyDoesNotStall.
  • Pacing no longer starves the peer it is pacing against. Withholding a frame also withholds
    the input the other peer needs to advance its own confirmation, so a clock that stopped entirely
    drove its peer’s advantage up until it stalled too, and the pair oscillated — measured on the
    180 ms profile as the effective rate halving to 40.84 Hz with confirmation frozen for 15.58 s of
    a 20 s run. Corrections are now applied at a duty cycle, withholding at most one frame in four,
    so the clock yields ground at 75% rate while the input stream never stops. The clock still holds
    outright against a peer that has genuinely gone silent while already past the rollback window,
    since frames advanced there could never be reconciled. Regression tests:
    RollbackCorePro.Production.PacingKeepsInputFlowing,
    RollbackCorePro.Production.StallHoldsInsideRollbackWindow.
  • A two-second lag spike no longer latches a session into permanent divergence.
    Max Future Input Frames (was 120) rejected any input frame more than that far ahead of the
    receiver’s clock, and the rejection path discards the whole packet — including the redundant
    copies of in-window frames that exist to heal holes. So once a peer fell 2 s behind, every packet
    from the leader was discarded wholesale and its confirmation froze permanently. That freeze then
    fed pacing the wrong sign, because GetFrameAdvantage() is CurrentFrame - ConfirmedFrame: the
    peer that was behind looked far ahead and got stalled, driving it further behind. Measured: 464
    packets rejected on one peer, confirmation stuck at frame 133 while the peer had produced 904.
    The window now defaults to 600, matching Max Remote Input Frames Per Player, which is what
    actually bounds the memory a peer can make you hold — so this costs no new exposure.
  • One dropped frame no longer freezes a peer’s confirmation for the rest of the session.
    Confirmation advances only over a contiguous prefix, and redundancy cannot heal a frame that was
    never sent in the first place. Callers poll input once per engine tick while the fixed clock can
    step up to Max Catch-Up Frames Per Tick frames, so any engine frame slower than the fixed rate
    silently dropped a frame from the wire — permanently. The documented Blueprint path
    (SendRollbackInputForCurrentFrame) and both demo environments were all affected. New
    SendInputFramesThrough() sends every frame from a per-player watermark through the current one,
    repeating the polled input across the gap — which is exactly what the local simulation did with
    it. Regression tests: RollbackCorePro.Network.InputStreamHasNoHoles,
    RollbackCorePro.Network.InputStreamPartialFillRetries.
  • Analog input no longer desyncs peers. FRollbackInput::Axes is a double-precision FVector
    but the wire carries three floats, so any stick value that is not exactly float-representable
    left the sender simulating a frame with 0.59999999999999998 while the receiver used
    0.60000002384185791. Axes are now normalised to wire precision at every entry point
    (InjectInputForFrame, SendInputFrame, BufferRemoteInputForRollback, local input capture)
    via FRollbackInput::QuantizeAxes(). Regression test:
    RollbackCorePro.Network.AnalogInputSurvivesWire.
  • URollbackManager and URollbackNetSubsystem no longer instantiate in editor worlds, where
    they ticked a second fixed-step clock and pumped a transport for the editor’s whole lifetime and
    gave every PIE session a sibling manager on an unrelated CurrentFrame. Regression test:
    RollbackCorePro.Production.NoEditorWorldSubsystems.
  • The Setup Wizard now persists settings with TryUpdateDefaultConfigFile() instead of
    SaveConfig(). URollbackCoreProSettings is a DefaultConfig class, so wizard changes were
    landing in the machine-local Saved/Config tree rather than the project’s DefaultEngine.ini
    that the Project Settings panel writes: the two paths could disagree and wizard choices were
    never source-controlled.
  • The wizard’s FilterPlugin check tested for a path that the blanket /Content/... rule already
    matched, so it always reported the rule missing and its “fix” appended a redundant line. It now
    tests for the exclusion rule that actually decides whether the demo content ships, and the fix
    removes that rule.

Known issues :smiling_face_with_tear:

  • The 180 ms / 3% loss soak profile still fails intermittently: roughly one run in four. When
    it trips, one peer’s confirmation freezes for around 3 s and the effective rate drops to ~53 Hz.
    Correctness is not affected in any observed run: DesyncCount and RejectedRollbackCount stayed
    at zero throughout, so this degrades pacing, it does not diverge peers. It is also distinct from
    the packet-rejection latch fixed above — rejections are zero in the failing runs — and its cause
    is not yet identified. For reference, 180 ms one-way is 360 ms RTT, well past what rollback
    fighting games target, and that profile already needs Max Rollback Depth raised to 20. The four
    profiles at or below 120 ms one-way are clean across every run.

Added

  • Simulation | Latency project settings: Input Delay (frames) (default 2), Enable
    Frame-Advantage Pacing
    (default on), Max Frame Advantage (frames) (default 8).
  • URollbackStateComponent::SetInputDelayFrames() / GetInputDelayFrames() for runtime control.
  • Rollback.NetAutoJoinNull [MatchId] [LocalPlayerId] [LocalPort]: searches for a Null LAN
    session and joins the best result automatically. The search is async, so a single -ExecCmds
    could not previously sequence find and join; this makes the whole matchmaking path scriptable.
  • Tools/Run-AutomationTests.ps1 — headless suite runner that exits non-zero on failure and
    fails the build if suspiciously few tests ran (which is how a silently-skipped plugin load
    presents itself).
  • .github/workflows/ci.yml — builds and runs the suite on UE 5.7, verifies two-process
    matchmaking, then compile-validates the plugin against 5.5, 5.6 and 5.7.
  • Tools/README.md documenting the supported packaging pipeline.

[0.6.0] update, 2026-08-31 :ringed_planet:

[This is a very technical & detailed changelog. All the info is not essential for plugin usage, but may be useful to some.]

The wire protocol is unchanged at v6, but you (the user) still need to ship this to every peer at once. The
float-semantics fix below changes the compiled simulation, and determinism is a property of the
binary rather than of the source. A 0.5.0 peer and a peer on this build therefore agree on the
protocol, connect normally, and then diverge in the last bit; which surfaces as a desync partway
into a match rather than as a clean refusal at the handshake. Treat this as a lockstep upgrade even
though nothing in the packet format moved.

Added :mechanic:

  • Protocol mismatches are reported instead of looking like a timeout. A datagram carrying our
    magic with a different version byte is a real peer on an incompatible build, and it was handled
    identically to line noise: PacketsRejected++ and nothing else. It now logs a warning naming
    both versions, counts FRollbackTransportStats::ProtocolMismatches, records
    GetLastProtocolMismatchVersion(), and broadcasts OnProtocolVersionMismatch (throttled per
    endpoint). Bind it to tell the player to update. GetWireProtocolVersion() and the
    RollbackWireProtocol constants are now public so a title can display and compare it.
  • Desyncs name the field that diverged. Previously a desync reported a frame, an entity, and
    the XOR of two checksums - which is not a lead. URollbackManager::CompareRollbackStates and
    DescribeRollbackStateDifference report per-field differences at full precision
    (Velocity: (12, 0, 0) -> (12.5, 0, 0)), and GetLastDesyncDetail() carries the newest one.
  • Desyncs inside the SaveGame payload can name the property. ActorData is one opaque blob, so
    it could only ever be reported as a byte offset. SetDetailedDesyncDiagnosticsEnabled(true) adds
    a per-property checksum to each snapshot and attributes the divergence by name. Off by default -
    it costs an extra reflection walk per saved frame.
  • ValidateDeterminism(FramesBack) catches nondeterminism in one process. Resimulates the
    window from its saved inputs and reports any field that comes out different. Most desyncs are not
    network faults but gameplay code that cannot reproduce its own result - unseeded randomness,
    TMap/TSet iteration order, uninitialised members - and those are enormously cheaper to find
    here than across two machines. Violations are counted separately from DesyncCount so an audit
    never perturbs the metrics it audits.
  • Socket subsystem selection is documented and diagnosable.
    FRollbackTransportConfig::SocketSubsystemName (and a project-wide default in Project Settings)
    selects the ISocketSubsystem carrying the transport - the seam for routing over EOS P2P to get
    NAT traversal and relay fallback. An unavailable subsystem now fails with a message naming what
    was requested, listing what is registered, and explaining the usual cause, rather than
    "...is not available." It deliberately does not fall back to platform UDP: a silent fall back
    would ship a project that believes it has relay support and passes every LAN test.
    New guide: Docs/nat-traversal-and-eos.md.

Changed :repeat_single_button:

  • The pacing soak now exercises the rollback path. It previously sent, flushed and ticked, and
    never called ApplyBufferedInputsToState or RollbackToFrame - so the rollbacks=0 /
    rejected=0 it reported were zero by construction, not by measurement. Each peer now applies
    the other’s real input and rolls back on disagreement (~1190 corrections per 20 s profile), and
    the suite asserts the path was genuinely driven so it cannot silently regress to the old
    behaviour.
  • The soak’s two peers no longer share a clock rate. FLatencyProfile::ClockDriftPpm scales one
    peer’s delta, reproducing the drift between two machines’ crystals that pacing exists to absorb -
    the one property two in-process peers otherwise cannot have. Values are deliberately accelerated
    (real crystals differ by ~200 ppm, which is a quarter of a frame over 20 s and would exercise
    nothing). New RCPSoak.Latency.ClockDrift profile runs a 1% rate difference.
  • The soak audits resimulation determinism at the end of each profile and fails on a violation.

Fixed

  • RollbackNetworkCommands.cpp compiles standalone. It opened #if !UE_BUILD_SHIPPING before
    including anything that defines it; under a unity build a neighbouring file had already pulled in
    Misc/Build.h, so it only broke when compiled on its own - a C4668 under warnings-as-errors and
    a routine marketplace rejection. Surfaced by switching Make-FabBuild.ps1 to
    BuildPlugin -StrictIncludes; -Rocket and -installed are not BuildPlugin parameters and had
    been silently ignored for two releases.

Fixed (cross-build determinism)

  • The simulation was not bit-reproducible across build configurations. Unreal compiles with
    fast-math by default, which permits the compiler to reassociate float expressions and to contract
    a*b+c into an FMA with a single rounding. Neither module set FPSemantics, so both inherited it.
    A DebugGame and a Development build of identical source, fed bit-identical input, diverged
    at frame 3 of a 240-frame trace with 57 of 240 frames differing. Two players on differently-built
    clients would desync with nothing wrong in the gameplay code — and since Win64, Mac, Linux,
    Android and iOS are all declared, cross-platform play means different compilers and architectures
    by definition.

    Found by the new Tools\Diff-BuildDeterminism.ps1. Fixed by setting
    FPSemantics = FPSemanticsMode.Precise on RollbackCorePro and RollbackVikingGame, after
    which DebugGame and Development produce identical streams over 600 frames on seeds 1, 2, 7 and 99.

    This carries no measurable performance cost. An earlier draft of this entry reported that
    Precise took RCPSoak.Latency.Transcontinental (180 ms one-way, 3% loss, depth 20 — the
    heaviest profile) from 59.95 Hz to 32.44 Hz with 45.87% stalls, and called the trade-off
    unsettled. That was a measurement error: the comparison was run once per arm against a soak
    profile that was failing intermittently for an unrelated reason. Re-measured over three
    full-suite runs per arm, fast-math produces the same failures at the same magnitudes, and the
    fixed-timestep clock never drops a catch-up frame in either arm — the frame-rate collapse was
    pacing withholding frames, not arithmetic cost. The real cause was a test-harness setting and is
    fixed below.

Added (differential build testing)

  • Tools\Diff-BuildDeterminism.ps1 runs the same simulation in two build configurations and
    compares per-frame state checksums. This is how the defect above was found, and it closes a gap
    that no amount of single-configuration testing could: environmental nondeterminism (optimisation
    level, reassociation, FMA contraction, a differing math library) is invisible to a suite that
    holds all of those constant.
  • URollbackManager::RunDeterminismTrace is the engine behind it: a fixed, self-contained
    simulation with no wall clock, no network and no randomness beyond a seeded integer input
    pattern. Deliberately not gated behind WITH_DEV_AUTOMATION_TESTS or !UE_BUILD_SHIPPING,
    because a comparison that cannot include the configuration you ship cannot speak about it;
    -RollbackDeterminismTrace=<file> emits the stream from any build, Shipping included.

Fixed (Linux build)

Cross-compiling for Linux with Clang 20 surfaced two defects immediately, both of which MSVC accepts silently:

  • A range-for over FJsonObject::Values bound a reference to a temporary.
    RollbackVikingFighter.cpp declared the loop variable as
    const TPair<FString, TSharedPtr<FJsonValue>>&, but that map is not keyed by FString on
    every engine version — UE 5.8 keys it by UE::TSharedString<char16_t>. MSVC silently
    constructed a temporary per entry (a copy nobody asked for); Clang rejects it outright under
    -Wrange-loop-construct. Now const auto&, which is correct on every version.
  • URollbackVikingGameSubsystem outscoped its own dependency. Its Initialize calls
    InitializeDependency<URollbackNetSubsystem>(), but it declared no DoesSupportWorldType
    while the net subsystem is Game/PIE only. In editor, inactive and preview worlds — all of
    which a cook creates — it was constructed without its dependency and UE ensured with “Failed
    a call to GetSubsystem from within Initialize of another subsystem”. A handled ensure fails a
    cook outright, which is how the first Linux cook died. It now declares the same world types.
    Regression test: RollbackVikingGame.Subsystem.MatchesDependencyWorldTypes.

With both fixed, Linux cooks and runs, and its determinism trace is identical to the
Win64/MSVC trace of the same engine over 600 frames.

Fixed (soak harness)

  • The latency soak was connecting with a quarter of the shipped input redundancy.
    ConnectLoopbackPeers set InputRedundancyFrames = 2 against a shipped default of 8. Input
    packets are unreliable and are never resent, so a frame’s only redundancy is the copies riding in
    the next N packets — 3 delivery chances instead of 9. At the Transcontinental profile’s ~5.9%
    end-to-end loss that is roughly 0.5 unrecoverable input holes per 20 s run, and it failed that
    profile on about 4 runs in 7. Now 8, after which 8 consecutive full-suite runs passed with max
    frame advantage 12–16 against a bound of 20 and no pacing stalls at all.

Fixed (burst packet loss could hang a match permanently)

  • A burst of lost packets could freeze a session for good, and starve both players. Input
    packets are unreliable and carry InputRedundancyFrames copies of recent history, so uniform
    loss is absorbed almost perfectly — but a burst wider than that window left a hole in the
    contiguous input stream that nothing could fill. ConfirmedFrame advances only over a contiguous
    prefix, so one missing frame pinned it permanently while later input piled up unused behind it.
    Pacing derives frame advantage from confirmation, so the pin did not stay local: advantage grew
    without bound, pacing concluded the peer had gone silent, and held the clock against a peer that
    was transmitting perfectly — which suppressed the local input stream and starved that peer in
    turn. Measured on a 24-packet burst profile: confirmation frozen for 17 s of a 20 s run, peak
    frame advantage 550 against a rollback window of 20, both peers stalled, the match running at
    half rate and never recovering. A burst of 9 packets at 60 Hz is 150 ms — ordinary mobile or
    congested-wifi jitter.

    Three changes, none of which touch the wire protocol:

    • Input redundancy now adapts to what the peer says it is missing. Every peer already reports
      its own frame advantage in every packet (protocol v6), and that advantage is the size of the
      hole it is stuck behind — so a peer missing your input tells you how far back to reach,
      unasked. The send path widens its redundancy window to cover it, bounded by
      MaxInputsPerPacket. The packet format already carries an explicit input count, so this is the
      same wire format at the same protocol version: no peer needs upgrading.
    • Peer silence is now a transport fact. It asks whether anything has arrived from the peer
      recently (URollbackNetSubsystem::IsPeerSilent), rather than whether your own confirmed frame
      has stopped advancing — which is equally the symptom of one lost input frame. A peer emits
      heartbeats whether or not it is producing frames, so a live-but-unconfirmable peer is no longer
      mistaken for a dead one and held against.
    • A frame that can never arrive is now written off rather than blocking forever. After a
      bounded wait, confirmation steps over the hole and resumes, counted as
      GetTransportStats().UnrecoverableInputHoles and logged with the frame range. This is not a
      repair — that frame is a genuine divergence, and the cross-peer checksum comparison is
      expected to report it — but a detectable desync is strictly better than a session that hangs.

    Covered by a new soak profile, RCPSoak.Latency.BurstLoss, which drops 24 consecutive packets
    and failed before these changes. Peak advantage on it is now 27, stall rate 0.00%, and
    confirmation recovers in 0.44 s instead of never.

Added (burst-loss diagnostics)

  • SimulatedBurstLossLength on FRollbackTransportConfig — drops packets in consecutive runs
    rather than independently, so a test can reproduce how real links fail. 0 (the default) keeps the
    existing uniform model.
  • GetTransportStats().UnrecoverableInputHoles — input frames written off as undeliverable.
    Non-zero means the link lost a burst wider than redundancy could repair; expect a checksum
    divergence for those frames.
  • URollbackNetSubsystem::IsPeerSilent() (Blueprint-pure) — whether the transport has heard
    from the peer recently.

Please note: plugin functionality on UE 5.8.2 is problematic and not yet available. It is currently shipped for 5.5, 5.6, 5.7.