ADR-0001: Owner-authoritative player movement with server-side clamps
- Date: 2026-08-22
- Status: Accepted
- Decision maker: Joao Miranda (product owner)
- Ticket: RIOT-51
Context
Section titled “Context”Prefabs/Player/Player.prefab carries two position replicators on the same GameObject, with
opposite authority:
| Component | Line | _ownerAuth |
Controller resolves to |
|---|---|---|---|
PurrNet.NetworkTransform |
:167, flag at :187 |
1 |
the owning client |
PurrNet.NetworkRigidbody |
:225, flag at :237 |
0 |
the server |
NetworkIdentity.cs:266 — IsController(bool ownerHasAuthority) => ownerHasAuthority ? isController : isServer
— is what makes these resolve differently on the same body. The result is not redundancy, it is
two systems fighting:
- On the host,
NetworkTransform.cs:413-430callsRigidbody.Sleep()every physics step for every remote player, because the host is not the NetworkTransform controller.NetworkRigidbodythen reads that frozen_rigidbody.positionand rebroadcasts it as authoritative with zerolinearVelocity. - On the owning client,
NetworkRigidbody.cs:163-166routes the owner downNonControllerTick, which soft-corrects toward the server’s echo of the client’s own position from ~1 RTT ago, and hard-snaps viaMovePositionpast the correction threshold. The owner is dragged backwards by a stale copy of itself. - Both components delta-pack position and rotation independently, so every player costs double transform state on the wire.
The player prefabs are the only objects in the project carrying both components. All 28 item
prefabs and every NetworkRigidbody in NewApartment.unity are uniformly _ownerAuth: 0.
Decision
Section titled “Decision”The owning client is authoritative over its own raccoon’s movement.
- Keep
NetworkTransformwith_ownerAuth: 1. DeleteNetworkRigidbodyfrom the player prefab. - The server does not simulate remote players. It validates the incoming transform stream and clamps or rejects values outside the thresholds below.
- Items keep server authority. This decision covers the player only; the 28 item prefabs stay
_ownerAuth: 0. That split is deliberate and is what RIOT-168 / RIOT-174 must be written against.
Player-vs-player physics: out of scope, deliberately
Section titled “Player-vs-player physics: out of scope, deliberately”Fighting is a real mechanic, but it is targeted, not emergent. PlayerAttack.cs already works
this way — it resolves a _bestAttackTargetRef to an IInteractable and acts on it.
Raccoons do not shove each other around as emergent rigidbody collision. Body-checking someone off a ledge is not a supported verb. Player-vs-player collision is cosmetic, resolved on each owner’s machine, and is permitted to disagree between clients.
This was asked and answered explicitly during the decision, because emergent shoving is the one requirement that would have invalidated owner-authority.
Knockback is a server-issued instruction, never a server-side force
Section titled “Knockback is a server-issued instruction, never a server-side force”Because the server does not control the player’s rigidbody, it cannot apply knockback with
AddForce. Any such call is corrected away by the owner within one tick.
Knockback is delivered as a networked event carrying an impulse vector, which the target’s own
client applies through its VelocityCompiler. The server decides that knockback happens and
how much; the owner executes it.
This is a hard constraint on the T3 damage work (RIOT-140, RIOT-54) and on RIOT-96.
Clamp thresholds
Section titled “Clamp thresholds”⚠️ These are initial values, not measured ones. They are derived from the code and must be validated in RIOT-94 before being treated as final.
They are expressed in real units, post-de-inflation. They are meaningless until RIOT-75 lands:
VelocityCompiler.cs:123-130 currently multiplies by Time.fixedDeltaTime in LateUpdate, a
constant ÷50 (TimeManager.asset:6 Fixed Timestep: 0.02), compensated by prefab tuning inflated
40–50× (Player.prefab:515 _walkSpeed: 250 against PlayerWalking.cs:17 _walkSpeed = 5f;
:529 _jumpHeight: 200 against PlayerJump.cs:12 _jumpHeight = 5f). Intended walk speed is
therefore 5 m/s — 250 × 0.02.
| Threshold | Initial value | Derivation |
|---|---|---|
| Max horizontal speed | 12 m/s | 5 m/s walk, plus headroom for knockback impulses and slides |
| Max vertical speed | 25 m/s | terminal fall velocity in a single-apartment map |
| Max per-tick position delta | 1.5 m | 12 m/s at the 20 Hz tick (InitializeScene.unity:197) is 0.6 m; margin for a knockback burst |
| NaN / infinity | always reject | not a clamp — a rejection, logged |
| Geometry raycast | out of scope for v1 | revisit only if wall-clipping is observed in play |
Excluding the raycast is a conscious acceptance of risk: a modified client can walk through geometry. Under a friends-only lobby model that is tolerable. It stops being tolerable if the game ever ships public matchmaking, at which point this ADR is superseded.
Alternatives considered
Section titled “Alternatives considered”Server-authoritative movement with client input RPCs. Rejected. The input channel does not
exist in any form — InputController.cs:9 is a plain MonoBehaviour that has never emitted an
RPC, so the server has never received an input frame in the history of this branch. It is hard-
blocked by RIOT-39, since PlayerModule.cs:34-45 destroys movement components on every non-owner.
And PurrNet 1.19.1 ships no prediction module — ColliderRollback is the only rollback
machinery in the package and zero instances exist in the project. The honest cost is a prediction,
reconciliation and rollback engine written by hand, in exchange for anti-cheat guarantees that
matter in ranked competitive shooters and not in a four-player party game on a 20 Hz tick.
Consequences
Section titled “Consequences”Unblocks: RIOT-61 (remove the losing replicator), RIOT-75 (de-inflate tuning), RIOT-67
(VelocityCompiler LateUpdate→FixedUpdate), RIOT-86 (GravityModule coroutine), RIOT-94 (the clamps
themselves), RIOT-76 (the Destroy(this) calls in VelocityCompiler/GravityModule).
Closes as not-applicable: RIOT-122 (ColliderRollback / lag compensation) — there is no
server-side hit validation of position to compensate for.
Cheating on position is possible within the clamp envelope. Accepted, see above.
Revisit if
Section titled “Revisit if”The game ships public matchmaking, or emergent player-vs-player physics becomes a designed verb. Either reopens this decision, and the second one requires a full re-plan of T1 and T2.