docs(physics): C1 satisfied by the existing publication mechanism
The C1 body-writer research found the atomic controller/body transaction already built and tested: RuntimeLocalPlayerPhysicsPublicationState plus the dormant local-activation family implement the sanctioned off-canonical-prepare + validated-atomic-commit shape end-to-end, with zero production callers. The committed writer map records the six canonical body writers, the two host escape hatches (the public Controller setter both hosts write directly; App's object-clock facade bypasses), the headless prepared-collision fragility, and both hosts' construction divergences. C1 therefore collapses into C3's route-1 flip — the remaining work is production wiring, not mechanism design — and C2 (the placement allocation budget) becomes the next slice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ae29639307
commit
6460596b56
2 changed files with 699 additions and 6 deletions
|
|
@ -74,12 +74,24 @@ same commit) → docs/handoff commit. No workarounds; no fused slices.
|
|||
deferral; refine at C3/C4 if the connected gates show it matters);
|
||||
(c) any future host exposure of `TrySetAutonomyLevel` must carry retail's
|
||||
`SendAutonomyLevelEvent` (699550).
|
||||
- **C1 — atomic controller/body publication (prerequisite C).** One
|
||||
Runtime-owned transaction publishing the exact same canonical body to
|
||||
graphical and no-window controllers, prepared off-canonical, committed
|
||||
atomically, respected by every body writer/binding/clock-epoch/teardown
|
||||
path. The rejected snapshot-lease design stays rejected. Adversarial-gate
|
||||
heavy (the campaign handoff's full list).
|
||||
- **C1 — atomic controller/body publication — SATISFIED BY EXISTING
|
||||
MECHANISM (research finding 2026-08-02, plan amended same session).**
|
||||
`RuntimeLocalPlayerPhysicsPublicationState` (1,033 lines) plus the
|
||||
~15-method dormant local-activation family on `RuntimeSetPositionState`
|
||||
already implement the full sanctioned option-2 transaction:
|
||||
off-canonical preparation against a scratch quantum clock and a sealed
|
||||
candidate controller, one validated atomic Commit, and a staged
|
||||
Evaluate/Commit/FinalizeActivation chain re-validated against
|
||||
PhysicsOwnershipEpoch/ObjectClockEpoch/ControllerOwnershipEpoch/session
|
||||
identity at every entry — with zero production callers. See
|
||||
[`2026-08-02-canonical-body-writer-map.md`](../research/2026-08-02-canonical-body-writer-map.md)
|
||||
(6 canonical body writers; the two host escape hatches; both hosts'
|
||||
divergences). The remaining work — routing both hosts' local-player
|
||||
construction through the publication lifecycle, sealing the public
|
||||
`RuntimeLocalPlayerMovementState.Controller` setter, retiring App's
|
||||
direct object-clock bypasses, and containing headless's uncaught
|
||||
prepared-collision `InvalidDataException` — IS the C3 route-1 flip and
|
||||
moves there. No separate C1 commit.
|
||||
- **C2 — placement allocation budget.** Pool or eliminate the operation/
|
||||
projection envelope allocations on the accepted placement path (root
|
||||
cause, not a raised cap), or obtain explicit user approval for a measured
|
||||
|
|
|
|||
681
docs/research/2026-08-02-canonical-body-writer-map.md
Normal file
681
docs/research/2026-08-02-canonical-body-writer-map.md
Normal file
|
|
@ -0,0 +1,681 @@
|
|||
# C1 body/controller-publication writer map (2026-08-02)
|
||||
|
||||
Repo: `C:\Users\erikn\.codex\worktrees\af5e\acdream`, branch `codex/port-claude-agents`,
|
||||
HEAD `ae296393`. READ-ONLY research; this file is the only write target.
|
||||
|
||||
Context read: `docs/plans/2026-08-02-placement-cutover.md` (slice C1),
|
||||
`docs/research/2026-07-31-remaining-physics-campaign-handoff.md` (rejected-prototype
|
||||
section, lines 143-168; prerequisite C, lines 203-221), and
|
||||
`docs/research/2026-08-02-cutover-route-inventory.md` route 1 + prerequisite-C
|
||||
section (lines 174-220) + route 8 (headless).
|
||||
|
||||
---
|
||||
|
||||
## 1. Every writer of `RuntimeEntityRecord.PhysicsBody`
|
||||
|
||||
`PhysicsBody` is `public PhysicsBody? PhysicsBody { get; private set; }`
|
||||
(`src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs:72`). The ONLY mutator is
|
||||
the internal method `SetPhysicsBody(PhysicsBody? body)`
|
||||
(`RuntimeEntityRecord.cs:176-182`):
|
||||
```
|
||||
internal void SetPhysicsBody(PhysicsBody? body)
|
||||
{
|
||||
if (ReferenceEquals(PhysicsBody, body)) return;
|
||||
PhysicsBody = body;
|
||||
PhysicsOwnershipEpoch++; // <-- the ONLY place PhysicsOwnershipEpoch is bumped
|
||||
}
|
||||
```
|
||||
So every "writer" is a caller of `.SetPhysicsBody(...)` (all 6 call sites, confirmed
|
||||
by full-repo grep, zero others):
|
||||
|
||||
1. **`RuntimeEntityDirectory.cs:359`** — inside
|
||||
`GetOrCreatePhysicsBody(RuntimeEntityRecord record, Func<incarnation,PhysicsBody> factory)`
|
||||
(need exact surrounding signature — read below). Public/internal API used by
|
||||
the route-1 "SECOND, narrower body-construction duplicate authority" for
|
||||
non-player static-animating physics objects
|
||||
(`DatLiveEntityProjectionMaterializer.cs:1003-1016`, per the route inventory).
|
||||
Guard: only sets if record has no body yet (idempotent-create pattern) — see
|
||||
full read below for exact guard.
|
||||
2. **`RuntimeEntityObjectLifetime.cs:766`** — `Entities.SetPhysicsBody(canonical, null)`
|
||||
inside a teardown method (need to confirm exact method — likely delete/retire
|
||||
path, paired with `Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false)`
|
||||
at line 767 in the SAME method). Clears body on deletion/teardown.
|
||||
3. **`RuntimeLocalPlayerPhysicsPublicationState.cs:405`** — `candidate.Record.SetPhysicsBody(candidate.Body)`
|
||||
inside `Commit(token, out activationToken)` (lines 373-411). **THIS IS THE
|
||||
DORMANT OPTION-2 MECHANISM** — see section 4 below. Guarded by `IsCurrent(candidate)`
|
||||
(epoch/session/identity/null-state re-check, lines 891-922) immediately before,
|
||||
and by `_physics.SetPosition.PrepareDormantLocalActivationOwnership(...)` called
|
||||
first (line 394) as the "seal the exact SetPosition owner before the
|
||||
irreversible no-fail suffix" step — i.e. this call site DOES chain into
|
||||
PrepareDormantLocalActivationOwnership per task 4's target.
|
||||
4. **`RuntimeLocalPlayerPhysicsPublicationState.cs:1025`** — `_entities.SetPhysicsBody(activation.Record, null)`
|
||||
inside `DiscardActivation()` (994-1032), the rollback/teardown path for the
|
||||
SAME dormant mechanism — only fires if `_entities.IsCurrent(activation.Record)`
|
||||
AND `ReferenceEquals(activation.Record.PhysicsBody, activation.Body)` (i.e.
|
||||
never clobbers a body some OTHER newer owner already installed — the exact
|
||||
anti-pattern the rejected prototype failed on).
|
||||
5. **`RuntimePhysicsState.cs:1558`** — `Entities.SetPhysicsBody(record, candidateBody)`
|
||||
— need full read; this is inside the remote/projectile body-binding family
|
||||
(see section 3).
|
||||
6. **`RuntimePhysicsState.cs:1666`** — `Entities.SetPhysicsBody(record, candidate)`
|
||||
— need full read; this is the OTHER binding site, guarded by
|
||||
`PhysicsBodyAcquisitionInProgress` (set true at :1645, cleared at :1676/1678).
|
||||
|
||||
**Writer count: 6 call sites, across 3 files** (`RuntimeEntityDirectory.cs` x1,
|
||||
`RuntimeEntityObjectLifetime.cs` x1, `RuntimeLocalPlayerPhysicsPublicationState.cs` x2,
|
||||
`RuntimePhysicsState.cs` x2).
|
||||
|
||||
## Consumers of `PhysicsOwnershipEpoch`
|
||||
|
||||
Only bumped in one place (`RuntimeEntityRecord.SetPhysicsBody`, above). Consumers
|
||||
(all in `RuntimeLocalPlayerPhysicsPublicationState.cs`) treat it as a
|
||||
compare-and-reject epoch stamped into every token/activation struct:
|
||||
- `RuntimeLocalPlayerPhysicsPublicationToken.PhysicsOwnershipEpoch` (field, :53)
|
||||
captured at `Prepare` time (:314).
|
||||
- `RuntimeLocalPlayerPhysicsActivationToken.PhysicsOwnershipEpoch` (:70) captured
|
||||
as `token.PhysicsOwnershipEpoch + 1UL` (:328) — i.e. the activation token
|
||||
encodes "the epoch AFTER my own commit bumps it", so `IsActivationCurrent`
|
||||
(931-962) and `IsActivationOwnershipEnvelopeCurrent` (717-745) comparing
|
||||
`activation.Record.PhysicsOwnershipEpoch == activation.Token.PhysicsOwnershipEpoch`
|
||||
will FAIL (reject) the instant any OTHER writer (remote/projectile bind, GC
|
||||
clear, non-player static body creation via `RuntimeEntityDirectory` — none of
|
||||
which should ever touch a local-player record, but the check is defense-in-depth)
|
||||
touches the same record's PhysicsBody between prepare and commit.
|
||||
- `IsCurrent(candidate)` (891-922, pre-Commit re-check) also compares
|
||||
`candidate.Record.PhysicsOwnershipEpoch == candidate.Token.PhysicsOwnershipEpoch`
|
||||
(unincremented — i.e. "nobody touched the body between Prepare and Commit").
|
||||
|
||||
**This IS the reentrancy defense the rejected prototype lacked** — see section 6.
|
||||
|
||||
## 2. The two production local-player controller constructions, end to end
|
||||
|
||||
### Graphical: `PlayerModeController.BuildControllerAndCamera`
|
||||
`src/AcDream.App/Input/PlayerModeController.cs:244-525`. Constructor list
|
||||
(52-74) shows it is injected with `RuntimeLocalPlayerMovementState controllerSlot`
|
||||
(the SAME slot type headless writes) — confirms the route-inventory's "open
|
||||
question" (2026-08-02-cutover-route-inventory.md:204-207): **App DOES write
|
||||
`_controllerSlot.Controller = controller` directly, at line 486.** Not a
|
||||
mystery/asymmetry — both hosts write the exact same public setter.
|
||||
|
||||
Steps, in order:
|
||||
1. `_approachCompletions.BeginControllerLifetime()` (250) — App-only approach
|
||||
lifecycle token.
|
||||
2. Capture rollback snapshots: `_camera.CaptureState()` (255),
|
||||
`_shadow.Capture()` (256) — presentation-only.
|
||||
3. `new PlayerMovementController(_physics, playerRecord.ObjectClock, PlayerMovementConstructionOptions.From(_skills.Snapshot))`
|
||||
(259-262) — **uses the PUBLIC constructor**, whose default publication
|
||||
lifecycle is `StandalonePublished` (`PlayerMovementController.cs:617-626`),
|
||||
NOT `CandidatePreparing`/`CreatePublicationCandidate`. This is the key
|
||||
divergence from the dormant mechanism (section 4): this controller never
|
||||
enters the `CandidatePreparing -> CandidateSealed -> RuntimeOwnedDormant ->
|
||||
RuntimePublished` lifecycle at all.
|
||||
4. Builds `MoveToManager`/`EntityPhysicsHost` closures over captured locals
|
||||
(267-346) — presentation-adjacent glue, host-specific.
|
||||
5. `EntityPhysicsHostComposition.SelectStableHostWithoutRebind` (347-350) —
|
||||
canonical-state read (checks `LiveEntityRecord.PhysicsHost`).
|
||||
6. `RuntimeMovementSkillProjection.ApplyTo(_skills, controller)` (366-368).
|
||||
7. `ApplyStepHeights(controller, playerEntity, playerGuid)` (375) — **reads
|
||||
`DatReaderWriter.DBObjs.Setup` directly** (per headless's own comment
|
||||
contrasting itself, `HeadlessSessionWorldProjection.cs:685-689`) — NOT
|
||||
through the prepared-collision/`IPreparedCollisionSource` seam headless
|
||||
uses. Divergence #1.
|
||||
8. `_controllerSlot.BeginMotionPreparation(controller, drainPriorAnimationQueue)`
|
||||
(404-407) — the ONE existing narrow "preparation lease" concept already in
|
||||
`RuntimeLocalPlayerMovementState` (separate from the dormant physics
|
||||
publication state) that lets a synchronous PartArray/type-5 completion
|
||||
reach the candidate `MotionInterpreter` before publish.
|
||||
9. **Duplicate authority** — `_physics.Resolve(...)` (409-413) then
|
||||
`_physics.ResolvePlacement(...)` (422-430) — direct canonical-state-free
|
||||
collision resolve, entirely outside `RuntimeSetPositionState`.
|
||||
10. `controller.PreparePositionForCommit(...)` (434-437),
|
||||
`controller.SetBodyOrientation(...)` (438).
|
||||
11. Camera construction + `_camera.EnterChaseMode(...)` (440-447) —
|
||||
presentation-only, but happens BEFORE the final canonical commit (445-447
|
||||
precede line 482-484) — i.e. camera activation today is NOT gated on a
|
||||
Runtime placement acknowledgement.
|
||||
12. Re-check host stability (449-458) — throws if the host changed during
|
||||
camera activation (defensive, but ad hoc — not an epoch/token check, a
|
||||
bespoke `ReferenceEquals` re-read).
|
||||
13. Shadow sync (`_shadow.SyncPose(...)`, 460-466).
|
||||
14. `EntityPhysicsHostComposition.InstallOrRebind(...)` (472-475) + another
|
||||
`ReferenceEquals` stability re-check (476-480).
|
||||
15. **Duplicate authority — final commit** (482-484):
|
||||
`playerEntity.SetPosition(initial.Position); playerEntity.ParentCellId =
|
||||
initial.CellId; controller.CommitPreparedPosition();` — direct writes to
|
||||
the App-side `WorldEntity`/render sidecar AND `controller`'s own internal
|
||||
frame, bypassing any Runtime `Place` receipt or `RuntimeEntityRecord`
|
||||
write. **`RuntimeEntityRecord.PhysicsBody`/`PhysicsOwnershipEpoch` are
|
||||
NEVER touched anywhere in this method** — `controller.PhysicsBody` (the
|
||||
`_body` field created in step 3) stays a private field of the
|
||||
`StandalonePublished` controller; nothing calls
|
||||
`Entities.SetPhysicsBody(playerRecord, controller.PhysicsBody)`. This
|
||||
means TODAY the canonical `RuntimeEntityRecord.PhysicsBody` slot for the
|
||||
graphical local player is **never populated at all** by this path — a
|
||||
previously-unstated confirmation that `SubmitPreparedPlacement`'s
|
||||
`operation.Record.PhysicsBody is not { } body` requirement (section 3)
|
||||
would REJECT any ordinary (non-initial) SetPosition submitted for the
|
||||
graphical local player today, because no writer ever puts a body on that
|
||||
record. (Route 2's "ForcePosition" duplicate authority,
|
||||
`LocalForcePositionTransaction`, works around this by mutating
|
||||
`PlayerMovementController`'s own body directly via `BlipPosition`, never
|
||||
touching `RuntimeEntityRecord.PhysicsBody` either — internally consistent
|
||||
with each other, both equally disconnected from the canonical record.)
|
||||
16. Slot commits (485-492): `_hostSlot.Host`, `_controllerSlot.Controller =
|
||||
controller` (the public, unguarded setter — bumps `ControllerOwnershipEpoch`
|
||||
unconditionally, see section 4), `_chase.Legacy/Retail`, `_mode.IsPlayerMode
|
||||
= true`.
|
||||
17. `catch`: rolls back camera + shadow only (494-518); does NOT roll back
|
||||
steps 15-16 because those are the LAST lines before `lifetimeCommitted =
|
||||
true` — structurally "hope nothing after this throws" rather than an
|
||||
explicit no-fail invariant.
|
||||
|
||||
**Canonical-state mutations in this method: NONE on `RuntimeEntityRecord`**
|
||||
(no `SetPhysicsBody`, no `SetFullCell`, no object-clock call) — everything
|
||||
mutated is App-local (`WorldEntity`, `PlayerMovementController`'s private
|
||||
body, `RuntimeLocalPlayerMovementState.Controller`,
|
||||
`LocalPlayerPhysicsHostSlot`, camera, shadow). The ONLY canonical-record
|
||||
writes for the local player's initial placement happen earlier in the
|
||||
hydration pipeline (`LiveEntityRuntime.MaterializeLiveEntity`/
|
||||
`RebucketLiveEntity`, route 1 hops 9-11) — entirely disjoint from this method.
|
||||
|
||||
### Headless: `HeadlessSessionWorldProjection.CreateController` + `SynchronizeLocalPlayer`
|
||||
`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:566-655`
|
||||
(read in full).
|
||||
|
||||
`SynchronizeLocalPlayer` (566-615):
|
||||
1. Guards on `record.ServerGuid == _runtime.PlayerIdentity.ServerGuid` and a
|
||||
present `Snapshot.Position` (568-573).
|
||||
2. `_collision.CenterOn(position.LandblockId)` (575) — headless collision-
|
||||
neighborhood readiness, no graphical analog.
|
||||
3. `_runtime.MovementOwner.Controller ?? CreateController(record)` (576-578)
|
||||
— lazy-construct-once via the SAME public `Controller` getter/setter
|
||||
`RuntimeLocalPlayerMovementState` exposes; no reentrancy guard against two
|
||||
concurrent calls both observing `null` (single-threaded host loop makes
|
||||
this safe in practice today, not structurally).
|
||||
4. `_runtime.EntityObjects.Physics.Engine.Resolve(...)` (589-594) then
|
||||
`.ResolvePlacement(...)` (595-605) — **the exact same duplicate-authority
|
||||
shape as graphical step 9**, hardcoded `DefaultRadius`/`DefaultHeight`
|
||||
constants (visible in the call, actual values not read here) instead of
|
||||
`_motionBindings.GetSetupCylinder`.
|
||||
5. `controller.SetPosition(...)` + `controller.SetBodyOrientation(...)`
|
||||
(610-614) — **duplicate final commit**, headless's version of graphical
|
||||
step 15. Also never touches `RuntimeEntityRecord.PhysicsBody`.
|
||||
|
||||
`CreateController` (639-655):
|
||||
1. `new PlayerMovementController(_runtime.EntityObjects.Physics.Engine, record.ObjectClock, PlayerMovementConstructionOptions.From(_runtime.CharacterOwner.MovementSkills.Snapshot))`
|
||||
(642-646) — **same PUBLIC constructor / `StandalonePublished` lifecycle**
|
||||
as graphical step 3.
|
||||
2. `ApplySetupStepHeights(record, controller)` (649, body at 657-691) —
|
||||
**reads via `_preparedCollision.ReadSetupCollision(setupId)`** (668-679),
|
||||
the prepared-asset seam, NOT raw DAT — divergence #1 mirrored (headless
|
||||
uses the "correct"/prerequisite-B-aligned source; graphical does not).
|
||||
**Throws `InvalidDataException`** if the read status isn't `Loaded`
|
||||
(672-675) — propagates uncaught up through `SynchronizeLocalPlayer` ->
|
||||
`ProjectSpawn`/`ProjectPosition` -> the wire-dispatch call chain. This IS
|
||||
gate 10 (late headless prepared-collision failure) manifesting today as an
|
||||
unhandled exception, not a retry.
|
||||
3. `RuntimeMovementSkillProjection.ApplyTo(_runtime.CharacterOwner.MovementSkills, controller)`
|
||||
(650-652).
|
||||
4. `_runtime.MovementOwner.Controller = controller;` (653) — **the exact
|
||||
same public setter graphical step 16 uses.**
|
||||
|
||||
**No headless equivalent of graphical steps 1 (approach lifetime), 8 (motion
|
||||
preparation lease), 11-14 (camera + host-stability re-checks), 17 (camera/
|
||||
shadow rollback)** — headless has no camera/shadow/approach concept at all
|
||||
(confirmed, matches the route inventory's "No headless equivalent of
|
||||
graphical hops 12-14/19").
|
||||
|
||||
### Top divergences between the two hosts (summary)
|
||||
1. **Setup/collision data source**: App reads raw DAT (`ApplyStepHeights` via
|
||||
`_dats`/`_datLock`); headless reads the prepared/baked asset
|
||||
(`ApplySetupStepHeights` via `IPreparedCollisionSource`). Same target
|
||||
values, different pipeline — a real fidelity risk if the two ever diverge
|
||||
(baking staleness).
|
||||
2. **Default cylinder fallback**: App falls back to `0.48f`/`1.835f` inline
|
||||
(`PlayerModeController.cs:416-420`) when `GetSetupCylinder` returns
|
||||
`< 0.05f` radius; headless uses named `DefaultRadius`/`DefaultHeight`
|
||||
constants at the `ResolvePlacement` call site (:599-600) — same intended
|
||||
values, defined in two places.
|
||||
3. **Failure handling**: App's `BuildControllerAndCamera` has an explicit
|
||||
try/catch/rollback for camera+shadow; headless's `CreateController`/
|
||||
`ApplySetupStepHeights` has NO surrounding try/catch — a prepared-collision
|
||||
read failure is a raw unhandled exception today.
|
||||
4. **Presentation surface**: App additionally owns approach-completion
|
||||
lifetime, motion-preparation lease, chase camera, shadow sync — none of
|
||||
which headless has or needs.
|
||||
5. **Neither host touches `RuntimeEntityRecord.PhysicsBody`, `PhysicsOwnershipEpoch`,
|
||||
or any `RuntimeSetPositionState` API** — both are 100% off to the side of
|
||||
the canonical record, confirmed by exhaustive grep (section 1's 6 writer
|
||||
call sites do not include either `PlayerModeController.cs` or
|
||||
`HeadlessSessionWorldProjection.cs`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Every other body binding/consumer
|
||||
|
||||
- **Remote dead-reckoning** (`RuntimeRemotePhysicsUpdater.cs` — flagged
|
||||
protected/dirty, read-only, NOT modified): grep confirms it only READS
|
||||
`record.PhysicsBody` via `ReferenceEquals(record.PhysicsBody, remote.Body)`
|
||||
currency checks (line 817) — it does not call `SetPhysicsBody`. The actual
|
||||
writer for remote motion is `RuntimePhysicsState.SetRemoteMotion`
|
||||
(`RuntimePhysicsState.cs:1446-1570`, full read) — throws
|
||||
`InvalidOperationException` on: binding-already-in-progress (1460-1464),
|
||||
body-would-be-replaced when a body already exists and doesn't match
|
||||
(1487-1492), losing an existing remote-placement contract (1493-1498), or
|
||||
post-callback ownership drift detected via a captured
|
||||
`sessionVersion`/`expectedBody`/`expectedRuntime` triple re-checked after
|
||||
the bind callback (1539-1549, "changed ownership during remote-motion
|
||||
binding"). Calls `Entities.SetPhysicsBody(record, candidateBody)` (:1558)
|
||||
ONLY when `expectedBody is null` (first bind) via `InitializeNewPhysicsBody`
|
||||
(:1556) — i.e. this is throw-on-conflict exclusivity (Option-1 flavor), not
|
||||
epoch/token gating. For a LOCAL PLAYER record this path should never fire
|
||||
(remote motion is for non-local entities) but the guard is defense-in-depth
|
||||
and IS one of the explicit gate checks
|
||||
(`!activation.Record.RemoteMotionBindingInProgress`/`RemoteMotion is null`)
|
||||
the dormant local-publication mechanism re-validates at every stage
|
||||
(section 4).
|
||||
- **Projectile binding**: `RuntimeProjectilePhysicsUpdater.cs` similarly only
|
||||
READS `record.PhysicsBody` (lines 447, 459, `ReferenceEquals` currency
|
||||
checks). The writer is `RuntimePhysicsState.BindProjectile`
|
||||
(`RuntimePhysicsState.cs:1309-1382`, full read) — same throw-on-conflict
|
||||
shape: binding-in-progress (1343-1347), body-mismatch on rebind
|
||||
(1330-1337), must-already-own-canonical-body-before-binding
|
||||
(1348-1352, "projectile must borrow its canonical physics body" — i.e.
|
||||
UNLIKE remote motion, `BindProjectile` requires `record.PhysicsBody` to
|
||||
ALREADY be non-null and matching BEFORE it will bind — it never calls
|
||||
`InitializeNewPhysicsBody`/`SetPhysicsBody` itself for a first-time body;
|
||||
something else (route 5's `ProjectileController.TryBind`,
|
||||
`ProjectileController.cs:176-265` per the route inventory) must construct
|
||||
the body ad hoc first via a DIFFERENT path than `GetOrCreatePhysicsBody`
|
||||
— worth flagging: **this is a 7th, App-side, ad hoc body-construction site
|
||||
not funneled through any of the 6 canonical writer methods** — App's
|
||||
`ProjectileController.TryBind` constructs a body and must be setting it onto
|
||||
the record through some other route (not confirmed by this pass; App-side
|
||||
`ProjectileController.cs` was not read in full — flag as open item, but it
|
||||
is explicitly OUT of C1's local-player scope per the campaign handoff's
|
||||
gate list item "remote and projectile binding/update" being about
|
||||
*interaction with* the local-player transaction, not projectile's own
|
||||
authority).
|
||||
- **`RuntimeSetPositionState.SubmitPreparedPlacement`** (`RuntimeSetPositionState.cs:2224-2274`,
|
||||
full read): requires `operation.Record.PhysicsBody is not { } body` to
|
||||
already be true (line 2254) — i.e. EVERY non-initial-construction
|
||||
SetPosition submission (ForcePosition, portal, remote Position, projectile
|
||||
correction) requires a body to already exist on the record, confirming the
|
||||
6 writer sites in section 1 are the exhaustive set of "who can put the
|
||||
FIRST body on a record." For the local player specifically, only
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.Commit` (section 4) does this
|
||||
today (dormant, unwired); in PRODUCTION, no writer ever populates
|
||||
`RuntimeEntityRecord.PhysicsBody` for either host's local player (section
|
||||
2 finding) — meaning `SubmitPreparedPlacement` would reject a local-player
|
||||
submission in production today, which is consistent with the route
|
||||
inventory's finding that Route 2 (ForcePosition) and Route 3 (portal) both
|
||||
bypass `RuntimeSetPositionState` entirely via their own duplicate
|
||||
authorities instead.
|
||||
- **`RuntimeSetPositionState.PrepareDormantLocalActivationOwnership`**: see
|
||||
section 4 — the ONE place the local-player-specific dormant body attach
|
||||
happens; requires a pre-opened `Operation` in stage `AwaitingPreparation`
|
||||
from `TryBeginExclusiveAuthoredPlacement`.
|
||||
- **Object-clock epoch transitions**
|
||||
(`RuntimeEntityRecord.SuspendObjectClock`/`ResetObjectClockForEnterWorld`,
|
||||
both `internal`, bump `ObjectClockEpoch`): full call-site grep found BOTH
|
||||
the expected `RuntimeEntityDirectory` wrapper call sites (which run
|
||||
`EnsureKnown(record)` first, `RuntimeEntityDirectory.cs:311-321`) AND
|
||||
**direct unwrapped calls from `src/AcDream.App/World/LiveEntityRuntime.cs`
|
||||
at lines 879, 891, 897, 1258, 3030, 3033** — App calls
|
||||
`record.SuspendObjectClock()`/`record.ResetObjectClockForEnterWorld(...)`
|
||||
straight on the `RuntimeEntityRecord` (accessible because these are
|
||||
`internal` and `AcDream.App` has `InternalsVisibleTo`), bypassing the
|
||||
`RuntimeEntityDirectory` facade's `EnsureKnown` check entirely. This is
|
||||
inside `LiveEntityRuntime`'s `RebucketLiveEntity`-family code (comment
|
||||
references `prepare_to_enter_world`/retail `update_object`'s parent
|
||||
early-out — matches the already-known route-1/prerequisite-D
|
||||
`RebucketLiveEntity` duplicate authority). **Previously-unstated
|
||||
implication for C1**: the SAME record whose `ObjectClockEpoch` the dormant
|
||||
publication mechanism gates on can have its epoch bumped by this
|
||||
direct-call path DURING the window between
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.Prepare` and `.Commit()`/
|
||||
`.CommitActivation()` if `RebucketLiveEntity` runs concurrently for the
|
||||
SAME entity (e.g. a second CreateObject/Position causing a re-rebucket
|
||||
mid-construction) — the epoch check (`IsCurrent`/`IsActivationOwnershipEnvelopeCurrent`
|
||||
comparing `record.ObjectClockEpoch == token.ObjectClockEpoch`) WOULD catch
|
||||
and reject this correctly (fail-safe), but it confirms the gate is load-
|
||||
bearing against a REAL, already-existing production writer, not a
|
||||
hypothetical.
|
||||
- **Deletion/teardown**: `RuntimeEntityObjectLifetime.TryAcceptDelete`
|
||||
(`RuntimeEntityObjectLifetime.cs:1555+`) calls `Entities.TryDelete` then
|
||||
`Entities.RemoveActive(active)` (1587) — this IMMEDIATELY flips
|
||||
`Entities.IsCurrent(record)` to `false` for that record (removes it from
|
||||
the active-by-guid table), which is the single check
|
||||
`CanPrepare`/`IsCurrent`/`IsActivationCurrent`/every gate in section 4
|
||||
depends on — so a delete landing at any point rejects the in-flight
|
||||
publication transaction on its NEXT check. Full body clear happens later
|
||||
in `RuntimeEntityObjectLifetime.CompleteProjectionRetirement`
|
||||
(:745-771, called from `RetireCanonicalOnly`/the graphical teardown-ack
|
||||
path): `Entities.SetPhysicsBody(canonical, null)` (:766) after
|
||||
`Physics.SetPosition.Forget(canonical, releasePreparedMover: true)` (:753,
|
||||
cancels any in-flight ordinary placement) and
|
||||
`ForgetInitialCreateResidence(canonical)` (:751, cancels any in-flight
|
||||
residence lease) — i.e. deletion cancels BOTH placement-lease families
|
||||
before clearing the body, consistent with prerequisite E's "quiesce before
|
||||
demote" discipline (though for landblock collision, not entity teardown —
|
||||
the pattern rhymes).
|
||||
- **`RuntimePhysicsState` per-frame body access**: `RuntimeOrdinaryPhysicsUpdater.cs`,
|
||||
`RuntimeRemotePhysicsUpdater.cs`, `RuntimeProjectilePhysicsUpdater.cs` each
|
||||
gate their per-tick work on `record.PhysicsBody is not { } body` /
|
||||
`ReferenceEquals(record.PhysicsBody, body)` currency checks (grep-confirmed,
|
||||
e.g. `RuntimeOrdinaryPhysicsUpdater.cs:68,284`) — read-only w.r.t. the
|
||||
`PhysicsBody` reference itself (they mutate the BODY's internal fields
|
||||
every tick, which is expected/normal simulation, not an ownership-slot
|
||||
write). No workset iterates and calls `SetPhysicsBody`. `RuntimePhysicsState.cs`
|
||||
itself has only two visible "workset" mentions (`ClearSpatialWorksets` at
|
||||
:1951, a doc-comment at :1307) — the ordinary/remote/projectile worksets
|
||||
live in their respective `RuntimeXPhysicsUpdater` files, out of this pass's
|
||||
read budget beyond the grep-confirmed read-only currency pattern above.
|
||||
|
||||
---
|
||||
|
||||
## 4. `RuntimeSetPositionState.PrepareDormantLocalActivationOwnership` — nucleus or dead end?
|
||||
|
||||
**Definition** (`RuntimeSetPositionState.cs:1026-1052`, full read):
|
||||
```csharp
|
||||
internal void PrepareDormantLocalActivationOwnership(
|
||||
RuntimeEntityRecord record, PhysicsBody body,
|
||||
in RuntimeEntityPlacementToken token)
|
||||
{
|
||||
...
|
||||
if (!token.IsValid
|
||||
|| record.Key != token.Entity
|
||||
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|
||||
|| operation.Token != token
|
||||
|| operation.Stage is not RuntimeEntityPlacementStage.AwaitingPreparation
|
||||
|| !ReferenceEquals(operation.Record, record)
|
||||
|| record.PhysicsBody is not null // <- record must have NO body yet
|
||||
|| !IsCurrent(operation)
|
||||
|| body.InWorld
|
||||
|| (body.TransientState & TransientStateFlags.Active) != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dormant local activation must bind to the exact current placement owner.");
|
||||
}
|
||||
operation.Body = body;
|
||||
operation.DormantLocalActivation = true;
|
||||
}
|
||||
```
|
||||
It THROWS (does not return a status) on any invariant violation — by design a
|
||||
"this should be structurally impossible if the caller validated first"
|
||||
assertion, not a retryable rejection. It requires a PRE-EXISTING placement
|
||||
`Operation` already opened via `TryBeginExclusiveAuthoredPlacement`
|
||||
(`RuntimeSetPositionState.cs:1004-1024`) in stage `AwaitingPreparation` — i.e.
|
||||
it is NOT a standalone entry point; it is ONE STEP inside a larger chain that
|
||||
also needs prerequisite B's mover-preparation authority
|
||||
(`IsExactPreparedPlacementCurrent`, `RuntimeSetPositionState.cs:1318-1340`)
|
||||
satisfied for the SAME token/command before
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.CanPrepare` will even call it.
|
||||
|
||||
**What it was built for**: it is called from exactly ONE place in the whole
|
||||
repo — `RuntimeLocalPlayerPhysicsPublicationState.Commit`
|
||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:394-397`), as the "seal the
|
||||
exact SetPosition owner before the irreversible no-fail suffix" step,
|
||||
immediately before `candidate.Controller.CommitRuntimeOwnership(...)` and
|
||||
`candidate.Record.SetPhysicsBody(candidate.Body)`. It exists purely to make
|
||||
the PLACEMENT OPERATION (owned by `RuntimeSetPositionState`) and the BODY
|
||||
(owned by `RuntimeEntityRecord`) become mutually aware atomically, so that
|
||||
the SAME operation can later be walked through the full retail SetPosition
|
||||
staged commit (ground phase -> collision dispatch -> response -> final
|
||||
commit) via `TryEvaluateDormantLocalActivation` ->
|
||||
`TryPrepareDormantLocalActivationCommit` ->
|
||||
`TryApplyDormantLocalActivationCommit` ->
|
||||
`TryPrepareDormantLocalActivationFinalCommit` ->
|
||||
`TryApplyDormantLocalActivationFinalCommit`
|
||||
(`RuntimeSetPositionState.cs:2025-2077`, full read of the final-commit
|
||||
method) — the LAST of which is where `_entities.SetFullCell`,
|
||||
`_entities.AdvancePlacementCommit`, `body.InWorld = true`,
|
||||
`_entities.SetPhysicsHost`, `controller.CommitRuntimeActivationFrame()`,
|
||||
`_physics.Engine.UpdatePlayerCurrCell`, `_physics.AcknowledgeSpatialProjection`,
|
||||
`_entities.ResetObjectClockForEnterWorld` (object-clock epoch bump, task 3),
|
||||
and `controller.ActivateRuntimePublication()` (controller goes LIVE) ALL
|
||||
happen in one synchronous, no-branch-for-failure block (:2025-2077), gated
|
||||
immediately before by `IsDormantLocalActivationPrephaseCurrent`/re-validated
|
||||
epoch checks. **This is genuinely the full retail SetPosition commit,
|
||||
already ported, already wired to the same body/controller the dormant
|
||||
publication candidate built.**
|
||||
|
||||
**Verdict: NUCLEUS, not a dead end** — but it is only ONE LOAD-BEARING STEP
|
||||
inside a much larger, ALREADY-COMPLETE mechanism:
|
||||
`RuntimeLocalPlayerPhysicsPublicationState` (1033 lines,
|
||||
`src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs`)
|
||||
+ its ~15 `RuntimeSetPositionState` dormant-activation methods. Constructed
|
||||
once at `GameRuntime.cs:261` and exposed via
|
||||
`RuntimeLocalPlayerMovementState.PhysicsPublication` (:59-61, itself
|
||||
`internal`, throws if unbound). **Confirmed by exhaustive grep: ZERO
|
||||
production callers in `src/AcDream.App/` or `src/AcDream.Headless/`** — the
|
||||
only callers anywhere are `tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs`.
|
||||
This is, functionally, **Option 2 from the rejected-prototype note ("Off-
|
||||
canonical preparation followed by one validated atomic Runtime commit that
|
||||
publishes the prepared controller/body relationship without copying stale
|
||||
state over newer authority") already built end to end** — complete with:
|
||||
- a `Prepare`/`Commit`/`Discard` triad for the BODY/CONTROLLER pair
|
||||
(analogous to, and reusing, the SAME token-epoch pattern as
|
||||
`RuntimeSetPositionState`'s ordinary placement operations);
|
||||
- a SEPARATE `EvaluateActivation`/`CommitActivation`/`DiscardActivation` triad
|
||||
for actually driving the body through retail SetPosition's staged commit
|
||||
once the body/controller pair is sealed;
|
||||
- re-validation of `PhysicsOwnershipEpoch`, `ObjectClockEpoch`,
|
||||
`ControllerOwnershipEpoch`, `SessionLifetimeVersion`, identity
|
||||
`ServerGuid`+`Revision`, and null/in-progress state for RemoteMotion/
|
||||
Projectile/PhysicsHost/DeleteAcceptedForTeardown at EVERY external entry
|
||||
point (`CanPrepare`, `IsCurrent`, `IsActivationCurrent`,
|
||||
`IsActivationOwnershipEnvelopeCurrent`,
|
||||
`IsCommittedActivationSuffixCurrent`) — this IS the reentrancy defense the
|
||||
rejected snapshot-lease prototype explicitly lacked (see section 6).
|
||||
|
||||
**What is genuinely missing (the real C1 work), given this mechanism already
|
||||
exists:**
|
||||
1. Nobody calls `TryBeginExclusiveAuthoredPlacement` + prerequisite B's
|
||||
`PrepareMover`/`ReadSetupCollision` chain + `PhysicsPublication.Prepare`/
|
||||
`Commit`/`EvaluateActivation`/`CommitActivation` from either host — this IS
|
||||
the wiring gap, exactly like every other route in the cutover.
|
||||
2. **The public `RuntimeLocalPlayerMovementState.Controller` setter
|
||||
(`RuntimeLocalPlayerMovementState.cs:37-50`) remains a live, unguarded
|
||||
escape hatch** — both `PlayerModeController.BuildControllerAndCamera:486`
|
||||
and `HeadlessSessionWorldProjection.CreateController:653` write it
|
||||
directly today, and NOTHING stops either host from continuing to do so
|
||||
even after C1 wires the dormant mechanism, unless that direct-write path
|
||||
is deleted/sealed off (e.g. made `internal` to only
|
||||
`RuntimeLocalPlayerPhysicsPublicationState`/`CommitRuntimeOwnedController`).
|
||||
The setter has NO epoch/token check on write (`CanCommitRuntimeOwnedController`
|
||||
is a SEPARATE, unused-by-the-setter validation method) — it will happily
|
||||
accept a second unguarded assignment even while a dormant activation is
|
||||
in flight, silently retiring whatever the dormant mechanism just
|
||||
published (`_controller?.RetireRuntimePublication()` at :45, which is a
|
||||
real no-op for anything not currently `RuntimeOwnedDormant`/
|
||||
`RuntimePublished` — see section 6 gate 7). **This is the single most
|
||||
important pre-existing defect C1 must close: the "exclusive" adjective in
|
||||
prerequisite C's "one Runtime-owned exclusive/versioned...transaction"
|
||||
is not yet true while this direct setter remains reachable from hosts.**
|
||||
3. Neither `new PlayerMovementController(physics, objectClock, options)`
|
||||
(public ctor, `StandalonePublished`) call site in the two hosts has been
|
||||
swapped for `PlayerMovementController.CreatePublicationCandidate` — until
|
||||
that swap happens, controllers built by either host never enter the
|
||||
`CandidatePreparing/CandidateSealed/RuntimeOwnedDormant/RuntimePublished`
|
||||
lifecycle the dormant mechanism's gates all key off of.
|
||||
|
||||
---
|
||||
|
||||
## 5. `PlayerMovementController` construction requirements
|
||||
|
||||
Constructor needs (from both direct-ctor call sites AND
|
||||
`CreatePublicationCandidate`, `PlayerMovementController.cs:617-690`):
|
||||
- `PhysicsEngine physics` (shared engine reference, both hosts pass their own
|
||||
`RuntimePhysicsState`/`_runtime.EntityObjects.Physics.Engine`).
|
||||
- `RetailObjectQuantumClock? objectClock` — App passes `playerRecord.ObjectClock`
|
||||
(the CANONICAL record's clock, `RuntimeEntityRecord.ObjectClock` at
|
||||
`RuntimeEntityRecord.cs:62`, always non-null per its field initializer);
|
||||
headless passes `record.ObjectClock` identically. The dormant mechanism's
|
||||
`CreatePublicationCandidate` instead passes a THROWAWAY
|
||||
`new RetailObjectQuantumClock()` (`PlayerMovementController.cs:687-688`) at
|
||||
construction time and only swaps in the REAL
|
||||
`candidate.Record.ObjectClock` later, inside `Commit`, via
|
||||
`controller.CommitRuntimeOwnership(candidate.Record.ObjectClock)`
|
||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:403-404` ->
|
||||
`PlayerMovementController.cs:774-786`) — i.e. the dormant candidate is
|
||||
built against a SCRATCH clock so construction can never observe or mutate
|
||||
the canonical record's real clock before the atomic commit swaps it in.
|
||||
This is exactly the "off-canonical preparation" half of Option 2.
|
||||
- `PlayerMovementConstructionOptions` (RunSkill/JumpSkill) — both hosts build
|
||||
via `PlayerMovementConstructionOptions.From(<a RuntimeMovementSkillState
|
||||
snapshot>)`; the dormant mechanism's `Prepare` also takes this as a caller-
|
||||
supplied parameter (`Prepare(..., PlayerMovementConstructionOptions
|
||||
options, ...)`, :191) — no divergence in shape, only in WHERE the skill
|
||||
snapshot is read from (App's local `_skills` field vs. headless's
|
||||
`_runtime.CharacterOwner.MovementSkills` vs. the dormant mechanism taking
|
||||
it as a caller parameter either way).
|
||||
|
||||
What construction MUTATES (beyond the private `_body`): `LocalEntityId`,
|
||||
`StepUpHeight`/`StepDownHeight` (Setup-derived), `SphereList` (Setup-derived,
|
||||
prerequisite B territory), `ObjectScale`, initial position/orientation via
|
||||
`PreparePositionForCommit`/`SetBodyOrientation`, physics state via
|
||||
`ApplyPhysicsState`, `MoveToFactory`/`PositionManager`
|
||||
(`MovementManager`/`MotionInterpreter` wiring). ALL of this is exactly what
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.Prepare`
|
||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:187-355`) already does against
|
||||
its private `CreatePublicationCandidate`-built controller, reading
|
||||
`command.Physics.StepUpHeight/StepDownHeight/Spheres/Scale/Position/CellId/
|
||||
CellLocalPosition/Orientation` from the CALLER-SUPPLIED
|
||||
`RuntimeSetPositionCommand` (i.e. the command already carries everything
|
||||
prerequisite B's mover-preparation chain produces) rather than reaching into
|
||||
DAT/prepared-collision itself.
|
||||
|
||||
**What an "off-canonical preparation followed by one validated atomic commit"
|
||||
must DEFER** (confirmed by the dormant mechanism's own design, section 4):
|
||||
- The record's REAL `ObjectClock` (use a scratch clock during prep).
|
||||
- `RuntimeEntityRecord.PhysicsBody`/`PhysicsOwnershipEpoch` (never touch the
|
||||
canonical record during prep; only `SetPhysicsBody` inside `Commit`, and
|
||||
only after `PrepareDormantLocalActivationOwnership` succeeds).
|
||||
- `RuntimeLocalPlayerMovementState.Controller`/`ControllerOwnershipEpoch`
|
||||
(only via `CommitRuntimeOwnedController`, never the public setter, during
|
||||
prep).
|
||||
- `body.InWorld`/`TransientState.Active` (explicitly forced false during prep,
|
||||
`Prepare`, :292-293) — the body must not be simulatable until the LATER
|
||||
activation commit flips it (`TryApplyDormantLocalActivationFinalCommit`,
|
||||
`body.InWorld = true` at :2056).
|
||||
- World-residence/host/shadow/camera publication (all deferred to the
|
||||
activation phase / presentation-observer layer, never inside `Prepare`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Adversarial gate list — exact code paths that would race TODAY
|
||||
|
||||
(Campaign handoff's list, `docs/research/2026-07-31-remaining-physics-campaign-handoff.md:210-221`.)
|
||||
For each: what the ALREADY-BUILT dormant mechanism does (if wired) vs. what
|
||||
the CURRENT production direct-construction paths do (today, unwired).
|
||||
|
||||
1. **Nested construction** — Dormant: `CanPrepare` requires `_activation is
|
||||
null` AND `record.PhysicsBody is null` AND `_movement.Controller is null`
|
||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:862-889`) — a second
|
||||
`Prepare` while one is in flight is REJECTED structurally. Today: NEITHER
|
||||
`BuildControllerAndCamera` NOR `CreateController` has any such guard —
|
||||
`CreateController`'s `_runtime.MovementOwner.Controller ?? CreateController(record)`
|
||||
(`HeadlessSessionWorldProjection.cs:576-578`) is a bare null-coalesce, not
|
||||
an atomic test-and-set; only single-threaded host-loop scheduling
|
||||
prevents an actual race today.
|
||||
2. **Reentrant SetPosition** — Dormant: every gate re-checks
|
||||
`PhysicsOwnershipEpoch`/`record.PositionAuthorityVersion` currency.
|
||||
Today: `BuildControllerAndCamera`'s final mutation
|
||||
(`playerEntity.SetPosition`/`ParentCellId`/`CommitPreparedPosition`,
|
||||
PlayerModeController.cs:482-484) has zero epoch check — a same-thread
|
||||
reentrant call (e.g. from a nested wire dispatch) would silently
|
||||
clobber with no detection.
|
||||
3. **Remote and projectile binding/update** — Dormant: `CanPrepare`/
|
||||
`IsCurrent`/`IsActivationCurrent` all check `record.RemoteMotion is null`,
|
||||
`record.Projectile is null`, `!RemoteMotionBindingInProgress`,
|
||||
`!ProjectileBindingInProgress` (defense-in-depth; should never legitimately
|
||||
fire for a local-player record). Today: no such check exists in either
|
||||
host's direct construction path.
|
||||
4. **Deletion and same-GUID new incarnation** — Dormant: `_entities.IsCurrent(record)`
|
||||
checked at every gate; `TryAcceptDelete` -> `RemoveActive` flips this
|
||||
immediately (section 3). Today: `BuildControllerAndCamera`/`CreateController`
|
||||
take a fixed `RuntimeEntityRecord`/`WorldEntity` parameter with NO
|
||||
re-validation against current canonical identity at the final commit.
|
||||
5. **Projection-owner replacement** — Dormant: `_entities.SessionLifetimeVersion
|
||||
== token.SessionGenerationAuthority` checked throughout. Today: no
|
||||
generation check in either direct path.
|
||||
6. **Object-clock epoch change** — Dormant: `ObjectClockEpoch` compared at
|
||||
every gate (section 3/4). Today: no check; AND there is a REAL, live
|
||||
concurrent writer already in production —
|
||||
`LiveEntityRuntime.cs:879/891/897/1258/3030/3033`'s direct
|
||||
`record.SuspendObjectClock()`/`ResetObjectClockForEnterWorld(...)` calls
|
||||
inside the `RebucketLiveEntity` family (section 3) — this is not a
|
||||
hypothetical gate, it is a currently-active call path on the SAME record
|
||||
type.
|
||||
7. **Reset and disposal** — Dormant: `ResetSession()`/`Dispose()` on
|
||||
`RuntimeLocalPlayerMovementState` explicitly cascade into
|
||||
`_physicsPublication?.ResetSession()`/`Dispose()`
|
||||
(`RuntimeLocalPlayerMovementState.cs:244-295`), which tear down
|
||||
candidate/activation state via `ReferenceEquals`-gated clears (never
|
||||
clobbering a newer owner, `DiscardActivation`,
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.cs:1002-1032`). Today's direct-
|
||||
construction controllers are built via the PUBLIC constructor
|
||||
(`StandalonePublished` lifecycle) — **`RetireRuntimePublication()`
|
||||
(`PlayerMovementController.cs:837-846`) only transitions
|
||||
`RuntimeOwnedDormant`/`RuntimePublished` state; it is a NO-OP for
|
||||
`StandalonePublished` controllers** — a previously-unstated finding: TODAY,
|
||||
`ResetSession()`/`Dispose()`/replacing `.Controller` on either host's
|
||||
directly-built controller produces NO explicit lifecycle transition at
|
||||
all; the controller is simply dropped/GC'd. Not a visible bug today
|
||||
(nothing reads `IsRuntimePublished` for these), but it means today's
|
||||
controllers are invisible to the exact teardown bookkeeping C1's target
|
||||
mechanism relies on.
|
||||
8. **Commit and rollback after replacement** — Dormant: ALL validation
|
||||
happens before the single canonical mutation
|
||||
(`PrepareDormantLocalActivationOwnership`, which itself throws leaving
|
||||
state untouched on failure); everything after is documented as
|
||||
"callback-free, non-allocating, and cannot fail"
|
||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:391-393`) — no rollback-
|
||||
after-newer-authority path exists BECAUSE nothing after that point can
|
||||
fail by construction. Today: `BuildControllerAndCamera`'s try/catch rolls
|
||||
back camera+shadow only; the final `playerEntity.SetPosition`/
|
||||
`ParentCellId`/`CommitPreparedPosition` triad (482-484) has nothing after
|
||||
it that can throw, so it's accidentally safe today, not structurally
|
||||
guaranteed.
|
||||
9. **Late graphical camera/shadow/host failure** — Today: `BuildControllerAndCamera`
|
||||
DOES handle this (explicit `_camera.RestoreState`/`_shadow.Restore` in the
|
||||
catch block, 494-518) — this is the ONE gate the CURRENT graphical path
|
||||
already handles reasonably. The dormant Runtime-side mechanism has NO
|
||||
camera/shadow concept (presentation-independent by design) — C1 must
|
||||
layer this handling in the PRESENTATION/observer phase (post-Runtime-
|
||||
commit), matching prerequisite D's rule that a host exception must not
|
||||
roll Runtime back, only retry the FIFO head.
|
||||
10. **Late headless prepared-collision failure** — Today:
|
||||
`ApplySetupStepHeights` (`HeadlessSessionWorldProjection.cs:657-691`)
|
||||
throws a raw, uncaught `InvalidDataException` (672-675) if the prepared
|
||||
Setup collision isn't `Loaded` — this propagates up through
|
||||
`CreateController` -> `SynchronizeLocalPlayer` -> `ProjectSpawn`/
|
||||
`ProjectPosition` with NO try/catch anywhere in between (grep-confirmed
|
||||
no surrounding try/catch in `HeadlessSessionWorldProjection.cs`'s these
|
||||
methods) — a genuinely unhandled-exception risk in production headless
|
||||
TODAY, not just a hypothetical C1 gate.
|
||||
|
||||
---
|
||||
|
||||
## Summary for the C1 contract
|
||||
|
||||
- **Writer count**: 6 confirmed call sites of `RuntimeEntityRecord.SetPhysicsBody`
|
||||
across 3 files (`RuntimeEntityDirectory.cs:359`,
|
||||
`RuntimeEntityObjectLifetime.cs:766`,
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.cs:405,1025`,
|
||||
`RuntimePhysicsState.cs:1558,1666`) — plus a probable 7th App-side ad hoc
|
||||
projectile body-construction site not yet traced to a canonical writer
|
||||
(flagged, out of local-player scope).
|
||||
- The dormant `RuntimeLocalPlayerPhysicsPublicationState` +
|
||||
`RuntimeSetPositionState`'s ~15 dormant-activation methods already
|
||||
implement essentially the COMPLETE Option 2 transaction (off-canonical
|
||||
prepare against a scratch clock/sealed candidate controller, single
|
||||
validated atomic commit, full retail-staged SetPosition activation) with
|
||||
epoch/token/generation/identity re-validation at every external entry
|
||||
point — it has ZERO production callers in either host.
|
||||
- The single largest remaining defect even AFTER wiring: the public
|
||||
`RuntimeLocalPlayerMovementState.Controller` setter is an unguarded escape
|
||||
hatch both hosts currently use directly; it must be sealed (made
|
||||
unreachable from hosts, or itself epoch-gated) for the word "exclusive" in
|
||||
prerequisite C to be true.
|
||||
Loading…
Add table
Add a link
Reference in a new issue