Skip to main content

Entangle vs. Mirror / FishNet / NGO / Photon Fusion - Architecture Review

Method: 11 Entangle subsystems were each mapped from the actual server + Unity code (not the docs), then benchmarked against the reference stacks, synthesized into one prioritized list, and every item was then adversarially re-verified against the code so nothing already-built or already-designed slips in. 30 items survived (0 rejected as already-done), plus 6 dimensions a completeness pass surfaced. Item grounding is file:line on this branch.

Companion doc: ENTANGLE_FRAMEWORK_IMPROVEMENT_PLAN.md (the hardening roadmap this review supersedes with a code-verified, competitor-benchmarked view). Much of that plan is now already built - execution-mode gating, two-phase join, RoomState-as-single-bootstrap, retention machinery.

Competitor snapshot (what they are today)

  • Mirror - mature, free, server-authoritative host model. Weaver code-gen (de)serializers + [Command]/[ClientRpc], SyncVars/SyncLists, snapshot interpolation, pluggable interest management, standard test runners in CI. Baseline the ecosystem measures against.
  • FishNet - the current OSS feature leader. Prediction v2 (client-side prediction + reconciliation), lag compensation (ColliderRollback), observers + per-SyncType conditions, source-gen serializers, object pooling, statistics API.
  • Unity NGO (Netcode for GameObjects) - first-party. NetworkObject/prefab hashes, NetworkVariable, network visibility, ILPP code-gen, Network Profiler + RNSM, integrates with UGS Relay/Lobby/Matchmaker.
  • Photon Fusion - Shared/Host/Server topologies, State/Input authority split, client prediction + resimulation, bit-packed delta snapshots, Areas-of-Interest. Fusion/Quantum bring deterministic authoritative ticks.
  • Backend-style (Nakama / Colyseus / UGS) - authoritative room servers, matchmaking (tickets/filters), queryable lobbies, session resume, per-match server handlers.

Where Entangle already matches or leads

Entangle is architecturally strong where it counts - this is not a weak framework playing catch-up:

  • Single-snapshot bootstrap discipline - RoomState is the ONE reality for join, rejoin, AND forced resync (JoinRoomHandler.cs:84, ReJoinRoomHandler.cs:223, RoomStateManager.ForceSyncRoomState) with no parallel presence path. Cleaner than Colyseus (schema-state + separate join/leave plumbing) and NGO (spawn state vs RPC bootstrap): Entangle collapses late-join, reconnect, and desync-recovery into one code path.
  • Transactional two-phase join/rejoin (TryBeginRoomJoin -> HandlePlayerJoin -> TryCommitRoomJoin) with explicit rollback restoring seat, spatial-hash, master assignment, InactivePlayerRecord, and original owner id on mid-join failure. Mirror/NGO/Fusion approval paths mostly just kick the connection; Entangle's mid-join-disconnect resilience is ahead of the pack.
  • Reconnect/rejoin taxonomy - complete RoomResponseCode set (RejoinTokenInvalid/IdentityMismatch/InactivePlayerNotFound/RoomExpired/RejoinWindowExpired) + full client event contract (OnReconnectStarted/Restored/RoomRejoinSucceeded/Failed). More precise than Photon's opaque rejoin, far ahead of Mirror/FishNet which leave rejoin to the app. The most mature slice of the module.
  • SnapshotInterpolator - GC-free 64-slot struct ring, cubic-Hermite using endpoint velocities as tangents (real motion curves/bounces, not chord-cutting), adaptive framerate-independent playback delay, wrapping-seq dedup - a notch above Mirror's position lerp and NGO's buffered interpolator, and it has real off-device test coverage.
  • Two-direction interest management - server-authoritative spatial-hash AoI with LOD-ring fanout AND auto-collapse to a single global group below N players (a small-room optimization none of Mirror/Fusion/NGO do automatically), plus a shipped subscription state-sync lane that gates bandwidth in both directions (server filters downstream AND tells the owner to stop sending when no subscribers exist). Upstream gating the mainstream Unity stacks can't do.
  • Per-op execution-mode gating - one centralized, greppable AllowsClientRelayedGameplay kill-switch across ~10 handlers; a per-room relay-vs-authoritative toggle none of Mirror/PUN2/NGO express as a single room-scoped switch (closer to Fusion topologies).
  • Crash-isolated plugin loader - manifest games, cached TS/V8 compilation reused across rooms, legacy Games/{Name}.dll with AssemblyResolve, every plugin callback try/caught. The TS/V8 path is a real differentiator over C#-only Mirror/NGO/Fusion; comparable to Nakama's per-match handlers.
  • Real auth abstraction - two-stage admission (transport RegisterIdentity then Entangle AuthenticateSession), normalized AuthContext, Device/ExternalWebhook/local-JWT providers by AuthMode - on par with Nakama's session model.
  • CachedEventStore - O(1) remove-by-id/tag, retention by count AND bytes, tombstone compaction, ordered replay honoring the batch budget - a persistent replayable room-event store Mirror/NGO don't ship in-box.
  • App-layer soak/integrity harness - per-sender seq + rolling FNV-1a checksum on reserved event code 250. None of Mirror/FishNet/NGO/Photon ship this in-box.

Bottom line

The gap is not features - it is hardening and operational maturity. The urgent, non-negotiable work is server-side authorization on relayed writes (today, in the default ClientRelayed mode, any authenticated peer can teleport/delete other players' entities and overwrite room/seat/group state - the client IsMine gate is cosmetic) plus per-peer rate-limiting / entity-caps to close a trivial DoS. Both are P0 and mostly follow an existing owner-checked pattern. Right behind: making the test suite actually run in CI (it doesn't today), auth hardening (unsigned webhooks, no session expiry), a client reconnect supervisor (the excellent server retention machinery is wasted because the client retries exactly once), and killing the three-way serialization duplication. The headline capability gap is matchmaking - a public EN.MatchMaker that returns nothing - which with a queryable room-property/filter model is the main thing blocking competitive/auto-match games. Long-tail parity (smallest-three rotation, source-gen [Networked], server lag-comp, client prediction) matters only if you chase twitch genres, and even Mirror/NGO skip most of it. The P0/P1 correctness + hardening + matchmaking band is the highest-leverage work across everything already built, and it matches the goal of making the stack predictable and service-ready rather than adding more features.

Prioritized improvement list

Legend: status = new / designed-not-built (a plan doc designs it, code doesn't have it) / partially-done. Effort S/M/L/XL. Every item was code-verified; where verification corrected the original claim it is noted inline.

P0 - correctness / security, do first

authz-relayed-writes - Server-side authorization on relayed gameplay writes · M · designed-not-built

Highest-severity hole in Entangle. In the default ClientRelayed mode (and Hybrid) any authenticated peer can teleport/move any entity (including other players' avatars), delete anyone's Items/Agents, overwrite room-wide state and the seat table, and move any player in/out of an AoI group. The client IsMine gate in SetPosition/SetRotation only decides whether to send - a crafted packet bypasses it. The state channel is already owner-checked, so the fix is a consistent, small change following an existing pattern: make the acting subject ALWAYS the authenticated PeerId, never a payload field.

  • Fix (five sites): (1) EntityUpdate - ProcessEntityUpdate resolves via sender-less FindPlayer/FindAgent/FindItem (RoomEntityManager.cs:186-208); pass PeerId and use the owner-checked (networkId,sender) overloads that already exist (used by HandleEntityStateChange). Covers both EntityUpdateHandler.cs:33 and the batched BatchedEntityUpdateHandler.cs:57 (used by NetworkTransformAdvanced). (2) DeleteEntity - DeleteEntityHandler.cs:36 discards PeerId, no owner/IsInRoom check. (3) RoomProperty writes unchecked (RoomEntityManager.cs:286-288) - gate on MasterClient==sender (the seat table is a RoomProperty). (4) UpdateSeat trusts client data.NetworkId (RoomSeatManager.cs:113-118) - use PeerId. (5) CustomGroup targets client EntityNetId (RoomEventManager.cs:148-175) - require it map to the sender or gate behind master.
  • vs Fusion/FishNet/NGO/Mirror all make transform writes and despawn privileged, owner/authority-gated operations.

rate-limit-entity-cap - Per-peer rate limiting + per-room entity caps · M · new

Zero application-level flood protection on any Entangle handler. A single authenticated peer can spam InstantiateEntity - each call adds a NetItem/NetAgent to the unbounded room lists and reliably broadcasts to the whole room - a trivial memory-exhaustion / broadcast-fanout DoS. Unknown entityIds spawn phantom server entities that leak (the "prefab not registered" log is client-side only; the server never validates the id).

  • Fix: lightweight per-peer token bucket keyed by (PeerId, OperationCode) at the top of each gameplay handler; a hard per-room MAX_ENTITIES cap + per-peer spawn cap in HandleEntityInstantiationRequest (RoomEntityManager.cs:88-113); a prefab-id allow-list rejecting unknown entityIds before allocation. Config in ServerConfig.yaml.
  • Verification note: the token bucket must be written new - FnVoice has no rate limiting and Persistence uses ASP.NET FixedWindow HTTP middleware, neither reusable inside the RUDP dispatch. (GenerateEntityId decrements from uint.MaxValue, but the unbounded-list leak holds regardless.)
  • vs Nakama/Photon/PlayFab rate-limit per socket; NGO/Fusion/Mirror treat the prefab registry as an authoritative allow-list.

tests-vstest-ci - Convert EntangleServer.Tests to a real VSTest project + run in CI · S · new

EntangleServer.Tests is OutputType=Exe with a hand-rolled reflection [Test] runner (Program.cs), so dotnet test can't discover it and CI runs zero tests (both workflows are build/publish-only). Convert to xUnit (Entangle.Tests already is - copy it), add a dotnet test step to build.yml. Cheap, one-time, and the enabling foundation for every other test item.

  • vs Mirror/FishNet run standard test runners in CI as a baseline.

auth-hardening - Sign webhooks, enforce session expiry, wire duplicate-login policy · M · designed-not-built

Four items, three confirmed real:

  • (1) Webhook signing - ExternalWebhookAuthService.cs:60-68 POSTs with no signature/HMAC/retry/backoff/breaker; Configuration.ExternalAuthSharedSecret exists but is never read. Anyone reachable can forge validation requests. (The newer PersistenceAuthService path does HMAC-sign; the legacy webhook fallback doesn't.) Add an HMAC header over body+timestamp, bounded retry, circuit breaker.
  • (2) Session expiry - AuthContext has 6 fields, no expiry; no create/join/rejoin path re-checks it, so a revoked token keeps full access for the connection's life. Add ExpiresAt/IssuedAt/AuthProvider/Claims and check on Create/Join/ReJoin.
  • (4) Duplicate-login toggle - the doc-documented AllowDuplicateDeviceId (+ TakeoverOldest/RejectNew) is unwired: GetActiveIdentityCount is never called, DuplicateActiveIdentity never returned, AuthenticateSessionAsync unconditionally takes over oldest. Also validate DisplayName length/charset.
  • Verification corrected sub-claim (3): the 20 hardcoded keys at FNELobby.cs:66-87 are dead code - real validation uses SecretKey from external AppKeys.xml. So this is "delete dead code," not "committed secrets grant access." The config/secret-store mechanism already exists.

P1 - high leverage: hardening structure + the headline capability gaps

matchmaking-mvp - Real matchmaking (JoinOrCreateRandom) + kill the phantom stub · L · new

MatchMaker is a comment-only empty stub on both sides yet is publicly instantiated as EN.MatchMaker - a phantom capability worse than an absent one (integrators wire against it, get silence). What Entangle calls "matchmaking" is manual browse-then-join. Delete/[Obsolete] the stub, then build JoinOrCreateRandom(query, roomConfig) reusing the two-phase join machinery (FNELobby.cs:619-666) + least-loaded placement. Design the query DTO now; defer skill/ELO/tickets.

  • vs Photon JoinRandomRoom+filters, UGS Matchmaker, Nakama tickets, Colyseus matchMaker.

room-property-bag-and-query - Queryable custom room properties + server-side list filtering · L · new

RoomQuery is a 2-value enum {All, Avaliable}; GetRoomsList filters server-side only by AppId. Any real lobby UI must download the full list and filter client-side (doesn't scale past a few hundred rooms). Add a Dictionary<string,string> Properties bag + lobbyKeys (Photon's model) to CreateRoomData/RoomInfoData/DiscoveryRoomInfo, and replace the enum with a RoomQuery DTO (nameContains, gameName, mode, property predicates, sortBy, paging) applied server-side in both GetRoomsList and discovery. Unlocks filtered browse and matchmaker criteria - highest-leverage lobby change after killing the stub. (NetRoomProperty is a runtime state channel, not lobby metadata.)

  • vs Photon custom room properties + SQL lobby, UGS QueryFilter, Steam SetLobbyData.

globally-unique-room-ids - Globally-unique ids + id-based cross-node resolution · M · new

Correctness bug in clustered deployments. Room ids are a per-process counter (each node emits 1,2,3...), and cross-node resolve keys on the non-unique room name (FNELoadBalancer.cs:495 FirstOrDefault(r.RoomName==id)); even the id lookup is ambiguous across nodes and degrades to name matching. Two nodes with same-named rooms leads to a player being routed to the wrong node/room. Make ids globally unique (node-prefix / snowflake / GUID), resolve strictly by id, keep name display-only. Note: RoomId is uint on the wire, so this needs a hand-port to the Unity mirror + both EntangleServer and FnVoiceServer lobbies. Single-node is unaffected (falls back to least-loaded).

  • vs Photon/UGS/Nakama/Colyseus all resolve by a globally-unique match id.

authority-resolver - Centralize authority as one AuthorityResolver / capability matrix · L · designed-not-built

The structural home for the P0 point-fixes. The mode-gate is centralized; the finer owner/master/subject checks are scattered - some check owner, some master, many nothing - which is exactly how the P0 holes crept in. Introduce one AuthorityResolver.CanActor(peerId, op, entity) -> {allow, reason} every handler calls, encoding {owner-required | master-required | subject==peer | mode-gate} in one place, plus a master-only guard on RoomStateChange (any player can currently lock/unlock the room) and a structured rejection reason on every deny (none exists today, so optimistic clients can't reconcile deterministically). Subsumes the narrower per-domain helpers proposed in the presenter/custom-group review.

  • vs Fusion (State/Input authority), NGO (write-permission enums) treat authority as one uniform concept.

movement-plausibility - Speed/teleport/rate validation on the authoritative write path · M · new

Distinct from authz-relayed-writes: that fixes who may write a transform; nothing validates what they write. RoomEntityManager.ProcessEntityUpdate does TransformLite.CopyFrom(...) and immediately relays with zero sanity checks - a grep for maxSpeed|teleport|distance.*valid|sanity finds nothing. In Authoritative/Hybrid the server is supposed to be trustworthy yet accepts client positions verbatim, so even an authorized owner can teleport/speed-hack/fly. Add a per-entity max-delta/speed clamp with configurable bounds + rejected-snap-back - the standard competitor anti-cheat primitive (Fusion, Mirror KCC).

regression-suites-core-paths - Tests for AOI, CachedEventStore, reconcile, subscription, authority · L · designed-not-built

Server tests cover only auth-identity, rejoin-evaluator, room-expiry, and the soak validator. The parts most likely to break subtly are untested: the ~270-line stateful CachedEventStore (eviction by count+bytes, tombstone compaction with an off-by-one-prone threshold - RoomEventManager.cs:275-546, entirely untested), AOI cell routing, spawn/remove diff reconciliation + forced resync (idempotency/ordering invariants), subscription sync, presenter/custom-group routing, master-migration + join-rollback + rejoin-restore, and the authority gates. Build a simulated-peers fixture; add a no-alloc guard on the steady-state relay path. Depends on tests-vstest-ci.

reconnect-supervisor - Client reconnect with backoff, jitter, attempt cap · M · partially-done

The server retention machinery is the module's most mature slice, but the client only knocks once: ReconnectAndJoin() fires a single-shot on PeerStatus.Timeout with no retry/backoff/jitter/cap - and when the LiteNetLib transport exhausts its handshake retries it surfaces as Disconnected, not Timeout, so the trigger never even re-fires. A single failed attempt strands the client while the inactive record is still valid for the whole retention window (and no jitter means a thundering-herd reconnect after a hiccup). Add a supervisor that retries Connect() on exponential backoff + jitter, bounded by max attempts or InactivePlayerRetentionMs, surfacing OnReconnectAttempt. Keep the single-shot as attempt #1. (The reconnect-vs-rejoin separation and callbacks are already built; backoff/jitter/cap is genuinely undesigned.)

  • vs FishNet ships a reconnect helper; Nakama SDKs retry with backoff.

reconnect-ownership-continuity - Preserve entity ownership + NetworkId across rejoin · L · new

Most likely user-visible reconnect defect. On disconnect, ReassignEntityOwnershipOnPlayerExit transfers all a leaver's items/agents to the master; on rejoin the player gets a new NetworkId (= new peerId) and never re-claims them - a reconnecting player loses a held item, or a formerly-owned entity is now someone else's - plus anything keyed on the old NetworkId (scores, teams, references) silently breaks. Record owned entity ids (or a stable owner-identity tag) in InactivePlayerRecord; on rejoin re-grant ownership of entities still tagged to that identity and unclaimed, and either preserve the original NetworkId (the record already carries PreviousPeerId) or broadcast PlayerIdRemapped(old,new) and re-key. Two-sided protocol change (server + hand-duplicated Unity mirror).

  • vs Photon's inactive-actor model preserves ActorNumber + owned buffered instantiations precisely for this.

ccu-load-harness - CCU/bot load generator + per-room tick-budget scheduler · L · new

FNELobby.Process ticks every room sequentially on one thread; one heavy room (large AOI grid, many entities, slow RoomLogic.OnTick) stalls the tick for all rooms on the node, with no per-room time budget or overrun metric. There is no headless CCU/bot load generator for Entangle rooms (only Persistence loadtest + the app-layer SoakProbe), so every scaling claim is unfalsifiable. Build a repeatable CCU harness and a fairness scheduler / worker partitioning so the single Process loop stops being the whole-node ceiling. Prerequisite for all scaling work below.

P2 - operational maturity, scaling, and correctness cleanups

shared-serialization-contract - One source of truth for the wire contract (CI golden-hash guard) · L · new

Divergences exist but the scary ones are latent, not actively shipping. Every model is hand-duplicated across EntangleCommon, Unity EntangleModels.cs, and FnVoiceCommon, and they've drifted - but: EntityType.Spectator=4 is a dead enum value (never produced), and ENTITY_STATE_BATCH_SIZE (960 vs 1024) is a sender-side packing heuristic the receiver never reads, so neither is a live wire break. The genuinely valuable, non-conflicting deliverable is a build-time golden-hash CI test that fails when the 3+ OperationCode enums / TransformLite layout / batch constants diverge. (Merging EntangleCommon+FnVoiceCommon is out of scope - it is an explicit non-goal of the shared-server plan.)

  • vs the exact bug class Mirror (Weaver) / FishNet (source-gen) / NGO (ILPP) eliminate.

seq-wraparound-and-magic21 - Wraparound-safe sequence compare + server stale-drop + delete dead code · M · new

Four correctness cleanups (3 solidly real):

  • (1) SequenceNumber is a ushort compared with raw > plus a duplicated magic >=21 warmup across 3+ sites. On a pure-unreliable path the wrap at 65535 (~18 min @60Hz continuous dirty) makes LastReceivedSequenceNumber permanently exceed small incoming seqs, dropping valid updates until a Reliable delivery resets it (a multi-second-plus freeze on long-lived busy entities). Replace with one RFC1982-style wraparound-safe compare; drop the magic-21.
  • (2) Server TransformLite.CopyFrom has no seq guard (the Unity side does, and the internals doc documents it inverted), so a stale unreliable sample can overwrite fresher and fan out to the whole AoI group. Add the guard; reconcile the divergent CopyFromSilently semantics.
  • (3) Delete the dead + broken ERingBuffer (all 3 copies) whose test asserts its buggy overwrite as correct.
  • (4) A 1-byte wire schema version on the two hand-packed layouts is defense-in-depth, not an uncovered gap - a module-SemVer handshake at RegisterIdentity already gates breaking layout changes if the version is bumped.

presenter-group-presence - Replicate presenter/group as authoritative presence + fix acks · M · designed-not-built

Still unbuilt. (1) SetPresenterModeHandler.cs:32-34 discards the bool from OnPresenterModeChange and echoes success even when rejected (client believes it became a presenter). (2) IsPresenter/GroupId are never in the RoomSnapshot and custom-group success echoes only to the requester, so remote clients and late joiners are blind; add EntityPresence fields + dedicated result payloads. (3) Custom-group remove trusts the client GroupId while resetting player.GroupId=0, leaving the player still in the real AOIGroup (persistent routing desync) - use the server's authoritative player.GroupId. (Fires only when an app actively drives presenter/groups, hence P2 despite being real bugs.)

  • vs FishNet/Fusion/NGO replicate role/interest-relevant state to all observers.

per-key-subscription - Ship per-key subscription granularity (finish StateKeyMask) · L · partially-done

The subscription feature is built end-to-end at lane granularity (ops, serializer, handler, manager, snapshot-on-subscribe, AOI intersection, owner publish gating, full client API). What's missing is the per-key refinement - the promise "subscribe to key 2 = face but not key 5" is unreachable, so a subscriber wanting one high-cost field receives the entire lane. Ship the designed StateKeyMask: add KeyMask (+ DeliveryMethod, RequestSnapshot) to the op, store per-subscriber masks, compute per-publisher ActivePublishMask as the union, gate individual keys in GetDelta, filter downstream per-subscriber. Also gate the per-packet SubscriptionLaneMask so entities with no subscription keys don't pay ~1-5 B on every unreliable delta.

  • vs FishNet per-SyncType observer conditions, Fusion per-property interest.

pool-leak-mixed-lane - Fix the pooled-state leak on the mixed broadcast+subscription path · S · new

Real leak, but the naive fix is unsafe (a double-free). When an entity dirties a Mandatory AND a Subscription key in the same unreliable tick, RouteUnreliableEntityState rents a broadcast state via CreateFilteredStateFromDelta and enqueues it with ReleaseAfterSend=false, so the pooled instance is never returned - the pool silently allocates fresh under load (the GC-hitch the whole delta design exists to avoid). Do not just switch to EnqueueOwnedState: OnEntityDataArrived fans the same reference into the cell + 8 neighbours + LOD cells + global (+ custom group), and per-queue owned release would return the one instance N times, causing multi-free/aliasing corruption. Correct fix: give the split-off fragment a single-owner lifetime that survives N enqueues (release inline after fan-out, or ref-count by enqueue count, or per-cell copy). Add a no-alloc guard test.

room-durability - Server-side room-state snapshot + rehydrate-on-restart · M · new

Entangle rooms are pure in-memory: a process crash or redeploy vaporizes every live room, its entities, RoomProperties, seats, and master assignment, and no reconnect path restores them (the rejoin work only covers re-joining an existing room). Persistence integration exists but is scoped to auth/profile/storage only. Add a periodic room-snapshot to the persistence layer + rehydrate-on-boot - high-value durability that directly leverages the persistence subsystem already built.

bandwidth-scaling - Delta-since-baseline transform encoding + server send-rate throttling · M · new

TransformLite always serializes a full quantized snapshot every send (no per-field delta-vs-last-sent beyond the static SyncFlags), and client send rate is unbounded, so downstream bytes-per-viewer grow O(entities-in-AOI x wire-size) with no server-side cap or CCU-adaptive downscaling. Smallest-three rotation packing shaves a few bytes; this addresses the actual high-CCU levers: per-field delta-since-baseline, server-driven adaptive send rate, importance-based rate scaling. The difference between ~30 and ~300 CCU per room on constrained clients.

webgl-il2cpp-conformance - Cross-platform gameplay conformance matrix + AOT guard · M · new

WebGL can't use the RUDP/UDP data plane (no raw UDP in browser), so WebGL gameplay must fall back to WS/WSS - but nothing validates that the gameplay path, unreliable-lane semantics, and AOI relay behave over WebSocket, and SecureSessionTransportPolicy restricts secure session to Datagram only, so WS gameplay is unencrypted at the app layer. IL2CPP stripping has already broken this stack once (Quest/Android IL2CPP at High stripping can remove Resources/reflection-only networked types). Add a cross-platform conformance matrix + link.xml/AOT guard for Entangle networked payloads.

per-room-metrics - Collect and surface per-room / per-system metrics · M · designed-not-built

Admin room endpoints expose only lobby fields (PlayerCount/Mode/Name) - operators can't tell which room is hot, thrashing AOI, or growing snapshots. Cheap first win: pipe the already-computed RoomEventManager counters (CachedEventCount/Bytes/CacheEvictions/LastReplayBatchCount - currently produced and silently dropped) into MapRoomDynamic. Then a per-room metrics struct sampled once per StatsCollector tick (active/inactive players, entities-by-type, snapshot size, state-updates/sec, subscription-group count, AOI rebuild/relay-batch counts) + a per-opcode/channel traffic breakdown for bandwidth attribution.

  • vs Photon dashboard, Mirror NetworkProfiler, NGO metrics.

aoi-tick-budget-timing - Fix the AOI per-tick time budget + add hot-path timing · M · designed-not-built

AOIGroup/AOIRealiableGroup gate drain loops on Environment.TickCount - startTime < 6, but TickCount has ~15.6 ms resolution on Windows, so the "6 ms" budget is effectively 0-or-15 ms - it drains the whole queue then breaks at a scheduler boundary. Each active cell gets its own budget three times with no global ceiling, and deltaTime is unused, so total tick time scales with cell count unbounded (a scalability cliff). Replace with Stopwatch.GetTimestamp(), enforce a global per-tick budget across cells, and add cheap EMA timing around Room.Tick, the AOI loops, and FNELobby.Process (there is zero timing today, so perf work is guesswork). A near-duplicate exists in FnVoiceServer, so plan a parallel port.

transform-component-consolidation - Archive the legacy PID NetworkTransform + NetworkRigidBody · M · designed-not-built

Three transform components ship active side-by-side: legacy PID NetworkTransform (per-frame PID, no buffer, no velocity channel - strictly below baseline), NetworkRigidBody with its own parallel RigidBodyData wire type (auto-registered as a MemoryPack union), and the intended-unified NetworkTransformAdvanced (whose header says it "replaces both"). A TransformLite protocol edit can silently diverge from RigidBodyData - the hand-porting hazard, multiplied. Move the two legacy components + RigidBodyData wire/formatter into _Archive/, add an editor warning + migration note, and delete the fully-commented-out NetworkTransformDebugVisualizer. Do this before any further transform work. (No prefab/scene references any of the three, so risk is a user manually adding the legacy component; mirror the archive to the package + FigNetXR.)

latency-aware-interpolation - Make interpolation delay latency-aware + fix the doc mismatch · S · designed-not-built

ComputeTargetDelay = meanGap*DelayTicks + jitter*JitterMultiplier (SnapshotInterpolator.cs:259-263) has no RTT term, yet the internals doc promises target = oneWayLatency + jitter + bufferSlack - the unified interpolator dropped the latency term the archived components had. The RTT source exists (EN.Socket.Ping). Wire half-Ping in as an additive floor (buffer-sizing to prevent dry-starvation on high-RTT jittery links), clamp as today, expose a fast-LAN opt-out, and fix the internals doc. A few lines; closes the largest doc-vs-behavior gap.

  • vs Mirror factors measured RTT/jitter into snapshot interpolation.

lb-placement-and-freshness - App/capacity/region-aware placement + event-driven cluster freshness · M · partially-done

Two discovery fixes. (1) Placement - the fallback is nodes.OrderBy(ActiveUsers).First(Online) with no term for whether the target app runs there, room headroom, or region; resolve() never passes the validated app in, so a client can be routed to a node not running the requested game. Region is scaffolded-but-dead (ClusterNodeInfo.Region surfaced/copied but BuildLocalSnapshot never sets it; no Configuration.Region). Make placement app-scoped first, add capacity, wire Region end-to-end. (2) Freshness - Utils.OnRoomCreated/OnRoomDestroyed fire but have zero subscribers; the LB dirty flag is set only on peer connect/disconnect + a 5 s heartbeat, so a new/deleted room is stale for up to ~5 s and resolve() can route to a deleted room. Subscribe the LB and call the already-thread-safe MarkDirty() - a tiny wire-up.

  • vs UGS/Photon do capacity+region placement; Photon/Colyseus propagate room create/destroy near-instantly.

inactive-player-prune-tick - Periodically prune expired inactive-player records · S · designed-not-built

IsInactivePlayerExpired runs only lazily inside FindInactivePlayer during a rejoin attempt; FNERoom.Tick never prunes. In a long-lived high-churn room where disconnected players never rejoin (common - users close the app), InactivePlayerRecords (each pinning a full NetPlayer) accumulate until room disposal - a slow leak. Add a low-frequency prune pass in Tick reusing IsInactivePlayerExpired.

shared-room-lobby-runtime - Extract shared Lobby/Room runtime (de-dup Entangle vs FnVoice) · XL · designed-not-built

Entangle and FnVoice ship forked ~600-LOC FNELobby/FNERoom near-duplicates that have already drifted (for example GetRoomsList: Entangle uses a foreach+Info-log while FnVoice uses LINQ+Error-log; the Entangle RoomNetworkManager has null-guards + ClearPeer + ContainsPeer the FnVoice copy lacks). Building a shared LobbyRuntime/RoomRuntime + product-adapter + capability-flags layer would turn every subsequent lobby fix (room-property bag, RoomQuery, matchmaker, unique ids) into a single-commit change instead of a two-commit hand-port.

  • vs N/A (internal maintainability - the repo's central "duplicated not shared" hazard between Entangle and FnVoice).

entangle-usage-docs-and-samples - Canonical usage docs + runnable per-mode/reconnect samples · L · designed-not-built

No developer-facing usage/quickstart/API doc existed prior to this documentation set covering: connect->auth->join->rejoin sequence, the execution-mode behavior table, the [Networked]/key/OnChange pattern, room-event vs cached-event categories + anti-patterns, an ownership/authority guide, a reject-code reference. Runnable samples are also needed: a ClientRelayed quickstart needing no server game module (the flagship EntangleGameListener targets "LiarsBar" with no server module in the repo, so the headline sample can't run authoritatively), and a reconnect/rejoin sample exercising the already-built events. It should also be documented which IEntangleRoomListener a game implements (the sample listener only logs - copying it yields zero spawns, no error).

send-api-failure-contract - One predictable failure contract on gameplay send APIs · S · new

Fire-and-forget APIs have three incompatible failure styles: Instantiate/Delete silently no-op when not connected/in-game (worst first-run DX - calling Instantiate before joining gives zero diagnostic); Room.SendEvent throws a raw Exception on the caller frame; SendCreateOrJoinGroup logs a warn. Adopt one rule: every gameplay send that can no-op logs a single actionable warn naming the failed precondition and never throws raw across the API boundary. Small warts: migrate the preserved .Sucess typo (~8 sites) to .Success + [Obsolete] it; delete the leftover TEMP DIAGNOSTIC Awake log.

P3 - long-tail parity (only if the genre demands it)

networked-codegen - Source generator for [Networked] state + typed OnChange + auto-keys · XL · new

[Networked] is pure runtime reflection: fields walked reflectively, callback resolved by string name, every field needs a hand-picked byte Key that throws at runtime on collision (also a known IL2CPP/AOT stripping failure mode). Add a Roslyn generator emitting registration + a strongly-typed OnChange partial + auto-assigned collision-checked keys. Even a partial generator (compile-time callback binding + key auto-assign) removes the two worst footguns. Pair with an author-time "Networked Properties" list + duplicate-key error box in EntangleViewEditor. (Call sites already use nameof, so the remaining compile-time gaps are callback signature/owning-type binding and key auto-assign.)

  • vs the biggest ergonomics gap vs FishNet [SyncVar] / Fusion [Networked] / Mirror Weaver / NGO ILPP.

rotation-smallest-three - Smallest-three quaternion packing + renormalize-on-read · M · new

Rotation is 4 independent shorts @1000 = 8 B (the single largest fixed transform cost) and per-component rounding denormalizes the quaternion with no renormalize on read. Add a smallest-three mode (2-bit largest-component index + 3x~10-bit is about 4 B) selected via a free SyncFlags bit (bits 11-15 unused) so the 8 B mode stays wire-compatible, and renormalize on read regardless. (The jitter benefit is minor since the client applies rotation via PID/interp not raw Slerp, and rotation is 8 B of a ~12-20 B transform - but the bandwidth win is real. Hand-duplicated across 4 copies, so effort is M.)

  • vs Fusion (bit-packed), FishNet (compressed rotation) do this as standard.

client-prediction-honest-positioning - Position Authoritative/Hybrid honestly re: prediction · XL · new

There is no client-side prediction/reconciliation for the local player anywhere - the model is pure client-authoritative relay (owner in the present, proxies in the past); Authoritative rejects client ops but offers no predict-and-correct loop. Authoritative/Hybrid v1 is honestly best suited to turn-based/deterministic-command genres (card/board/social) and out-of-scope for twitch FPS/racing. If action genres become a goal, a single opt-in ServerAuthoritativeMover with input+tick -> server replay -> client rollback would be the long-term scope. Lowest-leverage architectural gap (Mirror and NGO also lack CSP).

  • vs Fusion/FishNet/Quantum ship CSP.

server-lag-comp-rewind - Optional server-side position/rotation rewind for hit validation · L · new

The server relays transform bytes opaquely and keeps no historical hitbox buffer, so "was the target here when the shooter fired?" is unanswerable - hit-reg is client-reported or ~RTT-late. Since the server already deserializes Position for AoI, add an opt-in per-room ring of {tick, entityId, position, rotation} + a RewindTo(clientTick) query behind a hit-validation RPC - position/rotation only, no deterministic sim. Only relevant if competitive/shooter genres are a target (the stack is currently VR/co-op/tabletop-oriented). NetworkRigidBody has client-side rewind stubs only.

  • vs FishNet ships ColliderRollback; Fusion/Quantum have full rollback.

determinism-server-tick - Authoritative simulation tick/clock + fixed timestep + seeded RNG · L · new

Authoritative/Hybrid rooms run RoomLogic.OnTick(deltaTime) on wall-clock SYNC_RATE with no server-tick number, no fixed-timestep guarantee, and no deterministic RNG/seed surface - which blocks any lockstep, replay, or server-reconciled prediction game (RTS, fighting, physics). Scoping an explicit deterministic authoritative tick as a capability (rather than leaving deltaTime-driven OnTick as the only server-sim contract) pairs naturally with client-prediction-honest-positioning and server-lag-comp-rewind.

soak-counters-to-admin - Surface soak/integrity counters to the admin plane · S · new

ReliableIntegrityValidator's counters (SenderCount/TotalOk/Gaps/Duplicates/ChecksumMismatches/Failures) are read only by the silent reporter + tests; ServerStatsSnapshot has no soak block, so a healthy multi-day soak shows silence-until-Error. Wire a soak block into the stats snapshot / an admin endpoint. (A related idea, gap-aware rejoin via the code-250 soak sequence, doesn't hold up: code 250 is the soak probe's reserved code, intentionally never relayed/cached; regular events key on UniqueId and rejoin already replays the full active cache, so there is no general per-event sequence to delta against.)

  1. Security/correctness band (P0): authz-relayed-writes -> rate-limit-entity-cap -> auth-hardening -> tests-vstest-ci (do the CI/test conversion in parallel; it gates the regression suites).
  2. Structure + headline capability (P1): authority-resolver (the structural home for #1) -> matchmaking-mvp + room-property-bag-and-query (the capability gap) -> globally-unique-room-ids -> reconnect-supervisor + reconnect-ownership-continuity -> movement-plausibility -> regression-suites-core-paths. If a lobby feature batch is imminent, do shared-room-lobby-runtime before the lobby items.
  3. Scaling + observability (P2): ccu-load-harness (makes the rest falsifiable) -> aoi-tick-budget-timing + per-room-metrics -> bandwidth-scaling -> transform-component-consolidation (before further transform work) -> the correctness cleanups (seq-wraparound-and-magic21, pool-leak-mixed-lane, presenter-group-presence, inactive-player-prune-tick) -> room-durability, webgl-il2cpp-conformance -> docs.
  4. Long-tail (P3): only as the target genre demands - networked-codegen for DX, then client-prediction-honest-positioning/server-lag-comp-rewind/determinism-server-tick as a bundle if twitch/competitive becomes a goal, rotation-smallest-three whenever bandwidth is the pressing constraint.

Notes

  • pool-leak-mixed-lane: the obvious fix (EnqueueOwnedState) causes a double-free because of the AOI fan-out - needs the single-owner-survives-N-enqueues design before implementing.
  • shared-serialization-contract: scope to the CI golden-hash guard, not merging the Common libs (an explicit non-goal in the shared-server plan).
  • auth-hardening (3): it's dead-code deletion (AppKeys.xml already supersedes the hardcoded dict), not a live-secret leak.
  • Several fixes (globally-unique-room-ids, seq-wraparound-and-magic21, transform-component-consolidation, rotation-smallest-three, reconnect-ownership-continuity) touch the hand-duplicated wire/Unity code, so each needs a port to the Unity mirror + FnVoiceServer + the package per the repo's duplicated-not-shared rule.
  • Method footnote: benchmarks were drawn from framework knowledge as of the analysis; feature specifics for Fusion/FishNet evolve, so treat competitor claims as directional and the Entangle grounding as exact.