fix(physics): restore presentation when a park is cancelled (#312)
Regression from 7f1c1f5a (C4 route 4b-2). A remote player who recalled in,
arrived, and stood still was permanently absent from the world render AND the
radar while remaining fully simulated — 71 healthy physics ticks with contact
and walkable, interpolation enqueues, equipment attached, chat visible.
Route 4b-2 is the first commit that lets an ordinary remote UpdatePosition open
a canonical SetPosition. A park publishes a synchronous Withdraw that tears down
presentation registrations; only TryPublishPlace restores them.
RestoreParkWithdrawal — added in the same slice — restores InWorld, the object
clock, and canonical residency, i.e. the Runtime half only. Eight Opus reviews
verified those three fields and the tests asserted exactly them, so the suite
stayed green while the entity was invisible.
Why it is intermittent: the presentation half IS restored incidentally by the
per-packet prologue rebucket for a MOVING remote. It only sticks when the
entity parks on its FINAL accepted Position and then goes idle, because ACE
stops broadcasting for a stationary entity, so no later packet arrives to
re-publish it and nothing else re-drives.
The fix publishes a RuntimePlacementProjectionKind.WithdrawalRestored receipt on
the one ordered placement stream, acknowledge-only in Runtime (the parked
operation is already retired by CancelCoreDeferred), which the App sink maps to
the exact inverse of its own TryPublishWithdrawal: the projection half (bucket,
IsSpatiallyProjected, IsSpatiallyVisible, spatial indexes, RefreshPresentation)
plus the publish half (_worldState, _worldEvents, _effectPoses,
_localPlayerShadow, visibility sinks). Applied with commitPose: false, because
the withdrawal never moved the sidecar; a test feeds a deliberately wrong
position to pin that.
Two alternatives were refuted on measurement, not preference. Routing the
restore's SetFullCell through CommitCanonicalCell cannot fire on the shipped
remote path at all — the prologue rebucket has already recommitted a non-zero
FullCellId before the merge cancels the park, so no cell edge remains — and it
never touches the publish half regardless. Extending RestoreParkWithdrawal
directly reduces to the same receipt, since Runtime must not reach behind the
host sink.
Gated on the entity ending the rollback canonically whole (FullCellId != 0 &&
InWorld) rather than on residencyRestored, which is false on the shipped remote
path and would have made the fix a no-op. AP-136's quiescing-prefix refusal arm
is preserved: no receipt, entity stays withdrawn.
Corrects my own framing of the defect: _worldState/_worldEvents/_effectPoses are
lost but are NOT what kills render and radar (_worldState is the plugin
IGameState; _effectPoses is the pose registry, not entity.MeshRefs). The
load-bearing casualties are the visibility sinks and the
IsSpatiallyProjected/IsSpatiallyVisible + bucket removal that gates the radar.
Register: AD-63 filed (selection deliberately not restored — user intent),
AP-136 amended (its "restored visible" claim covered only the canonical half;
the gap was a defect, not a divergence). ShadowObjectRegistry.Suspend stays
out of scope per AP-136.
Seven-revert discrimination table including one that proves the test is not
merely re-checking the bucket. Suite 11,023 passed / 4 skipped / 0 failed.
Live gate is user-run and folds into #309: two clients, ACDREAM_PROBE_PARK=1,
recall a remote in and let it stand still; acceptance is
[park-restore] ... presentation=True for that guid plus a visible model and a
radar blip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
204d0ae047
commit
b1f914d508
10 changed files with 1086 additions and 12 deletions
|
|
@ -228,6 +228,68 @@ task scope held `RuntimeSetPositionState.cs` off-limits for that session
|
|||
**Acceptance:** A headless tick with N>0 outstanding placement receipts does
|
||||
not allocate a new array per tick.
|
||||
|
||||
## #312 — Cancelled park restored Runtime state but never the presentation half
|
||||
|
||||
**Status:** DONE (fixed 2026-08-04; awaiting the two-client visual gate below)
|
||||
**Severity:** HIGH (a remote player permanently invisible in world and radar)
|
||||
**Filed:** 2026-08-04
|
||||
**Component:** physics / placement / presentation
|
||||
|
||||
**Description:** A remote player that recalled into the observer's location was
|
||||
absent from both the 3-D world and the radar while remaining fully simulated —
|
||||
physics ticking, equipment attached, chat and spellcasting visible. It never
|
||||
recovered: not on remote movement, not on the observer walking away and back.
|
||||
Intermittent (it did not reproduce on the next recall).
|
||||
|
||||
**Root cause:** `ParkDeferred` publishes a `Withdraw` receipt with two halves.
|
||||
Runtime owns the canonical half (`InWorld`, transient bits, object clock,
|
||||
residency, spatial root) and `RestoreParkWithdrawal` rolls it back on cancel.
|
||||
The PRESENTATION half — the graphical bucket, `IsSpatiallyProjected` /
|
||||
`IsSpatiallyVisible`, the projection-visibility sinks (which drive
|
||||
`EntitySpawnAdapter.SetPresentationResident`, i.e. the WB draw registry), plugin
|
||||
world state and events, the effect-pose registry, the local-player shadow — is
|
||||
performed by the host sink, and its only mirror image was a later `Place`
|
||||
receipt. The per-packet prologue `RebucketLiveEntity` masked the hole for a
|
||||
MOVING remote; a remote that parks on its FINAL accepted Position and then goes
|
||||
idle never gets another packet, because ACE stops broadcasting for a stationary
|
||||
entity. That is the intermittency and the "never recovers".
|
||||
|
||||
Introduced by `7f1c1f5a` (C4 route 4b-2), the first commit that lets an ordinary
|
||||
remote `UpdatePosition` open a canonical `SetPosition` and therefore reach a
|
||||
restorable park. H1 (a latched `PhysicsStateFlags.Hidden`) was refuted: the
|
||||
failing entity's 71 `[remote-slide-tick]` lines come from the ordinary remote
|
||||
`Tick`, not the hidden-only loop.
|
||||
|
||||
**Fix:** a new `RuntimePlacementProjectionKind.WithdrawalRestored` receipt,
|
||||
published by `RestoreParkWithdrawal` on the one ordered placement stream exactly
|
||||
when the entity ends the rollback canonically whole. It is acknowledge-only in
|
||||
Runtime (the parked operation is already retired), and the host sink maps it to
|
||||
the exact inverse of its own withdrawal. Routing the restore's `SetFullCell`
|
||||
through `CommitCanonicalCell` was considered and rejected on measurement: the
|
||||
`CellCommitted` -> `RebucketLiveEntity` recovery it would fire never touches
|
||||
plugin world state, the world-event stream, or the effect-pose registry, and it
|
||||
cannot fire at all on the shipped remote path, where the prologue rebucket has
|
||||
already recommitted a non-zero `FullCellId` before the merge cancels the park.
|
||||
|
||||
**Files:** `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs`
|
||||
(`RuntimePlacementProjectionKind`, `PublishWithdrawalRestoration`,
|
||||
`RestoreParkWithdrawal`, `AcknowledgeProjection`);
|
||||
`src/AcDream.App/World/RuntimePlacementPresentationSink.cs`
|
||||
(`TryApplyWithdrawalRestoration`); `src/AcDream.App/World/LiveEntityRuntime.cs`
|
||||
(`TryApplyRuntimePlacementProjection`, `TryApplyRuntimePlacementPlace`'s
|
||||
`commitPose`); `src/AcDream.Headless/Hosting/HeadlessRuntimePlacementProjectionSink.cs`.
|
||||
|
||||
**Research:**
|
||||
[`2026-08-04-invisible-recalled-remote-diagnosis.md`](research/2026-08-04-invisible-recalled-remote-diagnosis.md).
|
||||
Register: AP-136 amended, AD-63 filed (selection is not re-established).
|
||||
|
||||
**Acceptance:** `WithdrawalRestored_ReinstatesEveryPresentationRegistrationTheWithdrawalRemoved`
|
||||
(App.Tests) and `CancellingWakeableParkPublishesTheWithdrawalRestorationReceipt`
|
||||
(Runtime.Tests) both fail with the fix reverted. Live gate, folded into #309's
|
||||
`ACDREAM_PROBE_PARK=1` two-client run: recall a remote into the observer, let it
|
||||
STAND STILL, and confirm it renders and blips; `[park-restore]` must report
|
||||
`presentation=True` for that guid.
|
||||
|
||||
## Recent-regression cleanup — 2026-08-03
|
||||
|
||||
Plan: [`2026-08-03-recent-regression-cleanup.md`](plans/2026-08-03-recent-regression-cleanup.md).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue