Skip to content

Replication: authority delegation (cell-based elected syncer)#227

Open
Segfaultd wants to merge 3 commits into
developfrom
feature/authority-delegation
Open

Replication: authority delegation (cell-based elected syncer)#227
Segfaultd wants to merge 3 commits into
developfrom
feature/authority-delegation

Conversation

@Segfaultd

@Segfaultd Segfaultd commented Jul 7, 2026

Copy link
Copy Markdown
Member

What

Adds server-side authority delegation to the replication layer: a non-owned, server-authoritative entity can have its per-frame simulation handed to a nearby client (the syncer), which streams state up while the server relays to everyone else. This is the classic server-elected syncer model, generalized onto the existing ownerGUID authority — the same primitive vehicles already use to delegate to a driver, now driven by proximity election instead of a single hard-coded trigger.

Reusable Framework primitive (any mod benefits); no MafiaNet changes.

How

  • Delegation trait on NetworkEntity (server-only, not replicated): opt-in delegatable flag, OrphanMode (Freeze / Destroy), and pinnedOwner for a scripted/persistent override.
  • OnOwnershipChanged(bool nowOwner) — client seam to spin up / tear down the local simulation. Fires after construction so the replicated pose is populated before a seeding handler reads it. All client owner changes now funnel through SetOwnerFromServer, so the callback can't be bypassed.
  • CanDelegateTo(guid) — game veto (role / line-of-sight) layered on top of proximity + dimension.
  • ElectOwner() (delegation_policy.h, header-only, networking-free): acquire/drop-range hysteresis, least-loaded with nearest tiebreak, optional load soft cap, returns UNASSIGNED when nobody qualifies.
  • ReplicationManager::RunDelegation() — cadence-gated glue; builds candidates from the viewer map, honors pinned owners, applies via SetOwner, defers orphan-destroys past the iteration. Called from NetworkPeer::Update after RebuildInterest. Set/GetDelegationParams exposed for the integration/config layer.

Behaviour / compatibility

Purely additive and opt-in — entities leave delegatable false and are untouched by election, so existing replication behaviour is unchanged. Orphan-on-disconnect already returns owned entities to the server via OnClosedConnection; the election pass re-delegates on the next tick.

Tests

code/tests/modules/delegation_ut.h covers ElectOwner in isolation: nearest pick, load balancing, acquire/drop hysteresis, re-election on drift, orphan fallback, dimension/veto exclusion, load soft cap, and Z-up ground-plane distance. Run with cmake --build build --target RunFrameworkTests.


Next steps (not in this PR)

Consuming side (per-game, e.g. M2O)

This PR ships only the mechanism; a game still has to opt in and implement the local-sim seam. On the consuming side, RunDelegation already runs automatically each tick — the game only needs to:

  1. Define a delegatable entity type, register it, and spawn it server-owned (delegatable = true).
  2. Implement the client seam: OnOwnershipChanged(true) spawns/adopts and drives the local game object; OnOwnershipChanged(false) tears it down.
  3. Carry velocity in OnSerializeConstruction/transform so the handoff doesn't stutter, and set game-unit DelegationParams via the integration layer (ground plane already follows the interest config).

Framework follow-ups (deferred as premature)

  • Grid cell→candidate reverse index — election currently scans the viewer map (O(entities × players)), fine at present scale; swap in a spatial index if entity/player counts grow.
  • lastOwner grace window — briefly accept the previous owner's in-flight updates after a handoff to smooth the seam (today the elected client seeds from the already-replicated pose).
  • Scripting hooksforceSyncer / onSyncerChange so script layers can pin or observe delegation.
  • Server population-authority layer — a sibling of election on the same cell structure, for server-blessed ambient/relevancy entities.

Segfaultd added 2 commits July 7, 2026 21:54
Server-side proximity election that hands a non-owned entity's authority
to a nearby client -- the syncer -- reusing the existing ownerGUID model.

Adds a Delegation trait (opt-in delegatable flag, OrphanMode, pinned
owner), an OnOwnershipChanged client hook, and a CanDelegateTo veto. The
decision is a pure, testable ElectOwner policy (acquire/drop hysteresis,
least-loaded with nearest tiebreak, soft cap), driven each interest
rebuild by ReplicationManager::RunDelegation and applied via SetOwner.
Orphans fall back to the server (frozen) or destroy per OrphanMode.
Unit tests for ElectOwner: nearest pick, load balancing, acquire/drop
hysteresis, re-election on drift, orphan fallback, dimension/veto
exclusion, load soft cap, and Z-up ground-plane distance.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a proximity-based ownership delegation system: a new ElectOwner policy header, periodic RunDelegation execution in ReplicationManager triggered from NetworkPeer::Update, and refactored NetworkEntity ownership adoption via SetOwnerFromServer with construction-safe OnOwnershipChanged callbacks. Unit tests cover election scenarios.

Changes

Proximity-based ownership delegation

Layer / File(s) Summary
Delegation policy contract
code/framework/src/networking/replication/delegation_policy.h
New header defines DelegationParams, DelegationCandidate, and ElectOwner, implementing hysteresis-based owner selection with load and distance tie-breaking.
Entity ownership adoption and construction lifecycle
code/framework/src/networking/replication/network_entity.h, code/framework/src/networking/replication/network_entity.cpp
Adds Delegation/OrphanMode types, OnOwnershipChanged/CanDelegateTo hooks, replaces SetOwner with SetOwnerFromServer, and suppresses ownership callbacks during construction via _constructing.
Replication manager delegation execution
code/framework/src/networking/replication/replication_manager.h, code/framework/src/networking/replication/replication_manager.cpp
Adds RunDelegation server-side election sweep, DelegationParams accessors, _groundXY tracking, and reorders stateEpoch/owner assignment in the SetOwnerRPC handler.
Peer update wiring
code/framework/src/networking/network_peer.cpp
Calls ReplicationManager::RunDelegation with the current time after RebuildInterest() in NetworkPeer::Update.
Delegation tests
code/tests/modules/delegation_ut.h, code/tests/framework_ut.cpp
Adds a new delegation unit test module covering ElectOwner scenarios and registers it in the test runner.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NetworkPeer
  participant ReplicationManager
  participant ElectOwner
  participant NetworkEntity

  NetworkPeer->>ReplicationManager: Update() -> RunDelegation(now)
  ReplicationManager->>ReplicationManager: build load map of owned entities
  loop each delegatable entity
    ReplicationManager->>ReplicationManager: collect eligible viewers (CanDelegateTo)
    ReplicationManager->>ElectOwner: ElectOwner(pos, currentOwner, candidates, params)
    ElectOwner-->>ReplicationManager: next owner guid
    alt owner changed
      ReplicationManager->>NetworkEntity: SetOwner(next)
      NetworkEntity->>NetworkEntity: SetOwnerFromServer(next) triggers OnOwnershipChanged
    else unassigned + OrphanMode::Destroy
      ReplicationManager->>NetworkEntity: DestroyEntity()
    end
  end
Loading

Possibly related PRs

  • MafiaHub/Framework#201: Builds directly on the replication framework this PR modifies, touching NetworkPeer::Update, ReplicationManager, and NetworkEntity owner adoption.

Poem

A hop, a scan, who owns the ground?
Radii shrink and load's renowned,
The rabbit elects with hop and hop,
Nearest, lightest — never stop! 🐇
Ownership passes, smooth and neat,
Delegation dance, complete!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replication authority delegation with an elected syncer model.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/authority-delegation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Cut the narration to terse invariants and drop the third-party mod
reference from the source.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
code/framework/src/networking/replication/delegation_policy.h (1)

24-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider guarding against acquireRange > dropRange misconfiguration.

The hysteresis contract relies on acquireRange <= dropRange (owner retained up to the wider drop threshold, new candidates only accepted within the tighter acquire threshold). If a caller ever misconfigures acquireRange > dropRange, the hysteresis logic in ElectOwner would behave inconsistently. Consider a lightweight assert or a clamp in the constructor/accessor to catch this early.

🛡️ Proposed defensive check
     struct DelegationParams {
         float acquireRange          = 80.0f;
         float dropRange             = 130.0f;
         uint32_t electionIntervalMs = 500;
+        // Note: acquireRange should always be <= dropRange for hysteresis to be meaningful.
         // Soft cap on how many delegated entities one client is newly given (0 = unlimited). The load
         // balancer already prefers the least-loaded candidate; this refuses to pile more onto a client
         // already at the cap when a lighter one exists, but never revokes what it holds.
         int loadSoftCap = 0;
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/framework/src/networking/replication/delegation_policy.h` around lines
24 - 32, DelegationParams currently allows an invalid hysteresis setup if
acquireRange is greater than dropRange, which can break the ownership election
behavior. Add a lightweight defensive check where DelegationParams is
initialized or consumed so acquireRange is always less than or equal to
dropRange, using a clear assert or equivalent clamp in the path used by
ElectOwner. Keep the check close to DelegationParams so misconfiguration is
caught early before replication delegation logic runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/framework/src/networking/replication/replication_manager.cpp`:
- Around line 246-251: The pinned-owner logic in RunDelegation() keeps forcing
ownership to delegation.pinnedOwner even after that peer disconnects, so update
the disconnect handling or the re-pin check to ensure only live peers are used.
In replication_manager.cpp, either clear entity->delegation.pinnedOwner when the
pinned peer disconnects, or gate the SetOwner path on a current
viewer/alive-peer check before reassigning. Keep the fix localized around
RunDelegation() and the disconnect path that manages delegation state.

---

Nitpick comments:
In `@code/framework/src/networking/replication/delegation_policy.h`:
- Around line 24-32: DelegationParams currently allows an invalid hysteresis
setup if acquireRange is greater than dropRange, which can break the ownership
election behavior. Add a lightweight defensive check where DelegationParams is
initialized or consumed so acquireRange is always less than or equal to
dropRange, using a clear assert or equivalent clamp in the path used by
ElectOwner. Keep the check close to DelegationParams so misconfiguration is
caught early before replication delegation logic runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b96536c-e895-440c-94d3-12f1bd4be9f6

📥 Commits

Reviewing files that changed from the base of the PR and between 1147c65 and 763fc9e.

📒 Files selected for processing (8)
  • code/framework/src/networking/network_peer.cpp
  • code/framework/src/networking/replication/delegation_policy.h
  • code/framework/src/networking/replication/network_entity.cpp
  • code/framework/src/networking/replication/network_entity.h
  • code/framework/src/networking/replication/replication_manager.cpp
  • code/framework/src/networking/replication/replication_manager.h
  • code/tests/framework_ut.cpp
  • code/tests/modules/delegation_ut.h

Comment on lines +246 to +251
if (entity->delegation.pinnedOwner != MafiaNet::UNASSIGNED_PEER_GUID) {
if (entity->ownerGUID != entity->delegation.pinnedOwner) {
SetOwner(entity, entity->delegation.pinnedOwner);
}
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and surrounding symbols first.
ast-grep outline code/framework/src/networking/replication/replication_manager.cpp --view expanded || true

# Find all references to pinnedOwner, ownership changes, and disconnect handling.
rg -n --hidden --no-ignore-vcs \
  -e 'pinnedOwner|OnClosedConnection|SetOwner\(|ownerGUID|UNASSIGNED_PEER_GUID|GetViewer\(' \
  code/framework/src code -g '!**/build/**' -g '!**/dist/**' || true

# Read the relevant slices from the replication manager.
sed -n '200,320p' code/framework/src/networking/replication/replication_manager.cpp

# Inspect any disconnect/connection cleanup paths that might clear pinning.
rg -n --hidden --no-ignore-vcs -e 'clear.*pinnedOwner|pinnedOwner\s*=|delegation\.pinnedOwner' code/framework/src code -g '!**/build/**' -g '!**/dist/**' || true

Repository: MafiaHub/Framework

Length of output: 30953


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the pinning/delegation definitions and any public APIs around them.
sed -n '130,220p' code/framework/src/networking/replication/network_entity.h
sed -n '1,220p' code/framework/src/networking/replication/network_entity.cpp
sed -n '120,180p' code/framework/src/networking/replication/replication_manager.h

# Search for any code that explicitly clears or updates pinnedOwner.
rg -n --hidden --no-ignore-vcs \
  -e 'pinnedOwner\s*=|SetPinned|ClearPinned|delegation\.pinnedOwner|pinned syncer|pinned' \
  code/framework/src code/tests -g '!**/build/**' -g '!**/dist/**' || true

Repository: MafiaHub/Framework

Length of output: 18815


Clear or validate pinnedOwner when the pinned peer disconnects code/framework/src/networking/replication/replication_manager.cpp:241-243

pinnedOwner is never cleared on disconnect, so RunDelegation() keeps reassigning the entity to a disconnected peer and bypasses normal election/orphan handling. Gate the re-pin on a live viewer, or clear delegation.pinnedOwner in the disconnect path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/framework/src/networking/replication/replication_manager.cpp` around
lines 246 - 251, The pinned-owner logic in RunDelegation() keeps forcing
ownership to delegation.pinnedOwner even after that peer disconnects, so update
the disconnect handling or the re-pin check to ensure only live peers are used.
In replication_manager.cpp, either clear entity->delegation.pinnedOwner when the
pinned peer disconnects, or gate the SetOwner path on a current
viewer/alive-peer check before reassigning. Keep the fix localized around
RunDelegation() and the disconnect path that manages delegation state.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants