fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival. Portalling worked before this change and works after it — this is not a bug fix, EXCEPT that it found and fixed one dead-code production bug. THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the accepted destination at Place time, but TryBeginPortalReveal already consumes that slot at Aim time — so the arm was 100% dead code and every real portal Place refused with host-token-unavailable. Found only because we refused to accept 7 skipped tests instead of chasing the count to zero. RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING: SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with flags 0x1012, followed by PlayerPositionUpdated. BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed here (ConstrainTo @0x0045418A) and velocity is zeroed (set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook runs AFTER placement (@0x004538AE). THE THREE-ROUND DEFECT CHAIN, HONESTLY: - Round 1 released the player at the pre-teleport position while the anim stream marched on — the contract wrongly assumed Place re-fires (process rule 1's third occurrence this campaign). - Round 2's fix inferred commit from a global PendingCount, which three non-committing paths also clear — making the SAME bug complete cleanly and silently. Strictly worse than round 1: round 1 at least tripped portal-complete-before-materialized. - Round 3 latches the commit where it actually happens (ReconcileAndAcknowledgePortal), keyed on reveal generation and teleport sequence, via TryConsumePortalCommit. Two of the three required regression tests landed and are sabotage-verified on both hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted / HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted). The third (force-arm-takes-the-slot) was judged unnecessary on review: with the inference gone, PendingCount is only a "don't ask yet" guard at both gates, so a force operation occupying or vacating the slot no longer changes an input the commit decision reads — the case collapses into what the landed test already discriminates. THE B2/P3 RESOLUTION: both round-2 reviews were right about different branches of the same synchronous call. RuntimePlacementProjectionSubscription .OnPlacement acknowledges the FIFO head only when TryApply returns true; a Place whose portal authority went stale (transit ended/superseded while parked) used to return false, wedging every later entity's placement receipt behind it forever. Both sinks (RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink) now acknowledge-and-ignore a stale-authority Place instead of refusing it. The regression test (RuntimePlacementPresentationSinkTests .PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had been asserting the old, wrong `false` behaviour; it now asserts and sabotage-verifies the fix. Also lands: AP-144 (register discipline — the portal movement-event send reuses the stricter UsePositionFromServer gate where retail's SendMovementEvent is the looser autonomy_level != 0 test, diverging only at level 1, currently unreachable), AP-145 + issue #318 (the local-player collision-shadow presentation write bypasses its own publisher's ShadowObjects write via a direct cache .Set(), self-healing only once dedup diverges — filed, not fixed, pending a composition test), AD-42 deleted (its last citation retired by the canonical portal arm), AD-2 updated (the wait-cue's trigger predicate now covers a second cause), and two documentation corrections: the enter_world misattribution (both call sites are in SmartBox::HandleCreateObject, only one in the player branch — portal arrival is TeleportPlayer, not enter_world) and the stale "local player never reaches this path" comment on the generic-remote-render-pose write. Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing weakened. STILL OWED: the connected two-client gate, with ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines actually appear in the capture — and explicitly NOT scored as covering issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects directly). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd3129e9d6
commit
e0f96a55bf
24 changed files with 5261 additions and 243 deletions
|
|
@ -24,6 +24,65 @@ What does NOT go here:
|
|||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||
|
||||
## #318 — C4 route 3 §8 items 8/9/10 residual: no end-to-end composition test, no local-player shadow assertion, no T8 ordering
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (does not block round-3 acceptance per both reviewers; carried
|
||||
into C5)
|
||||
**Filed:** 2026-08-05, C4 route 3 round-3 review (retail B5/A5, architecture
|
||||
B5), carried per both reviewers' explicit conditions
|
||||
**Component:** Runtime / portal placement / local-player presentation
|
||||
|
||||
**Description:** The retail review's round-1 §3.4 premise — that
|
||||
`TryApplyRuntimePlacementPlace` does not write pose/rotation/`ParentCellId` or
|
||||
rebucket — was WRONG; round 3 verified it DOES. That closed the original
|
||||
blocking concern, but three narrower gaps remain and both reviewers agreed
|
||||
they must be tracked rather than silently dropped:
|
||||
|
||||
1. No end-to-end composition test exercises the full portal-arrival →
|
||||
canonical commit → presentation-suffix → `PhysicsEngine.ShadowObjects`
|
||||
chain for the LOCAL player specifically (existing tests cover pieces —
|
||||
the canonical commit, the presentation sink's `TryApply`, the drive
|
||||
controller — but not the full composed path with a real
|
||||
`RuntimePlacementPresentationSink` wired to a real `PhysicsEngine`).
|
||||
2. No test asserts the local-player collision SHADOW lands at the
|
||||
destination. The discriminating assertion for that future test:
|
||||
`PhysicsEngine.ShadowObjects` must hold a row at the destination cell/
|
||||
position, not just `LocalPlayerShadowState`'s internal dedup cache — see
|
||||
the register row (AP-131 amendment, filed alongside this issue) for the
|
||||
asymmetry this exposes: `LocalPlayerShadowState.Set` updates the dedup
|
||||
cache without publishing to `ShadowObjects`, self-healing only on the
|
||||
local player's first subsequent movement tick.
|
||||
3. No test proves T8's ordering — that the canonical commit's writes
|
||||
(pose/rotation/`ParentCellId`/rebucket) precede the presentation suffix's
|
||||
OWN redundant writes to the same fields, rather than racing or reversing.
|
||||
|
||||
**Root cause / status:** Not a defect — a coverage gap. The underlying
|
||||
mechanism (`RuntimePlacementPresentationSink.TryApply` →
|
||||
`LiveEntityRuntime.TryApplyRuntimePlacementProjection` →
|
||||
`TryPublishPlace` → `LocalPlayerShadowState.Set`) is correct by code reading
|
||||
and by the individual unit tests that DO exist; what's missing is the
|
||||
COMPOSED, end-to-end proof plus the specific shadow-registry assertion.
|
||||
|
||||
**Files:** `src/AcDream.App/World/RuntimePlacementPresentationSink.cs`
|
||||
(`TryPublishPlace`, `LocalPlayerShadowState.Set` call); `src/AcDream.App/Physics/LocalPlayerShadowState.cs`;
|
||||
`src/AcDream.Core/Physics/PhysicsEngine.cs` (`ShadowObjects`);
|
||||
`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs`
|
||||
(`ReconcileAndAcknowledgePortal`, the T8 probe log).
|
||||
|
||||
**Research:** `docs/research/2026-08-04-c4-route-3-contract.md` §3.4;
|
||||
`docs/research/2026-08-04-c4-route-3-retail-review-round2.md` §D (B5/A5);
|
||||
`docs/research/2026-08-04-c4-route-3-architecture-review-round2.md` B5.
|
||||
|
||||
**Acceptance:** A composition test drives a real portal arrival through the
|
||||
canonical drive controller and the real `RuntimePlacementPresentationSink`
|
||||
against a real `PhysicsEngine`, then asserts `PhysicsEngine.ShadowObjects`
|
||||
holds the local player at the destination position/cell (not merely
|
||||
`LocalPlayerShadowState`'s cache) and that the write ordering matches T8 (a
|
||||
probe or log-order assertion). Do not score the existing connected/manual
|
||||
gate as covering this — it exercises the live path but does not assert the
|
||||
shadow registry specifically.
|
||||
|
||||
## #317 — `TryCommitAuthoritativeVelocity`'s call site has no established retail basis
|
||||
|
||||
**Status:** OPEN
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -91,6 +91,34 @@ same commit) → docs/handoff commit. No workarounds; no fused slices.
|
|||
and local-player owners.
|
||||
- `RuntimePortalPlacementAuthority` has zero producing call sites; the
|
||||
adapter from `RuntimeWorldTransitState` does not exist.
|
||||
**Corrected 2026-08-04 (C4 route 3 closure,
|
||||
`docs/research/2026-08-04-c4-route-3-contract.md`), itself corrected
|
||||
2026-08-05 (A10 architecture review — the first correction asserted a
|
||||
false fact of its own), and rewritten 2026-08-05 (N5 retail-review
|
||||
round-3 fix — the prior wording of this correction contradicted
|
||||
itself).** The original bullet conflated two separate claims into one
|
||||
sentence, and only one of them was true. What pre-dated route 3 and WAS
|
||||
accurate: the `RuntimePortalPlacementAuthority` type existed (referenced
|
||||
by route 2's `Pending.Portal` field, always `Present: false`), its
|
||||
`IsValid` check existed, and the sinks' portal-authority gates plus
|
||||
`BeginAcceptedPlacementCore`'s gate already read it. What was NOT
|
||||
accurate, and is what "zero producing call sites; the adapter does not
|
||||
exist" actually described: the PRODUCER half — nothing built a
|
||||
`Present: true` authority and called the consumer arm
|
||||
(`RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival`/
|
||||
`SubmitAndResolvePortal`/`ClassifyPortalArrival`) — that consumer arm
|
||||
ALSO did not exist before route 3. Route 3 added the producer and the
|
||||
consumer together, in the same slice: the producer is
|
||||
`LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement` (now
|
||||
`TryAdvancePortalCommit`/`TryExecuteCanonicalPortalPlacementCore`, per the
|
||||
2026-08-05 A1 review fix), which builds the authority from
|
||||
`WorldRevealCoordinator`/`RuntimeWorldTransitState` facts and calls
|
||||
`TryExecuteAcceptedPortalArrival`; the identical Runtime entry point is
|
||||
shared by the headless host. So: the type/`IsValid`/consumer-gate facts
|
||||
pre-dated route 3 and were true before it; the arm (both the producer
|
||||
that builds a live authority and the consumer that reads one) did not
|
||||
exist before route 3 and is what the original bullet's "zero producing
|
||||
call sites" language was pointing at.
|
||||
- The exact-Setup mover chain (`PrepareMover` /
|
||||
`RuntimeSetPositionMoverPreparer.TryBuild` /
|
||||
`IPreparedCollisionSource.ReadSetupCollision`) exists piecewise, unwired.
|
||||
|
|
|
|||
|
|
@ -206,25 +206,56 @@ which confirms that worker completion alone is not draw readiness.
|
|||
|
||||
### 2.1. Destination placement enters the spatial cell before simulation resumes
|
||||
|
||||
> **2026-08-04 correction (C4 route 3, D-T9), itself corrected 2026-08-05
|
||||
> (R6 retail review):** the listing below attributes portal arrival to
|
||||
> `player.enter_world(destination)`. That is wrong — a caller sweep of the
|
||||
> named retail decomp
|
||||
> (`docs/research/named-retail/acclient_2013_pseudo_c.txt:93770-93828`) shows
|
||||
> both `CPhysicsObj::enter_world` call sites (pseudo-C `:93797` @0x004550EC
|
||||
> and `:93824` @0x00455095) living inside **`SmartBox::HandleCreateObject`
|
||||
> @0x00454C80** — `CObjectMaint::CreateObject` @0x00454FD8 is merely a
|
||||
> *callee* it invokes partway through, not the enclosing function the first
|
||||
> correction pass named. The two call sites are also **not both in the
|
||||
> player branch**: @0x004550EC sits in the `if (arg3 != this->player_id)`
|
||||
> NON-player branch (`PhysicsDesc::get_position` → `enter_world` for a
|
||||
> newly-created REMOTE object); only @0x00455095 sits in the player branch,
|
||||
> after `SmartBox::init_player` + `CellManager::ChangePosition`. Both sites
|
||||
> are the LOGIN/CreateObject path that creates a physics object for the
|
||||
> first time — neither is portal arrival. Portal arrival is
|
||||
> `SmartBox::TeleportPlayer` (`0x00453910`) → `CPhysicsObj::SetPositionSimple`
|
||||
> (`0x00453924`/`0x005162B0`) — confirmed by C4 route 3's own §1 citations
|
||||
> and grep at `acclient_2013_pseudo_c.txt:92514-92521`. The conclusion below
|
||||
> (commit the cell before releasing simulation) is unaffected —
|
||||
> `SetPositionSimple` reaches the identical `change_cell`/`update_object`
|
||||
> machinery this section describes — only the entry-point name and
|
||||
> pseudocode's `enter_world` call are wrong; read `SetPositionSimple(destination)`
|
||||
> wherever this section says `enter_world(destination)`.
|
||||
>
|
||||
> This routing is `SmartBox::TeleportPlayer` → `SetPositionSimple`
|
||||
> everywhere; nothing in the passages below distinguishes retail's specific
|
||||
> Recall/Lifestone/GM-teleport CAUSES, since they all funnel through the same
|
||||
> accepted-destination Position at this layer.
|
||||
|
||||
Named retail references:
|
||||
|
||||
- `CPhysicsObj::change_cell` at `0x00513390`
|
||||
- `CPhysicsObj::update_object` at `0x00515D10`
|
||||
- `CPhysicsObj::enter_world` at `0x00516170`
|
||||
- `SmartBox::TeleportPlayer` at `0x00453910`
|
||||
- `CPhysicsObj::SetPositionSimple` at `0x005162B0`
|
||||
- `CPhysicsObj::prepare_to_enter_world` at `0x00511FA0`
|
||||
- `CPhysicsObj::set_hidden` at `0x00514C60`
|
||||
|
||||
Retail does not separate an accepted destination Position from the object's
|
||||
live cell pointer. `enter_world` runs `SetPosition`, which installs the object
|
||||
in its destination `CObjCell`, before the PartArray and MovementManager
|
||||
enter-world boundaries complete. `update_object` then rejects only a parented
|
||||
object, a null `cell`, or a Frozen object; Hidden is not a reason to skip the
|
||||
live cell pointer. `SetPositionSimple` installs the object in its destination
|
||||
`CObjCell`, before the PartArray and MovementManager enter-world boundaries
|
||||
complete. `update_object` then rejects only a parented object, a null `cell`,
|
||||
or a Frozen object; Hidden is not a reason to skip the
|
||||
ScriptManager/ParticleManager tail.
|
||||
|
||||
```text
|
||||
accepted portal destination becomes ready:
|
||||
player.enter_world(destination)
|
||||
SetPosition(destination)
|
||||
SmartBox.TeleportPlayer(destination)
|
||||
SetPositionSimple(destination)
|
||||
change_cell(destination CObjCell)
|
||||
PartArray.HandleEnterWorld()
|
||||
MovementManager.HandleEnterWorld()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,411 @@
|
|||
# C4 route 3 — architecture / adversarial review, ROUND 2 (delta) — 2026-08-05
|
||||
|
||||
**Verdict: FAIL.**
|
||||
|
||||
Reviewed: the uncommitted working tree at HEAD **`cd3129e9`**, +2,570/-232 across
|
||||
16 files. Round-1 report:
|
||||
[`2026-08-04-c4-route-3-architecture-review.md`](2026-08-04-c4-route-3-architecture-review.md).
|
||||
`dotnet build AcDream.slnx -c Debug` exits 0.
|
||||
|
||||
**Round-1 findings closed: A4, A6, A9, A10(a).** A1, A2, A3 were addressed with
|
||||
real design work that is directionally right — the A1 fix in particular
|
||||
(inverting the readiness feed instead of touching the sequencer) is the correct
|
||||
architectural answer to a hard constraint, and I want that stated plainly.
|
||||
|
||||
**The FAIL is one defect, present symmetrically on both hosts, introduced by
|
||||
the A1/A3 fixes themselves:** both new "am I committed yet?" gates infer
|
||||
*commit* from *the drive controller's global pending slot being empty*. That
|
||||
slot empties on at least three paths that do **not** commit — including the one
|
||||
the drive's own doc comment names as the *expected* outcome of a park. When it
|
||||
does, the graphical controller latches `_placementCommitted = true` and the
|
||||
headless projection reports `IsCollisionReady: true`, and both hosts then march
|
||||
the full completion sequence against an unmoved body. That is round-1's A1/A3
|
||||
restored, and on the graphical side it is now *worse*, because
|
||||
`AcknowledgePortalMaterialized` succeeds where it previously failed its
|
||||
invariant.
|
||||
|
||||
Numbering continues as **B*n*** to avoid collision with round 1.
|
||||
|
||||
---
|
||||
|
||||
## MAJOR — the FAIL
|
||||
|
||||
### B1 — `PendingCount == 0` is not "committed"; both hosts infer commit from a signal that is also set by three non-committing paths
|
||||
|
||||
**Severity: MAJOR (FAIL basis). Both hosts. Uncovered by any test.**
|
||||
|
||||
Graphical, `src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:646-658`:
|
||||
|
||||
```csharp
|
||||
if (_awaitingDeferredWake)
|
||||
{
|
||||
if (_acceptedPositionDrive.PendingCount != 0)
|
||||
return false;
|
||||
_awaitingDeferredWake = false;
|
||||
_placementCommitted = true; // <-- infers commit from "not pending"
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Headless, `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:816-821`:
|
||||
|
||||
```csharp
|
||||
if (_awaitingPortalWake)
|
||||
{
|
||||
committed = _acceptedPositionDrive.PendingCount == 0;
|
||||
if (committed)
|
||||
_awaitingPortalWake = false;
|
||||
}
|
||||
```
|
||||
|
||||
`PendingCount` is
|
||||
`RuntimeAcceptedPositionDriveController.cs:330` — `_pending is null ? 0 : 1`.
|
||||
It is (a) **global**, not portal-scoped, and (b) cleared by every terminal path,
|
||||
committing or not. `Advance()` clears `_pending` at five sites; three of them
|
||||
run **without** a portal commit:
|
||||
|
||||
| site | condition | committed? |
|
||||
|---|---|---|
|
||||
| `:975` | *"The watch died — most likely a subsequent accepted Position's merge-time `Forget`"* | **no** |
|
||||
| `:920` | A2's new abandon-at-wake (`!IsPortalAuthorityCurrent`) | body moved by `RetryDeferred`, **suffix skipped** |
|
||||
| `:998` | A2's new abandon at the prepare-retry branch (`CancelToken`) | **no — nothing ever placed** |
|
||||
|
||||
**The `:975` path is the modal case, not a corner case.** The method's own
|
||||
doc comment (`:880-892`) says it verbatim:
|
||||
|
||||
> `RuntimeEntityObjectLifetime.TryApplyPosition` calls `Forget` on EVERY
|
||||
> accepted Position for this entity … ACE broadcasts at 5-10 Hz, so a
|
||||
> `DeferredCell` park surviving past one broadcast interval is cancelled
|
||||
> before its collision generation can ever commit it — **the exact
|
||||
> far-destination case the park exists to serve**.
|
||||
|
||||
So: park → within ~100-200 ms an ordinary broadcast `Forget`s it → `Advance`
|
||||
clears `_pending` → `PendingCount == 0` → both gates declare success.
|
||||
|
||||
**Concrete failure scenario (graphical).** Portal to a landblock whose
|
||||
collision generation has not committed. `TryExecuteAcceptedPortalArrival`
|
||||
returns `DeferredCell`; `_awaitingDeferredWake = true`. One ACE broadcast
|
||||
later the park is Forgotten and `_pending` clears. Next `Tick`:
|
||||
`_placementCommitted = true` → `placementReady = true` → the sequencer leaves
|
||||
`Tunnel` and fires `Place` → the `if (!_placementCommitted) return;` guard at
|
||||
`:557` **passes** → `_placement.Place(_pendingRotation)` writes the render
|
||||
entity from the *unmoved* `controller.Position` and rebuckets to the *source*
|
||||
cell → `ObserveMaterialized(_pendingRevealGeneration, sequence, _pendingCell)`
|
||||
**succeeds** (the reveal is still active and current) → `PlayExitSound` reveals
|
||||
the world viewport → `FireLoginComplete` sends LoginComplete, and
|
||||
`_transit.Complete(generation)` now **passes** its
|
||||
`portal-complete-before-materialized` check because materialization was falsely
|
||||
acknowledged. The player is released into the world standing at the
|
||||
pre-teleport position, the transit reports a clean completion, and nothing logs
|
||||
an invariant failure. Round 1's A1 at least tripped
|
||||
`FailInvariant("portal-complete-before-materialized")`; this does not.
|
||||
|
||||
**Concrete failure scenario (headless).** Identical shape:
|
||||
`PrepareDestination` returns `IsCollisionReady: true`, so
|
||||
`RuntimeLiveEntitySessionController.TryAdvancePortalCompletion` runs
|
||||
`AcknowledgeDestinationReadiness` → `AcknowledgePortalMaterialized` →
|
||||
`Complete` → `TerminalProjected` → `LoginComplete` → `EndTeleport`, and sets
|
||||
`controller.State = PlayerState.InWorld`, all with the body unmoved. That is
|
||||
round-1 A3 verbatim.
|
||||
|
||||
**Secondary hazard from the same root:** because the slot is global, a portal
|
||||
park killed by a merge-time `Forget` can be immediately replaced by the force
|
||||
arm's own `RetainPending` from that same merge
|
||||
(`TryExecuteAcceptedLocalPosition` → `RetainPending`). The portal gate then
|
||||
polls a **ForcePosition** operation, waits for it, and latches "portal
|
||||
committed" when the force operation settles.
|
||||
|
||||
**Why no test caught it.** Both new park tests
|
||||
(`PortalDeferredCell_ParksThenCommitsExactlyOnceOnTheCollisionGenerationWake`,
|
||||
`HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake`)
|
||||
commit the destination's collision generation so the park resolves by
|
||||
committing. `PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale`
|
||||
asserts the **drive's** behaviour (`Assert.Empty(gameActions)`) and stops
|
||||
there — it never asks what the *host gate* concludes from the resulting
|
||||
`PendingCount == 0`. The seam between "the drive retired the park without
|
||||
committing" and "the host decides the portal is placed" is exactly where the
|
||||
defect lives and is exactly what no test crosses.
|
||||
|
||||
**Fix direction.** Stop inferring. The drive already knows the answer with
|
||||
certainty — `ReconcileAndAcknowledgePortal` runs on commit and only on commit.
|
||||
Publish that fact:
|
||||
|
||||
- add a portal-commit observable to `RuntimeAcceptedPositionDriveController`
|
||||
— e.g. `bool TryConsumePortalCommit(long revealGeneration, ushort teleportSequence)`
|
||||
latched in `ReconcileAndAcknowledgePortal` and cleared on consumption, or an
|
||||
`Action<RuntimePortalPlacementAuthority>` commit callback supplied at
|
||||
construction alongside `isPortalAuthorityCurrent`;
|
||||
- have both host gates consume **that**, keyed on the reveal
|
||||
generation/sequence they are waiting for, so a force pending, a Forgotten
|
||||
park, and an abandoned park are all correctly "still not committed";
|
||||
- give the park a terminal "abandoned" outcome the host can see, so it can
|
||||
either re-attempt cleanly (`_awaitingDeferredWake = false` and try Begin
|
||||
again next tick) or converge through the existing transit cancellation
|
||||
rather than silently succeeding.
|
||||
|
||||
**Required tests (both must fail against the current code):**
|
||||
1. Graphical: park, then kill the park with a merge-time `Forget` (an ordinary
|
||||
accepted Position — the fixture's `OfferDestination` already performs the
|
||||
merge) instead of committing the collision generation; drive 100 ticks;
|
||||
assert `Placement.Called == false`, `Movement.Controller.Position` unchanged,
|
||||
`Reveal.PortalMaterializationCount == 0`, `Session.LoginCompleteCount == 0`,
|
||||
`Controller.IsActive == true`.
|
||||
2. Headless: same, asserting `PrepareDestination` keeps returning
|
||||
`IsCollisionReady: false` and `controller.State` stays `PortalSpace`.
|
||||
3. Graphical: park, then let the force arm take the pending slot; assert the
|
||||
portal gate does not latch when the *force* operation settles.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM
|
||||
|
||||
### B2 — A2's re-validation does not close the FIFO wedge it was written for; it only narrows reachability
|
||||
|
||||
The implementer's own note is accurate and I confirm it: `RetryDeferred` →
|
||||
`CommitCanonical` publishes the `Place` receipt asynchronously, and
|
||||
`IsPortalAuthorityCurrent` runs only afterwards, inside `Advance`, gating the
|
||||
*suffix*. But the wedge was never in the suffix — it is in the receipt:
|
||||
|
||||
- `Advance:920`'s abandon branch is reached only **after**
|
||||
`TryPeekAcknowledgedPlacement` succeeded, i.e. the sink **already accepted**
|
||||
the receipt. On that path there was never a wedge to prevent.
|
||||
- The wedge path is the one where the sink **refuses**:
|
||||
`RuntimePlacementPresentationSink.TryApply` →
|
||||
`RuntimeWorldTransitState.IsCurrentPlacementAuthority` false → return false →
|
||||
`RuntimePlacementProjectionSubscription.OnPlacement` leaves it at the FIFO
|
||||
head → every later placement receipt for every entity is blocked and the
|
||||
drive's pending never converges. `IsPortalAuthorityCurrent` never runs on
|
||||
that path, because `TryPeekAcknowledgedPlacement` never yields.
|
||||
|
||||
What the A1 fix *does* buy is reachability: with `placementReady` false during
|
||||
a park, the sequencer cannot reach `FireLoginComplete`, so the transit no
|
||||
longer ends underneath an outstanding park in the ordinary flow. The remaining
|
||||
entries are mid-transit supersession (a second F751 while parked →
|
||||
`OnTeleportStarted` → `ResetTransit` → `EndTeleport`), `ResetSession`, and
|
||||
`ResetGenerationPresentation` — i.e. exactly the §9 "honest gap" cases.
|
||||
|
||||
That is a genuine narrowing and I credit it. It is not closure, and the
|
||||
consequence remains unbounded (whole-FIFO stall + non-convergent shutdown), so
|
||||
contract stop condition 4 ("P3 finds a portal receipt no mechanism can consume
|
||||
or retire — the FIFO-wedge shape changes the design, not the test") still
|
||||
applies.
|
||||
|
||||
**Fix direction.** The retire path has to exist at the receipt, not the suffix.
|
||||
Either (a) let the sinks treat a `Place` whose entity/versions are current but
|
||||
whose portal authority is dead as acknowledge-and-ignore (the same shape
|
||||
`Discard`/`ExecutorCompleted`/`WithdrawalRestored` already use, and for the
|
||||
same stated reason — refusing wedges the ordered stream), or (b) have
|
||||
`CommitCanonical` drop a portal suffix it can already see is not current rather
|
||||
than publishing a receipt nothing can consume. (a) is smaller and matches the
|
||||
existing precedent in both sinks' own doc comments.
|
||||
|
||||
### B3 — Headless `_awaitingPortalWake` is not reset across teleports within one session
|
||||
|
||||
`HeadlessSessionWorldProjection.cs:762`. The graphical twin
|
||||
(`_awaitingDeferredWake`) is cleared in `ResetTransit:932`, which
|
||||
`OnTeleportStarted` calls — clean. The headless field has no equivalent: it is
|
||||
cleared only when the poll declares success, and `HeadlessSessionWorldProjection`
|
||||
is constructed per *session*, not per teleport.
|
||||
|
||||
**Scenario.** Teleport 1 parks → `_awaitingPortalWake = true`. Before the next
|
||||
`PumpPortalCompletion`, a new F751 arrives; `TryCompletePortal` overwrites
|
||||
`_pendingPortalCompletion` with reveal 2. The next pump calls
|
||||
`PrepareDestination(reveal 2)`, which takes the **poll** branch left over from
|
||||
reveal 1 — so reveal 2's placement is never even attempted, and if
|
||||
`PendingCount` happens to be 0 it is immediately declared committed. This is
|
||||
B1's inference bug plus a stale latch, on a path that does not require a
|
||||
Forgotten park.
|
||||
|
||||
**Fix direction.** Key the latch to the reveal generation (or clear it in
|
||||
`BeginTeleport`, which already runs per teleport on this host).
|
||||
|
||||
### B4 — Both hosts' retry loops are unbounded and undiagnosable
|
||||
|
||||
A refusal loop (`Contention`, stale reveal, or a park that never converges)
|
||||
now retries forever with no timeout and no terminal path.
|
||||
|
||||
- **Graphical**: the player holds in the tunnel with the retail wait cue after
|
||||
5 s. This is a *modelled* end state — AD-2 already documents "predicate never
|
||||
satisfies → portal transit remains in the authored tunnel and presents the
|
||||
centered wait cue" — so an infinite stall is strictly better than round 1's
|
||||
silent release, and I do not consider it blocking. Two gaps, though: AD-2
|
||||
attributes that state to streaming/DAT failure only, and now a *placement*
|
||||
refusal produces the identical user-visible state; and `_holdSeconds` /
|
||||
`ObserveWait` are now driven by `!placementReady` rather than `!dataReady`,
|
||||
which is a real semantic change to the wait cue's meaning. The
|
||||
`[tp-probe] REFUSED cause=…` line does distinguish them in the log — good —
|
||||
but AD-2 should say so.
|
||||
- **Headless**: worse, because there is no cue and no bound. A bot whose
|
||||
destination collision never becomes resident sits in `PlayerState.PortalSpace`
|
||||
indefinitely, connected and healthy-looking; `PumpPortalCompletion` is
|
||||
entirely silent. Under K4's 30-session envelope this is an invisible stuck
|
||||
session. At minimum emit one probe line on the first N retries; ideally bound
|
||||
the wait and fail loudly.
|
||||
|
||||
### B5 — §8 items 8/9/10 (the committed-receipt presentation suite) — **does NOT block**, with conditions
|
||||
|
||||
Answering the coordinator's direct question.
|
||||
|
||||
**Does not block route 3.** Reasons, in order of weight:
|
||||
|
||||
1. The render-entity half now has a *proven mechanism*, not a claim.
|
||||
`LiveEntityRuntime.TryApplyRuntimePlacementPlace:1394-1402` performs
|
||||
`entity.SetPosition(projection.WorldPosition)`, `entity.Rotation = …`,
|
||||
`entity.ParentCellId = token.ExactCellId` and `RebucketLiveEntity`,
|
||||
synchronously inside `CommitCanonical`, before `TryPublishPlace` snapshots.
|
||||
That path is shared with route 2's force arm and C3c's first entry and has
|
||||
existing coverage. Round 2 correctly rewrote the class doc to say so
|
||||
(round-1 A10(a) closed).
|
||||
2. The restored `Movement.Controller.Position`/`CellId` assertions prove the
|
||||
canonical body resolved the offered destination — which is the half route 3
|
||||
actually changed ownership of.
|
||||
3. The shadow half is a **pre-existing route-2 defect**, not a route-3
|
||||
regression: `LocalPlayerShadowState.Set` (written by the sink) updates only
|
||||
the dedup cache, never `PhysicsEngine.ShadowObjects`, while
|
||||
`LocalPlayerShadowSynchronizer.SyncPose` dedups against that same cache. It
|
||||
self-heals on the player's first >1 cm move. Route 3 widens the window (via
|
||||
`LocalPlayerProjectionController.Project:102`'s PortalSpace early return) but
|
||||
does not change its kind.
|
||||
|
||||
**Conditions — all three, or it does block:**
|
||||
|
||||
- It is recorded as an **open issue with a number** (#312's layer / route 2's
|
||||
B2 gap, now two campaigns old) and carried explicitly into C5's parity-test
|
||||
scope. Not a comment; a tracked item.
|
||||
- The connected gate is **not** scored as covering it. The probe fields
|
||||
(`leash`, `autorun`, `hookTail`) say nothing about the shadow; there is no
|
||||
visual for it. If the user's session passes, the shadow claim remains
|
||||
test-verified-nowhere and must be reported that way (the §9 "honest gap"
|
||||
discipline).
|
||||
- The register gets one line under AD-2 or a sibling row naming the
|
||||
cache-without-publish asymmetry, so the next reader does not assume
|
||||
`LocalPlayerShadowState.Current` means "published".
|
||||
|
||||
**Concretely, what the test needs** (in
|
||||
`tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs`, whose
|
||||
fixture already owns `BeginPortal` and a `LocalShadow`):
|
||||
|
||||
1. **Committed local-player portal Place through the real sink.** Pre-seed the
|
||||
render `WorldEntity` at a *different* (wire) pose — this is also §8 item 9's
|
||||
T8-ordering half. Drive a `Place` receipt for the local player carrying a
|
||||
VALID portal authority. Assert, after: `entity.Position`/`Rotation`/
|
||||
`ParentCellId` equal the receipt's `WorldPosition`/`Orientation`/
|
||||
`ExactCellId` (the wire pose did not survive); the spatial bucket moved to
|
||||
the destination landblock; `LocalShadow.Current` equals the resolved pose;
|
||||
**and `PhysicsEngine.ShadowObjects` actually holds a row for the player at
|
||||
the destination cell** — that last assertion is the one that discriminates
|
||||
cache-only from published, and is the whole point.
|
||||
2. **Discrimination half.** The same receipt with a stale/superseded portal
|
||||
authority must be refused **and then retired** — not left at the FIFO head.
|
||||
This is also B2's regression test.
|
||||
3. **Refused-Place presentation (§8 item 10).** Already partly covered by
|
||||
`RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears`; extend it
|
||||
to assert the pre-teleport pose is still the *presented* pose (entity +
|
||||
world snapshot store), not only that the body is unmoved.
|
||||
4. **Sabotages that must fail:** remove the sink's `entity.SetPosition` → (1)
|
||||
fails; leave the shadow write as cache-only → (1)'s `ShadowObjects`
|
||||
assertion fails; make the stale-authority receipt return `false` forever →
|
||||
(2) fails.
|
||||
|
||||
---
|
||||
|
||||
## MINOR
|
||||
|
||||
### B6 — `isPortalAuthorityCurrent` should be a required constructor parameter
|
||||
|
||||
The coordinator's specific concern, checked: **both production sites wire it** —
|
||||
`SessionPlayerComposition.cs:597-603` and `HeadlessSessionHost.cs:665-671`,
|
||||
both to `RuntimeWorldTransitState.CanPlacePortalDestination`. Four test sites
|
||||
do not, which is fine.
|
||||
|
||||
The residual is that a null default silently restores the round-1 defect, and
|
||||
nothing catches a future production site that forgets it — there is no
|
||||
architecture guard for this the way
|
||||
`RuntimePhysicsOwnershipTests.ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel`
|
||||
guards the placement channel. With only 2 production + 4 test constructions,
|
||||
making the parameter required (tests pass `static _ => true`) converts a silent
|
||||
regression into a compile error for ~6 lines of churn. Same argument will apply
|
||||
to B1's commit observable.
|
||||
|
||||
### B7 — `_placementCommitted` is checked once and never re-validated
|
||||
|
||||
`LocalPlayerTeleportController.cs:557`. Round 1's Place handler ran
|
||||
`CanPlacePortalDestination` immediately before mutating; round 2 moved that
|
||||
check into `TryAdvancePortalCommit`'s **non-deferred** branch only
|
||||
(`:668-676`). Once `_placementCommitted` latches, the only guard before
|
||||
`_placement.Place()` / `ObserveMaterialized` is `IsCurrentLifetime`. If the
|
||||
reveal is cancelled between the commit and the Place event,
|
||||
`ObserveMaterialized` refuses (`IsCurrentPortalDestination`) but the
|
||||
presentation suffix has already run and the stream continues to
|
||||
`FireLoginComplete` with an unmaterialized reveal. Much less severe than B1 —
|
||||
the body genuinely is at the destination — but it is the same family. Cheap
|
||||
fix: keep the `CanPlacePortalDestination` re-check in the Place handler
|
||||
alongside `_placementCommitted`.
|
||||
|
||||
### B8 — A8 (round 1) confirmed still open and confirmed non-blocking
|
||||
|
||||
Five portal tests still call `ConvergePortalHost` as the last statement of the
|
||||
test body rather than in a `finally`, so an assertion failure is still masked
|
||||
by a `Dispose()` throw during unwinding. Correctly flagged rather than silently
|
||||
dropped. Test hygiene only — no production effect. It does mean that when B1's
|
||||
new tests are written, a genuine failure may again present as a teardown throw;
|
||||
fixing the `try/finally` first would save that debugging round.
|
||||
|
||||
### B9 — round-1 A11/A12 unchanged
|
||||
|
||||
`PhysicsDiagnostics.LocalTeleportHostKind` process-global (accepted);
|
||||
`AddSyntheticIndoorCell` geometry-free (accepted — and less load-bearing now
|
||||
that A6's position assertions are restored).
|
||||
|
||||
---
|
||||
|
||||
## Closed since round 1
|
||||
|
||||
| round-1 finding | status | evidence |
|
||||
|---|---|---|
|
||||
| **A4** — route facts unread, inversions hardcoded | **CLOSED** | `RuntimeAuthoritativePositionRoute.RunsTeleportHook:164` / `.ConstrainAfterRouting:170`; `ReconcileAndAcknowledgePortal` reads both; `CommitCanonicalTeleportFrame(bool zeroVelocity, bool rearmConstraintLeash)` branches on them. `ConstrainPhase.None` sabotage now fails `PortalCommitted_MovesBodyArmsLeashOnceCancelsAutorunAndSendsExactlyOneMovementEvent`'s stale-anchor assertion, as the contract's §8 item 11 intended. |
|
||||
| **A6** — superseded-teleport discriminator removed | **CLOSED** | `NewerStart_ReplacesOldDestinationWithoutReusingIt` asserts `Movement.Controller.Position.X/Y == 2` and the cell; `SameLandblockDestination_…` asserts `(20,30,4)` + `0x20210123`; the Z-vs-X/Y reasoning in the comment is sound. |
|
||||
| **A9** — two sequence sources | **CLOSED** | The authority now uses `destination.TeleportSequence`, with a `Debug.Assert` pinning it equal to the caller's copy. |
|
||||
| **A10(a)** — class doc claimed the suffix was the render entity's only mover | **CLOSED** | Doc rewritten to state the writes are redundant repeats of the canonical receipt's mutation, with the ordering cited. |
|
||||
| **A1** — refused Place did not stop the anim stream | **partially** — the mechanism is right (readiness inversion, sequencer untouched), and the `Contention` path is now correctly held and tested (`RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears`). **B1 reopens it for the `DeferredCell` path.** |
|
||||
| **A2** — no wake re-validation | **partially** — D-T2.4's re-validation now exists and is wired in both hosts and tested; **B2** shows it does not close the FIFO wedge. |
|
||||
| **A3** — headless discarded the status | **partially** — status now honoured, throws on `Rejected`/`NotApplicable` and on a missing drive, retryable pump added. **B1/B3 reopen it.** |
|
||||
| **A5** — presentation suite | **still open** — see B5 for the definite blocks/does-not-block answer. |
|
||||
| **A7** — dual-host parity | **CLOSED** | `HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake` drives a real wired drive end to end and asserts body position, cell, and `PlayerState`. |
|
||||
| **A8** | open, non-blocking (B8). |
|
||||
| **A11 / A12** | unchanged (B9). |
|
||||
|
||||
## Also verified this round
|
||||
|
||||
- `_pendingDestination` lifetime — round-1 judgment stands, not re-litigated.
|
||||
- The sequencer's own invariants survive the late `worldReady`: `Tunnel` is
|
||||
explicitly a hold state (*"Hold here until worldReady"*), `TickTunnel` still
|
||||
runs on the hold path so `CurrentAnimationFrame` keeps advancing into
|
||||
`TunnelContinue`'s exit window, and `maxForce` at 5 s covers a stale frame.
|
||||
`worldReady` has exactly one consumer (`case TeleportAnimState.Tunnel`), so
|
||||
no other transition changed meaning.
|
||||
- No double-Begin headless: `TryCompletePortal`'s own
|
||||
`TryGetAcceptedTeleportDestination`/`TryBeginPortalReveal` prefix cannot
|
||||
succeed twice for one reveal (the destination slot is consumed), and
|
||||
`_awaitingPortalWake` suppresses a second concurrent `Begin` while parked.
|
||||
(The stale-latch problem is B3, a different failure.)
|
||||
- `_placementCommitted` / `_awaitingDeferredWake` are both cleared in
|
||||
`ResetTransit:931-932`, so the graphical latches do not survive a new F751,
|
||||
a session reset, or a generation reset.
|
||||
- Build green.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| # | Severity | Finding |
|
||||
|---|---|---|
|
||||
| B1 | MAJOR | `PendingCount == 0` inferred as "committed" on both hosts; three non-committing paths clear it, including the drive's own documented modal outcome. Reintroduces A1 (graphical, now with a *successful* false materialization) and A3 (headless). Untested. |
|
||||
| B2 | MEDIUM | A2's re-validation gates the suffix, not the receipt; the FIFO wedge is narrowed (supersession/reset only) but not closed. |
|
||||
| B3 | MEDIUM | Headless `_awaitingPortalWake` is not reset per teleport; a stale latch can skip reveal N+1's placement attempt entirely. |
|
||||
| B4 | MEDIUM | Unbounded, undiagnosable retry loops on both hosts; headless has no cue, no bound, and no log. |
|
||||
| B5 | — | §8 8/9/10 presentation suite: **does not block**, subject to three stated conditions; required test spelled out. |
|
||||
| B6 | MINOR | Make `isPortalAuthorityCurrent` (and B1's commit observable) required constructor parameters. |
|
||||
| B7 | MINOR | `_placementCommitted` never re-validated before the presentation suffix. |
|
||||
| B8 | MINOR | A8 still open (5 tests), confirmed non-blocking. |
|
||||
| B9 | MINOR | A11/A12 unchanged. |
|
||||
550
docs/research/2026-08-04-c4-route-3-architecture-review.md
Normal file
550
docs/research/2026-08-04-c4-route-3-architecture-review.md
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
# C4 route 3 — architecture / adversarial review (2026-08-04)
|
||||
|
||||
**Verdict: FAIL.**
|
||||
|
||||
Reviewed: the uncommitted working-tree diff (`git diff HEAD` + untracked) on
|
||||
`claude/acdream-physics-divergence-5aa784` at HEAD **`cd3129e9`** (route 7's
|
||||
commit, "child cell propagation moves from a render tick into Runtime"). 16
|
||||
files, +1,573/-216.
|
||||
|
||||
Reference documents read in full: the route-3 contract
|
||||
(`2026-08-04-c4-route-3-contract.md`), the route-3 scoping, the route-2
|
||||
contract, and the route-5/route-7 review defect classes.
|
||||
|
||||
Independent verification performed against source, not against the
|
||||
implementer's summary: `RuntimeWorldTransitState`, `RuntimeSetPositionState`'s
|
||||
commit tail, `RuntimeEntityObjectEventStream`/
|
||||
`RuntimePlacementProjectionSubscription` (publication synchronicity),
|
||||
`RuntimePlacementPresentationSink` + `LiveEntityRuntime
|
||||
.TryApplyRuntimePlacementPlace`, `LocalPlayerShadowSynchronizer`,
|
||||
`LocalPlayerProjectionController`, `TeleportAnimSequencer`, and both hosts'
|
||||
`Advance()` pump sites. `dotnet build AcDream.slnx -c Debug` exits 0.
|
||||
|
||||
**The `_pendingDestination` fix — the item flagged as highest risk — is
|
||||
correct, and is not the reason for the FAIL.** See §"Judgment on the
|
||||
`_pendingDestination` lifetime" at the end.
|
||||
|
||||
The FAIL rests on A1 and A2: the slice adds five new ways for the Place edge
|
||||
to refuse, and the refusal path it hands them to does not stop the teleport
|
||||
animation stream. One of those five (`DeferredCell`) additionally commits the
|
||||
placement out of band after the transit has ended, which both splits the body
|
||||
from presentation and leaves a placement receipt nothing can consume — proof
|
||||
obligations **P3** (graphical half) and **P4** are undischarged, and D-T2.4's
|
||||
"the wake path must re-validate the portal authority before committing" is not
|
||||
implemented at all.
|
||||
|
||||
---
|
||||
|
||||
## MAJOR
|
||||
|
||||
### A1 — A refused canonical Place does not stop the teleport animation; the player is released into the world without ever having been placed
|
||||
|
||||
**Severity: MAJOR (FAIL basis).**
|
||||
|
||||
`src/AcDream.Core/World/TeleportAnimSequencer.cs:134-142`:
|
||||
|
||||
```csharp
|
||||
case TeleportAnimState.Tunnel:
|
||||
if (worldReady)
|
||||
{
|
||||
evts.Add(TeleportAnimEvent.Place);
|
||||
Advance(TeleportAnimState.TunnelContinue, enterTunnel: false);
|
||||
_continueElapsed = 0f;
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
`TeleportAnimEvent.Place` is emitted **exactly once**, and the sequencer
|
||||
advances to `TunnelContinue` in the same statement block, unconditionally and
|
||||
with no knowledge of whether the consumer's handler succeeded. There is no
|
||||
path back to `Tunnel`.
|
||||
|
||||
`src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:505-526`:
|
||||
|
||||
```csharp
|
||||
case TeleportAnimEvent.Place:
|
||||
if (!_worldReveal.CanPlacePortalDestination(...)) return;
|
||||
if (!TryExecuteCanonicalPortalPlacement(sequence))
|
||||
return; // <- new in this slice
|
||||
...
|
||||
_placement.Place(_pendingRotation);
|
||||
...
|
||||
_worldReveal.ObserveMaterialized(...);
|
||||
```
|
||||
|
||||
The `return` exits `Tick`, but the sequencer has already left `Tunnel`. Every
|
||||
subsequent `Tick` therefore runs the rest of the stream with **no placement
|
||||
and no materialization**:
|
||||
|
||||
| next event | what runs |
|
||||
|---|---|
|
||||
| `TunnelContinue` → `TunnelFadeOut` | — |
|
||||
| `PlayExitSound` (`:532-542`) | `_worldReveal.RevealWorldViewport()` + `_presentation.ExitTunnel()` |
|
||||
| `FireLoginComplete` (`:543-554`) | `_mode.EnterWorld()`, `_session.SendLoginComplete()`, `_worldReveal.Complete()`, `ResetTransit(clearSession: false)` |
|
||||
|
||||
`WorldRevealCoordinator.RevealWorldViewport` only needs a live host projection
|
||||
(`WorldRevealCoordinator.cs:258-266`) — present. `Complete()` reaches
|
||||
`RuntimeWorldTransitState.Complete` (`:701-706`), which hits
|
||||
`FailInvariant("portal-complete-before-materialized")` and returns `false`;
|
||||
`FailInvariant` (`:934-946`) only increments a counter and logs — it does not
|
||||
throw and does not stop the caller. `ResetTransit` then runs `EndTeleport()` +
|
||||
`_worldReveal.Cancel()`.
|
||||
|
||||
**Concrete failure scenario.** A `Contention` outcome (route 2's force arm, a
|
||||
route-7 parent drive, or an earlier park still owns the entity's placement
|
||||
token at the Place edge) makes `TryExecuteAcceptedPortalArrival` return
|
||||
`Contention` at `RuntimeAcceptedPositionDriveController.cs:492-501`. The
|
||||
player watches the portal tunnel finish normally, the world viewport is
|
||||
revealed, `LoginComplete` is sent to ACE — and the player is standing at the
|
||||
**pre-teleport position** in the **pre-teleport cell**. ACE has them at the
|
||||
destination. Every subsequent server broadcast fights the client. Nothing in
|
||||
the client logs above `[world-reveal] event=invariant-failure` (a `SafeLog`
|
||||
line), and the D-T8 probe never emits because
|
||||
`ReconcileAndAcknowledgePortal` never ran.
|
||||
|
||||
**Why this is the slice's problem and not inherited.** The pre-existing
|
||||
`CanPlacePortalDestination` early return (`:506-512`) has the same shape, but
|
||||
it fires only when the transit no longer owns this reveal — a case where
|
||||
marching on is at worst redundant, because a newer transit owns the world.
|
||||
This slice adds **five new refusal causes that all fire while the transit is
|
||||
perfectly healthy**: `host-token-unavailable`, `NotApplicable`, `Rejected`,
|
||||
`Contention`, and `DeferredCell` (`LocalPlayerTeleportController.cs:596-625`,
|
||||
which treats everything except `Committed` as a refusal at `:619`).
|
||||
|
||||
**Contract obligations violated.** §4 item 4 ("on every refusal … the transit
|
||||
remains coherent … never a half-state … no path leaves the player permanently
|
||||
in portal space with a dead operation"); §4 item 5 ("a refused placement must
|
||||
NOT … `RevealWorldViewport`, must NOT advance the anim-event stream's terminal
|
||||
events"); D-T5's Begin-refusal row ("the anim stream stays where it is, so the
|
||||
NEXT Tick re-attempts the Place edge … if the Place anim event is one-shot,
|
||||
the re-attempt must be driven by the same Tick predicate that produced it, and
|
||||
THAT mechanism must be stated in the commit"); and proof obligation **P4**
|
||||
verbatim. P4 was to be discharged by reading `TeleportAnimSequencer`. It is
|
||||
one-shot. No re-attempt driver exists.
|
||||
|
||||
**Fix direction.** Two shapes are available without touching the sequencer
|
||||
(stop condition 2 forbids sequencer timing changes):
|
||||
|
||||
1. Make the Place edge idempotent-and-latched at the controller: keep a
|
||||
`_placementCommitted` flag; on a refusal, do NOT let the stream reach
|
||||
`PlayExitSound`/`FireLoginComplete` — gate those two cases on the latch and
|
||||
drive a bounded re-attempt from the same `ready` predicate that produced
|
||||
the Place event (the contract's stated fallback). A refusal that never
|
||||
converges must then take the existing transit cancellation
|
||||
(`ResetTransit(clearSession: false)` — which already cancels the reveal and
|
||||
restores presentation) rather than a silent world release.
|
||||
2. Or treat a refusal as an immediate transit cancellation and let the
|
||||
existing supersession path own recovery. Louder, smaller, and it satisfies
|
||||
D-T5's "never a silent wedge" — but it needs the user's eyes because it is
|
||||
user-visible (the portal fails and the player stays put) rather than a
|
||||
silent desync.
|
||||
|
||||
Either way this needs a test: *refused arm → the anim stream does not reach
|
||||
`RevealWorldViewport`/`FireLoginComplete` with an unplaced body.*
|
||||
|
||||
---
|
||||
|
||||
### A2 — A `DeferredCell` portal park splits the body from presentation and leaves a placement receipt nothing can consume (P3 undischarged on the graphical host; D-T2.4's re-validation missing)
|
||||
|
||||
**Severity: MAJOR.**
|
||||
|
||||
`RuntimeAcceptedPositionDriveController.SubmitAndResolvePortal:614-645` parks a
|
||||
`DeferredCell` outcome into `_pending` carrying the portal authority.
|
||||
`LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement:619` returns
|
||||
`false` for it, so A1's march runs: the stream reaches `FireLoginComplete`,
|
||||
`ResetTransit` calls `_transit.EndTeleport()` and `_worldReveal.Cancel()`.
|
||||
|
||||
The park is still live. `Advance()` is pumped by the graphical host at
|
||||
`src/AcDream.App/Net/GraphicalSessionEventRoute.cs:117` and
|
||||
`src/AcDream.App/World/LiveEntityHydrationController.cs:416`. When the
|
||||
destination landblock's collision generation eventually commits:
|
||||
|
||||
- `Advance:763-771` runs `ReconcileAndAcknowledgePortal` — the body moves, the
|
||||
leash re-arms, autorun cancels, and **one outbound movement event is sent** —
|
||||
seconds after the player was already released into the world at the old
|
||||
position. Presentation is never told: `_placement.Place` and
|
||||
`ObserveMaterialized` are unreachable (the anim event is one-shot, A1).
|
||||
- The commit publishes a `Place` receipt whose `Token.Portal` still names the
|
||||
ended reveal. `RuntimeWorldTransitState.IsCurrentPlacementAuthority:258-275`
|
||||
requires `IsCurrentPortalDestination` (`:870-882`), which requires
|
||||
`_teleportActive` — cleared by `EndTeleport` (`:547-556`). It returns false
|
||||
**forever**.
|
||||
- `RuntimePlacementPresentationSink.TryApply:100-106` therefore returns
|
||||
`false`; `RuntimePlacementProjectionSubscription.OnPlacement:134-136` leaves
|
||||
the receipt at the FIFO head. Every later placement receipt **for every
|
||||
entity** is blocked behind it, and `AcceptedPositionDrivePendingCount` never
|
||||
returns to zero, so `GameWindowLifetime.DisposeGameRuntime` throws on
|
||||
shutdown.
|
||||
|
||||
That is precisely the failure mode P3 exists to rule out ("A receipt nothing
|
||||
can ever consume or retire is a FIFO wedge"). P3 was discharged only for the
|
||||
headless *happy path*; the graphical park was not walked.
|
||||
|
||||
Independently, **D-T2.4's explicit requirement is not implemented**: "its wake
|
||||
path must re-validate the portal authority before committing". Neither
|
||||
`Advance`'s `AwaitingCommitWake` branch (`:763-771`) nor its
|
||||
`IsPlacementCurrent` re-submit branch (`:813-819`) re-checks the portal
|
||||
authority — they pass `pending.Portal` straight through.
|
||||
|
||||
**Fix direction.** (a) Re-validate the portal authority at both wake points
|
||||
(`_entityObjects` has no transit handle today — the drive needs a
|
||||
`Func<bool>`-style currency predicate or the authority passed back through the
|
||||
transit owner); on failure take `AbandonPending`'s exact shape
|
||||
(`restoreCancelledPark: true` + `PublishCancellation`) so the park is retired
|
||||
rather than committed. (b) Independently, A1's fix must prevent the transit
|
||||
from ending while a portal park is outstanding.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM
|
||||
|
||||
### A3 — Headless discards the arm's status entirely; a failed placement is silent
|
||||
|
||||
`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:768-780`:
|
||||
|
||||
```csharp
|
||||
if (_acceptedPositionDrive is not null)
|
||||
{
|
||||
var authority = new RuntimePortalPlacementAuthority(...);
|
||||
_ = _acceptedPositionDrive.TryExecuteAcceptedPortalArrival(destination, authority);
|
||||
}
|
||||
```
|
||||
|
||||
Two problems. First, the status is discarded: any non-`Committed` outcome
|
||||
leaves the body unmoved while `TryCompletePortal`
|
||||
(`RuntimeLiveEntitySessionController.cs:530-585`) proceeds through
|
||||
`AcknowledgeDestinationReadiness` → `AcknowledgePortalMaterialized` →
|
||||
`Complete` → `TerminalProjected` → `LoginComplete` → `EndTeleport` — asserting
|
||||
a materialization that did not happen (§4 item 5) and telling ACE the login
|
||||
completed. The deleted `ResynchronizeLocalPlayerForPortalArrival` was
|
||||
unconditional and could not fail this way. Second, the `is not null` guard
|
||||
means a composition regression that fails to wire the drive silently disables
|
||||
headless portal placement with no signal at all; the previous code had no such
|
||||
mode.
|
||||
|
||||
**Fix direction.** Treat a non-`Committed` status as a hard failure on this
|
||||
path (`TryCompletePortal` already throws on every other Runtime refusal —
|
||||
match that), and make the drive a required constructor dependency for the
|
||||
production projection.
|
||||
|
||||
### A4 — Both inversions are hardcoded; the classifier route facts that encode them are never read
|
||||
|
||||
`ReconcileAndAcknowledgePortal(RuntimeEntityRecord, in RuntimeAuthoritativePositionRoute route, in RuntimePortalPlacementAuthority)`
|
||||
(`RuntimeAcceptedPositionDriveController.cs:667-698`) **never references
|
||||
`route`**. `PlayerMovementController.CommitCanonicalTeleportFrame:1987-2035`
|
||||
unconditionally zeroes velocity, runs `StopCompletelyAtPhysicsObjectBoundary`,
|
||||
`UnStick`/`UnConstrain`, and `RearmConstraintLeashAtCurrentPosition`.
|
||||
|
||||
So `route.ZeroVelocity`, `route.ConstrainPhase`, and `route.TeleportHookPhase`
|
||||
are recorded-not-consumed, even though D-T2.3 pinned "`route.ZeroVelocity` is
|
||||
honored at the commit". The visible behaviour is correct today only because
|
||||
the classifier's LocalPlayer-teleport branch happens to agree with the
|
||||
hardcoded method.
|
||||
|
||||
Consequence for test quality: the contract's own §8 item 11 sabotage —
|
||||
"hardcode the force route onto the portal arm (`ConstrainPhase.None`) → test
|
||||
5a fails" — **cannot fail**, because no code path reads `ConstrainPhase`. No
|
||||
test discriminates the classifier from the executor. A future classifier edit
|
||||
(the classifier is a shared surface routes 2/4b-2/4b-3 also consume) diverges
|
||||
from behaviour silently.
|
||||
|
||||
**Fix direction.** Either consume the route facts in the frame commit (branch
|
||||
on `ZeroVelocity`/`ConstrainPhase`) and add the discriminating test, or delete
|
||||
the unused `route` parameter and state explicitly, in the class doc and in
|
||||
AD-2, that the inversions are enforced by `CommitCanonicalTeleportFrame` and
|
||||
NOT by the classifier — so the next reader does not trust a route fact that
|
||||
nothing reads.
|
||||
|
||||
### A5 — Contract §8 item 8's committed-receipt presentation suite is missing; invariant 6's "the local-player collision shadow agrees" is asserted nowhere
|
||||
|
||||
No test in this diff drives a committed portal placement through the **real**
|
||||
`RuntimePlacementPresentationSink` plus the suffix.
|
||||
`tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs` is
|
||||
untouched; its only portal test
|
||||
(`PortalPlace_RequiresExactCurrentTransitHostAndSequence:467`) drives a
|
||||
synthetic token, not a local-player placement. The App teleport tests assert
|
||||
`harness.Placement.Called` against a *fake* placement.
|
||||
|
||||
That missing suite would have surfaced the following latent inconsistency,
|
||||
which route 3 newly makes reachable on the portal path:
|
||||
|
||||
- `RuntimePlacementPresentationSink.TryPublishPlace:226-232` writes
|
||||
`_localPlayerShadow.Set(entity.Position, entity.Rotation, record.FullCellId)`.
|
||||
`LocalPlayerShadowState.Set` updates **only the cache** — it never touches
|
||||
`PhysicsEngine.ShadowObjects`.
|
||||
- `LocalPlayerShadowSynchronizer.SyncPose:59-70` dedups against that same
|
||||
cache (`cellId` equal AND position within 1 cm AND orientation within
|
||||
tolerance ⇒ return without publishing).
|
||||
- `LocalPlayerProjectionController.Project:102` early-returns for
|
||||
`PlayerState.PortalSpace`, so the sink's cache write is the last word until
|
||||
the player re-enters the world.
|
||||
|
||||
Net: the first post-arrival `SyncShadow` sees a cache that already claims the
|
||||
resolved pose and resolved cell, skips, and the player's collision shadow row
|
||||
is not published at the destination. It self-heals the first time the player
|
||||
moves more than ~1 cm, so the window is short — but during it, other entities
|
||||
have no collider for the player at the destination. (The shape is pre-existing
|
||||
from route 2's force arm, where `Project` runs every frame so the window is
|
||||
one frame; route 3's portal-space skip widens it.)
|
||||
|
||||
**Fix direction.** Write the §8 item 8 suite. Independently, either have the
|
||||
suffix re-publish the shadow through `LocalPlayerShadowSynchronizer`
|
||||
(`force: true`) after writing the resolved entity pose, or stop the sink from
|
||||
writing a "last published" cache entry it did not publish.
|
||||
|
||||
### A6 — The one test that discriminated *which* destination gets placed lost its discriminator
|
||||
|
||||
`tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs`,
|
||||
superseded-teleport scenario:
|
||||
|
||||
```diff
|
||||
- Assert.Equal(new Vector3(2f), harness.Placement.Position);
|
||||
+ Assert.True(harness.Placement.Called);
|
||||
```
|
||||
|
||||
The old assertion proved the **second** destination `(2,2,2)` was placed and
|
||||
not the first `(1,1,1)`. `Assert.True(Called)` cannot distinguish them. That is
|
||||
exactly the property the `_pendingDestination` caching change puts at risk
|
||||
(caching an Aim-time value instead of re-reading). Two sibling tests lose the
|
||||
same class of assertion, and one now contradicts its own name:
|
||||
`SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition` no longer
|
||||
asserts any position or cell.
|
||||
|
||||
This is the route-5 "tests that assert only negatives"/weakened-successor
|
||||
defect class. The replacement assertion is **available**: the harness now owns
|
||||
a real Runtime `RuntimeLocalPlayerMovementState`, so
|
||||
`Assert.Equal(expected, movement.Controller!.Position)` and `.CellId` are
|
||||
reachable; the harness simply does not expose `movement`.
|
||||
|
||||
**Fix direction.** Expose the Runtime controller on the harness and restore a
|
||||
positive position/cell assertion in each of the three tests, at minimum in the
|
||||
superseded-teleport one.
|
||||
|
||||
### A7 — Dual-host parity (§8 item 6) is not met; the headless flip is untested
|
||||
|
||||
`tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs:413-424` now
|
||||
documents that the fixture's projection is built without a drive controller,
|
||||
so "the canonical portal arm this method now calls is a no-op here by
|
||||
construction", and defers the headless committed-portal test as "an open item,
|
||||
not attempted here given this session's time budget".
|
||||
|
||||
The contract states plainly: "The headless arm reuses the identical Runtime
|
||||
entry — dual-host parity is a test obligation, not an aspiration (§8)."
|
||||
Combined with A3, the entire headless production placement flip — the deletion
|
||||
of `ResynchronizeLocalPlayerForPortalArrival`, the new `portal` parameter, the
|
||||
new call — has zero behavioural coverage. The only surviving assertion change
|
||||
is `CenterCount` 3→2.
|
||||
|
||||
---
|
||||
|
||||
## MINOR
|
||||
|
||||
### A8 — Portal-test cleanup runs at the end of the test body, so the first assertion failure is masked by a teardown throw
|
||||
|
||||
`ConvergePortalHost` is invoked as the last statement of each portal test
|
||||
(`RuntimeAcceptedPositionDriveControllerTests.cs:1273/1380/1423/1519`). If any
|
||||
earlier `Assert` throws, cleanup is skipped, `StartedRuntime.Dispose()` throws
|
||||
during unwinding, and C# `using`/`try…finally` lets the finally-exception
|
||||
**replace** the in-flight one.
|
||||
|
||||
The implementer diagnosed their own instance of this correctly (claim 1 —
|
||||
verified: the mechanism is real and the two corrected assertions were genuine
|
||||
test bugs, since an accepted `TeleportAdvanced` merge rebases the world frame
|
||||
onto the destination per #283, and `ReconcileAndAcknowledgePortal` does
|
||||
legitimately send one movement event). But the pattern remains in the shipped
|
||||
tests, and it is exactly how a genuine host-projection leak would also present
|
||||
— which is why it is worth removing rather than remembering.
|
||||
|
||||
**Fix direction.** `try { … } finally { ConvergePortalHost(…); }`, or make
|
||||
`StartedRuntime.Dispose` record non-convergence and assert it explicitly.
|
||||
|
||||
`ConvergePortalHost` itself is otherwise sound: it cannot double-release —
|
||||
`AcknowledgeHostProjection`'s `TerminalProjected` branch removes the record
|
||||
(`RuntimeWorldTransitState.cs:343-355`) and a second call returns false — and
|
||||
it cannot leak, because a forgotten call throws at Dispose.
|
||||
|
||||
### A9 — Two sources for one teleport sequence
|
||||
|
||||
`LocalPlayerTeleportController.cs:613` builds the authority's
|
||||
`TeleportSequence` from `_transit.ActiveTeleportSequence` (passed in as
|
||||
`sequence`), while `ClassifyPortalArrival`
|
||||
(`RuntimeAcceptedPositionDriveController.cs:514-535`) derives its
|
||||
accepted/prior pair from `destination.TeleportSequence`. They agree today —
|
||||
`OfferTeleportDestination:490-497` refuses a second destination for an
|
||||
already-accepted active sequence — but two sources for one fact is the
|
||||
campaign's "mapping written against one caller's reachable set" shape.
|
||||
|
||||
**Fix direction.** Use `destination.TeleportSequence` in both, or assert
|
||||
equality at the producer.
|
||||
|
||||
### A10 — Two documentation statements assert behaviour the code does not have
|
||||
|
||||
(a) The new class doc on `LocalPlayerTeleportPlacement`
|
||||
(`LocalPlayerTeleportController.cs:188-193`) says the sink "snapshots whatever
|
||||
the entity already holds and writes no pose itself — this is the render
|
||||
entity's mover". `TryPublishPlace` writes no pose, but the sink's own upstream
|
||||
call chain does: `RuntimePlacementPresentationSink.TryApply:108` →
|
||||
`LiveEntityRuntime.TryApplyRuntimePlacementProjection:1301` →
|
||||
`TryApplyRuntimePlacementPlace:1386-1420`, which performs
|
||||
`entity.SetPosition(projection.WorldPosition)`, `entity.Rotation = …`,
|
||||
`entity.ParentCellId = token.ExactCellId`, **and** `RebucketLiveEntity` — all
|
||||
before `TryPublishPlace` snapshots. The suffix's entity writes and rebucket are
|
||||
therefore redundant repeats of a mutation the canonical receipt already made.
|
||||
|
||||
This is harmless at runtime today, but it is the "a doc asserting behaviour
|
||||
the code does not have" class — and the contract carries the same misreading
|
||||
(§3.4, D-T4, §12.5(b)), so correcting the code comment alone is not enough.
|
||||
|
||||
(b) The plan correction in `docs/plans/2026-08-02-placement-cutover.md` says
|
||||
"`RuntimeAcceptedPositionDriveController`'s portal arm reading it — was
|
||||
already live before route 3 (from route 2's shared drive controller)". The
|
||||
portal arm was added by **this** slice. A correction that itself asserts a
|
||||
false fact is worse than the line it corrects.
|
||||
|
||||
### A11 — `PhysicsDiagnostics.LocalTeleportHostKind` is a process-global mutable set from a host
|
||||
|
||||
`src/AcDream.Core/Physics/PhysicsDiagnostics.cs` + `HeadlessSessionHost.cs:625`.
|
||||
Correct under K3/K4 (all sessions in a headless process are headless), and the
|
||||
doc comment says so — noted only so it is not later mistaken for per-session
|
||||
state. No action required this slice.
|
||||
|
||||
### A12 — `AddSyntheticIndoorCell` is representative enough to pass the gate, and no more
|
||||
|
||||
The helper registers a `CellPhysics` with an empty `Resolved` polygon
|
||||
dictionary, one `PortalInfo(0,0,0)`, and a leaf-only BSP root. It is not shaped
|
||||
to make a specific assertion pass — it mirrors
|
||||
`RuntimeSetPositionStateTests.AddSyntheticCell` and its only effect is to make
|
||||
`PhysicsEngine.IsSpawnCellReady` return true for an indoor cell, which is a
|
||||
genuine fixture gap (a bare `AddLandblock` passes an empty `CellSurface` list,
|
||||
so indoor destinations parked `DeferredCell` forever). Accepted.
|
||||
|
||||
The caveat: because the destination cell has no geometry, the App-layer tests
|
||||
prove "the arm returned `Committed`", not "the destination resolved somewhere
|
||||
sane". That makes A6's missing position assertions more load-bearing, not
|
||||
less.
|
||||
|
||||
---
|
||||
|
||||
## Verified correct (checked against source, not taken on report)
|
||||
|
||||
- **P1 (the D-T3 duty map).** `RuntimeSetPositionState.cs:5036-5058` calls
|
||||
`PhysicsObjUpdate.CommitSetPositionContactTransition` unconditionally inside
|
||||
the canonical commit, and `CommitSetPositionContactPrefix`
|
||||
(`PhysicsObjUpdate.cs:153-175`) derives `Contact`/`OnWalkable`/
|
||||
`WaterContact` from the placement result's own `InContact`/`OnWalkable`. Not
|
||||
re-seeding `TransientState` in `CommitCanonicalTeleportFrame` is correct and
|
||||
is more faithful than `SetPositionCore`'s unconditional
|
||||
`Contact|OnWalkable|Active` overwrite, exactly as claimed.
|
||||
- **P3, happy path, both hosts.** `RuntimeEntityObjectEventStream
|
||||
.PublishPlacement:164-171` → `EnqueueAndDrain` →
|
||||
`RuntimePlacementProjectionSubscription.OnPlacement:122-150` is
|
||||
**synchronous**, inside `CommitCanonical`. Headless's placement therefore
|
||||
commits and its receipt is consumed inside `PrepareDestination`, strictly
|
||||
before `AcknowledgePortalMaterialized`/`Complete`/`EndTeleport`. The
|
||||
receipt-past-`EndTeleport` hazard is discharged for the committed path. (The
|
||||
residual is A2's park.)
|
||||
- **Trap T7 / route-2 blast radius.** No portal pending reaches
|
||||
`SettlePending` or `_newestForce`: the three `pending.Portal.Present` guards
|
||||
at `:763`, `:794`, and `:820` fence every terminal path, and
|
||||
`SubmitAndResolvePortal` is a genuine sibling of `SubmitAndResolve` rather
|
||||
than an overload of it. `git diff` shows **zero** expectation changes in any
|
||||
force-arm test — the contract's §4 item 8 tripwire is clean.
|
||||
- **Implementer claim 1 (the teardown throw was a test bug).** Mechanism
|
||||
verified. Both corrected assertions were genuinely wrong for the stated
|
||||
reasons, and the `using`-finally exception-replacement is real. See A8 for
|
||||
the residual.
|
||||
- **Sabotage B's asymmetry.** Verified structurally:
|
||||
`PortalProducerInvalidAuthority_ArmDoesNotRunAndNothingMutates` builds an
|
||||
authority with `RevealGeneration: 0`, which fails
|
||||
`RuntimePortalPlacementAuthority.IsValid` at
|
||||
`TryExecuteAcceptedPortalArrival:459` — **before** `ClassifyPortalArrival` is
|
||||
reached. Forcing the classifier to reject cannot change that test's outcome,
|
||||
so the 4-of-5 asymmetry is exactly what the code shape predicts. Good
|
||||
evidence.
|
||||
- **Register bookkeeping.** AD-42's deletion is justified (its last citation
|
||||
was D2's two-call `Resolve`+`ResolvePlacement`, which is gone); AD-2's
|
||||
amendment states the deferred-place adaptation, the T8 tolerance, and the
|
||||
leash-anchor nuance as D-T9 required; the `:2276` stale comment correction
|
||||
landed; the 2026-07-16 pseudocode `enter_world` correction landed. Row count
|
||||
49→48 is consistent.
|
||||
- **Build.** `dotnet build AcDream.slnx -c Debug` exit 0 at the reviewed tree.
|
||||
|
||||
---
|
||||
|
||||
## Judgment on the `_pendingDestination` lifetime
|
||||
|
||||
**The cached destination's lifetime is correct. I found no way to Place
|
||||
against a superseded destination, and the Place-time re-read it replaced
|
||||
protected against nothing.**
|
||||
|
||||
The reasoning, checked against source:
|
||||
|
||||
1. **The bug was real and total.** `RuntimeWorldTransitState
|
||||
.TryBeginPortalReveal:159-183` clears `_hasAcceptedDestination` and
|
||||
`_acceptedDestination` at `:180-181` on success.
|
||||
`TryGetAcceptedTeleportDestination:522-527` returns `_teleportActive &&
|
||||
_hasAcceptedDestination`. Since `AimDestination` drives
|
||||
`TryBeginPortalReveal` through `WorldRevealCoordinator.TryBeginPortal`
|
||||
(`:742`), the slot is empty at every Place edge. The old re-read could
|
||||
never succeed — every real portal placement would have refused with
|
||||
`cause=host-token-unavailable`. Not a stale-destination guard; a hard
|
||||
failure.
|
||||
|
||||
2. **Write/clear is in exact lockstep with `_pendingCell`.**
|
||||
`_pendingRotation`/`_pendingCell`/`_pendingDestination`/
|
||||
`_hasPendingDestination` are written together at `:780-783` and cleared
|
||||
together at `:801-805` in `ResetTransit`. `_pendingCell != 0u` is itself the
|
||||
`haveDestination` predicate (`:481`), so the two cannot diverge.
|
||||
`_pendingDestination.Position.ObjCellId` **is** `_pendingCell` by
|
||||
construction (`Position position = destination.Position;` at `:715`).
|
||||
|
||||
3. **A second Aim cannot produce a mismatched pair.** Supersession by a new
|
||||
F751 goes through `OnTeleportStarted` → `ResetTransit(clearSession: false)`,
|
||||
which clears all four fields and bumps `_lifetimeGeneration`. Supersession
|
||||
by a second destination on the *same* sequence is impossible:
|
||||
`OfferTeleportDestination:490-497` returns `false` once `_destinationAccepted`
|
||||
is set, and `TryBeginPortalReveal` clears only `_hasAcceptedDestination`,
|
||||
leaving `_destinationAccepted` latched for the life of the reveal. So the
|
||||
destination is pinned from Aim to terminal, by the transit itself.
|
||||
|
||||
4. **The one torn window fails closed.** `_pendingRevealGeneration` is written
|
||||
at `:749`, before the `IsCurrentLifetime`/recenter guards at `:750`,
|
||||
`:757`, `:764`, while the other three are written at `:780-783`. A `false`
|
||||
return from any of those guards leaves a NEW generation paired with an OLD
|
||||
cell/destination. Both Place-edge gates then refuse:
|
||||
`CanPlacePortalDestination(newGen, seq, oldCell)` fails
|
||||
`IsCurrentPortalDestination`'s `destinationCell == _snapshot.DestinationCell`
|
||||
check, and `TryRegisterHostProjection(newGen, oldCell)` fails the same
|
||||
comparison at `RuntimeWorldTransitState.cs:197-209`. Neither can commit a
|
||||
stale pair. (This tearing predates the slice — `_pendingCell` already had
|
||||
it; `_pendingDestination` does not worsen it.)
|
||||
|
||||
5. **Terminal clearing.** Commit → `FireLoginComplete` → `ResetTransit`.
|
||||
Session reset / generation reset → `ResetSession` / `ResetGenerationPresentation`
|
||||
→ `ResetTransit`. Cancellation through `ResetTransit(clearSession: false)`.
|
||||
A *refused* Place leaves the fields set — but so does `_pendingCell`, and
|
||||
both Place-edge gates are keyed on the reveal generation, so a retained
|
||||
value is inert until a new Aim overwrites it or a reset clears it.
|
||||
|
||||
The one thing the caching genuinely costs is test coverage, not correctness:
|
||||
A6 removed the only assertion that could have distinguished a stale cached
|
||||
destination from a fresh one. Restore it.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| # | Severity | Finding |
|
||||
|---|---|---|
|
||||
| A1 | MAJOR | Refused Place does not stop the anim stream; player released into the world unplaced. P4 undischarged. |
|
||||
| A2 | MAJOR | `DeferredCell` park commits after `EndTeleport`: body/presentation split + unconsumable Place receipt (P3 graphical half, D-T2.4 re-validation missing). |
|
||||
| A3 | MEDIUM | Headless discards the arm's status; `is not null` guard silently disables placement. |
|
||||
| A4 | MEDIUM | `route.ZeroVelocity`/`ConstrainPhase`/`TeleportHookPhase` never read; inversions hardcoded; contract sabotage 3 cannot fail. |
|
||||
| A5 | MEDIUM | §8 item 8 presentation suite missing; local-player shadow invariant unasserted, and a real cache/publish desync sits behind it. |
|
||||
| A6 | MEDIUM | Superseded-teleport test lost its destination discriminator; two sibling tests weakened, one now contradicts its name. |
|
||||
| A7 | MEDIUM | Dual-host parity test obligation (§8 item 6) not met; headless flip untested. |
|
||||
| A8 | MINOR | Portal-test cleanup outside `finally` masks the first failure behind a Dispose throw. |
|
||||
| A9 | MINOR | Two sources for the teleport sequence (transit vs destination). |
|
||||
| A10 | MINOR | Class doc and plan correction each assert behaviour the code does not have. |
|
||||
| A11 | MINOR | Process-global `LocalTeleportHostKind` (accepted, noted). |
|
||||
| A12 | MINOR | Synthetic indoor cell is geometry-free — fine as a gate, weak as a placement oracle. |
|
||||
434
docs/research/2026-08-04-c4-route-3-retail-review-round2.md
Normal file
434
docs/research/2026-08-04-c4-route-3-retail-review-round2.md
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
# C4 route 3 — retail-conformance review, round 2 (delta) — 2026-08-05
|
||||
|
||||
**Verdict: FAIL — but a near miss.** All three round-1 MAJOR retail
|
||||
findings (R1, R3, R8) are genuinely fixed, and the R1 fix is the right
|
||||
shape for the right retail reason. What blocks is small and cheap: one
|
||||
unsound "the park committed" inference that reopens R1's failure mode on
|
||||
a narrow path (**N1**), one approximation shipped with a code comment
|
||||
instead of the register row the project's binding rule requires
|
||||
(**R7**), and one transient condition converted into a fatal exception on
|
||||
the headless endurance path (**N3**).
|
||||
|
||||
**R4/A5 (the presentation suite) explicitly does NOT block** — see §D.
|
||||
That call changed from round 1 because the A10 correction is true: I
|
||||
verified the canonical Place receipt, not the suffix, is the render
|
||||
entity's mover, and both halves of that path already have tests.
|
||||
|
||||
Scope: delta against my round-1 report
|
||||
(`2026-08-04-c4-route-3-retail-review.md`). Same working tree, HEAD
|
||||
`cd3129e9`, uncommitted. Review only.
|
||||
|
||||
---
|
||||
|
||||
## §A — round-1 findings: disposition
|
||||
|
||||
| # | round-1 finding | round-2 status |
|
||||
|---|---|---|
|
||||
| R1 | `Place` one-shot; no re-attempt driver; reveal completes with an unplaced body | **FIXED** — §B, and the fix is retail-correct |
|
||||
| R2 | headless discards the arm's status, acks a materialization that never happened | **FIXED** — §B.3 |
|
||||
| R3 | probe's `leash` field can never read `armed` | **FIXED** — `Constraint?.IsConstrained` at `RuntimeAcceptedPositionDriveController.cs:838` |
|
||||
| R4 | App presentation suite missing; assertions weakened | **PARTIALLY closed, does not block** — §D |
|
||||
| R5 | headless dual-host parity untested | **FIXED** — `HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake` |
|
||||
| R6 | `enter_world` caller-sweep correction mis-stated the retail record | **FIXED and independently re-verified** — §C.4 |
|
||||
| R7 | retail's movement refresh is autonomy-gated; acdream's was not | **PARTIALLY fixed — BLOCKS on register discipline** — §C.3 |
|
||||
| R8 | probe fired only on commit; refusals invisible under the pinned env var | **MOSTLY fixed** — two App-side causes remain invisible, §E.2 |
|
||||
| R9 | stale "TryBeginPortal (below)" | **FIXED** (`:751` now reads "above") |
|
||||
| R10 | `AD-131` does not exist | **FIXED** → `AP-131` |
|
||||
| R11 | probe hardcoded `autorunCancelled: true` | **FIXED** — reads `CancelAutoRun()`'s bool |
|
||||
|
||||
---
|
||||
|
||||
## §B — the R1 fix: correct, and correct for the right retail reason
|
||||
|
||||
`LocalPlayerTeleportController.cs:527`
|
||||
|
||||
```csharp
|
||||
bool placementReady = dataReady && TryAdvancePortalCommit(sequence);
|
||||
...
|
||||
var (_, events) = _presentation.Tick(deltaSeconds, placementReady);
|
||||
```
|
||||
|
||||
Inverting the readiness feed instead of touching the sequencer is the
|
||||
right call, for a reason worth recording: **it is retail's own shape.**
|
||||
Retail holds the player in portal space on `CellManager::blocking_for_cells`
|
||||
and `SmartBox::UseTime` @0x00455410 runs only `CheckPrefetchStatus` until
|
||||
the destination is usable — the hold lives in the *readiness predicate*,
|
||||
not in the animation. Feeding the sequencer "the canonical commit has
|
||||
already happened" rather than "the data is ready" reproduces that hold
|
||||
without changing a single sequencer timing (stop condition 2 respected).
|
||||
`TeleportAnimSequencer.cs` is untouched — confirmed by `git diff`.
|
||||
|
||||
### B.1 — arrival ORDER is preserved; Inversion B intact
|
||||
|
||||
Verified by tracing the single tick on which the commit succeeds:
|
||||
|
||||
1. `TryAdvancePortalCommit` → `TryExecuteAcceptedPortalArrival` →
|
||||
`TryPrepareAndSubmitAuthoredPlacement` → **canonical body commit**
|
||||
(`CommitCanonical`, retail `SetPositionSimple` @0x005162B0).
|
||||
2. `ReconcileAndAcknowledgePortal` → `CommitCanonicalTeleportFrame`
|
||||
(UnStick @0x00514EEE / UnConstrain @0x00514F02 / re-arm @0x0045418A,
|
||||
velocity zero @0x004541B4, StopCompletely) — **after** the placement.
|
||||
3. `CancelAutoRun()` + movement refresh — the `PlayerTeleported`
|
||||
@0x006B32B0 port, **after** the hook tail.
|
||||
4. Only then does the sequencer see `worldReady=true`, emit `Place`, and
|
||||
run the presentation suffix (`NotifyTeleported`, camera reset,
|
||||
reconcile) and `ObserveMaterialized`.
|
||||
|
||||
Retail: `SetPositionSimple` @0x00453924 → `PlayerPositionUpdated`
|
||||
@0x00453932 → `teleport_hook` @0x004538AE → `PlayerTeleported`
|
||||
@0x004538B3 → `set_viewer` @0x004538D5. **Same order.** Inversion B
|
||||
(local hook AFTER placement) holds; the gating did not move it.
|
||||
|
||||
Two sub-order deltas versus retail, both traced and both **unobservable**
|
||||
— stated so a future reader does not re-derive them:
|
||||
|
||||
- Retail's `ConstrainTo` @0x0045418A and `set_velocity` @0x004541B4 run
|
||||
in `HandleReceivedPosition` *after* `PlayerPositionUpdated` returns,
|
||||
i.e. after `SendMovementEvent` and `set_viewer`. acdream runs both
|
||||
inside `CommitCanonicalTeleportFrame`, before them. Nothing reads the
|
||||
leash between those points, and `MoveToStatePack` @0x006B4720 packs
|
||||
`InqRawMotionState` + `m_position` + contact + longjump + timestamps —
|
||||
**no velocity** — so the outbound bytes are identical either way.
|
||||
- `NotifyTeleported()` (teleport_hook's TargetManager teardown) now runs
|
||||
in the presentation suffix, i.e. after `PlayerTeleported`, where retail
|
||||
runs the whole hook before it. Purely local; no interaction with the
|
||||
outbound send.
|
||||
|
||||
### B.2 — the refusal path is now genuinely held, and tested with teeth
|
||||
|
||||
`RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears`
|
||||
(App.Tests) forces a real Runtime `Contention` by taking the entity's
|
||||
placement token, drives **100 ticks at 0.1 s** — 10 s, roughly 3× the
|
||||
tunnel's own 2–5 s timing, well past where the pre-fix code fired
|
||||
`FireLoginComplete` — and asserts the positive facts: body unmoved,
|
||||
`IsActive`, `Snapshot.Completed == false`, `LoginCompleteCount == 0`.
|
||||
It then releases the competing operation and asserts the very next tick
|
||||
commits at the offered destination. That is a real discriminator, not a
|
||||
negative-only assertion, and it directly kills R1.
|
||||
|
||||
A permanent refusal now holds in the tunnel showing retail's centered
|
||||
wait cue (`_holdSeconds` accumulates on `!placementReady`) until
|
||||
supersession or session reset — which is exactly what contract D-T5
|
||||
pinned and §4 item 4 requires.
|
||||
|
||||
### B.3 — headless
|
||||
|
||||
`PrepareDestination` now throws if no drive is wired, gates the attempt
|
||||
on `_collision.IsReady(destination.CellId)`, and returns
|
||||
`IsCollisionReady: committed`;
|
||||
`RuntimeLiveEntitySessionController.TryAdvancePortalCompletion` returns
|
||||
early on `!IsCollisionReady`, retried by `PumpPortalCompletion` from
|
||||
`HeadlessSessionHost.Tick`. The materialization ack can no longer
|
||||
precede a placement. R2 closed.
|
||||
|
||||
### B.4 — P3 (no wedgeable portal receipt) is now ESTABLISHED
|
||||
|
||||
Neither review had closed this. `RuntimePlacementProjectionSubscription.OnPlacement`
|
||||
(`:122-150`) calls `_sink.TryApply(in head)` **synchronously on publish**,
|
||||
so a portal-carrying Place receipt is applied and acknowledged inside the
|
||||
commit call, while the transit is still active and
|
||||
`IsCurrentPlacementAuthority` still holds. This matters most on headless,
|
||||
where `TryAdvancePortalCompletion` runs `Complete` + `EndTeleport`
|
||||
synchronously right after the commit and the FIFO is only republished at
|
||||
the end of the same `Tick` — had delivery been deferred, that receipt
|
||||
would have failed the portal gate forever and wedged the placement FIFO
|
||||
for every entity. It does not. **P3 satisfied on both hosts.**
|
||||
|
||||
---
|
||||
|
||||
## §C — retail verification of the round-2 changes
|
||||
|
||||
### C.1 — A4: the route fields carry retail semantics (with one granularity defect)
|
||||
|
||||
Verified in `RuntimeAuthoritativePositionRouteClassifier.cs:164-171`:
|
||||
|
||||
- `ConstrainAfterRouting => ConstrainPhase is AfterPositionOperation` —
|
||||
**correct and discriminating.** It separates the three real retail
|
||||
cases: force (`None`, early return @0x0045409D), local teleport
|
||||
(`After`, `ConstrainTo` @0x0045418A), local non-teleport (`Before`,
|
||||
`ConstrainTo` @0x004541EC). The sabotage the contract's §8 item 11
|
||||
demanded can now fail as designed.
|
||||
- `ZeroVelocity` — read directly; retail `set_velocity` @0x004541B4.
|
||||
|
||||
**N4 (LOW) — `RunsTeleportHook` gates too much.**
|
||||
`RuntimeAcceptedPositionDriveController.cs:795-801` uses
|
||||
`route.RunsTeleportHook` (`TeleportHookPhase is not None`) to gate the
|
||||
**entire** `CommitCanonicalTeleportFrame` call. But that method does far
|
||||
more than retail's `teleport_hook` @0x00514ED0 (CancelMoveTo, UnStick,
|
||||
StopInterpolating, UnConstrain, TargetManager, report_collision_end): it
|
||||
also resets the render-lerp anchors, publishes `UpdateCellId`, runs
|
||||
StopCompletely, resets the input edges, and resets the object clock —
|
||||
none of which retail conditions on the hook. Retail's `SetPositionInternal`
|
||||
@0x00515330 does the frame/cell work unconditionally. Today the portal
|
||||
route always sets the phase, so there is no live effect; but a future
|
||||
`TeleportHookPhase.None` would silently skip the render-root cell publish
|
||||
— the doorway-FLAP class. Gate only the `UnStick`/`UnConstrain`/re-arm
|
||||
block on the hook phase.
|
||||
|
||||
`RunsTeleportHook` also collapses `Before` and `After` into one boolean.
|
||||
Harmless here because the call site is unconditionally post-commit, but
|
||||
it means the field cannot express Inversion B by itself.
|
||||
|
||||
### C.2 — the "sequence" plumbing
|
||||
|
||||
`TryExecuteCanonicalPortalPlacementCore` now takes the authority's
|
||||
`TeleportSequence` from `_pendingDestination` and `Debug.Assert`s it
|
||||
equals the transit's `ActiveTeleportSequence`. Sound: the two can only
|
||||
diverge through a bug, and `RuntimeWorldTransitState.OfferTeleportDestination:495-503`
|
||||
accepts **exactly one** destination per active sequence
|
||||
(`if (_destinationAccepted) return false;`), so the Aim-time snapshot is
|
||||
unique per sequence by construction. (This also disposes of a hazard I
|
||||
went looking for: a second destination cannot supersede within a
|
||||
sequence, and a new sequence routes through `OnTeleportStarted` →
|
||||
`ResetTransit`, which clears `_placementCommitted`/`_awaitingDeferredWake`
|
||||
at `:932`.)
|
||||
|
||||
### C.3 — R7: the autonomy gate — **the approximation is real and needs its register row (BLOCKING)**
|
||||
|
||||
The retail reading is now exactly right, and I verified both halves:
|
||||
|
||||
- `CommandInterpreter::UsePositionFromServer` @0x006B3B40 (pseudo-C
|
||||
`:699506-699510`) is literally `return this->autonomy_level != 2`.
|
||||
acdream's `RuntimeCharacterState.UsePositionFromServer` (`:122`) is
|
||||
`AutonomyLevel != FullAutonomyLevel(2)` — **an exact port.**
|
||||
- `CommandInterpreter::SendMovementEvent` @0x006B4680 gates on
|
||||
`this->autonomy_level != 0` (@0x006B46BB, pseudo-C `:700283`).
|
||||
|
||||
So `!UsePositionFromServer` sends at level 2 only; retail sends at levels
|
||||
1 **and** 2. The implementer's characterisation is accurate, and the
|
||||
divergence is currently unreachable — `TrySetAutonomyLevel` has **zero
|
||||
production callers** (only `RuntimeCharacterStateTests`), so
|
||||
`AutonomyLevel` is always 2 and the two gates agree. Retail's own default
|
||||
is 2 (`command_line_autonomy_level = 0x2`, pseudo-C `:1088429`).
|
||||
|
||||
That is precisely what an approximation is: correct today, wrong the day
|
||||
someone wires level 1. CLAUDE.md's register rule is binding and admits no
|
||||
implementer discretion — *"Any commit that introduces a deviation (an
|
||||
adaptation, an approximation, a stopgap, a 'retail does X but we…') adds
|
||||
its register row IN THE SAME COMMIT. … A deviation found without a row is
|
||||
a bug twice over."* The shipped disposition is a code comment at
|
||||
`RuntimeAcceptedPositionDriveController.cs:812-824` saying *"not
|
||||
register-worthy on its own (no user-visible#-labeled symptom yet)"*. A
|
||||
symptom is not the threshold; a deviation is. Either thread the raw
|
||||
`AutonomyLevel` through (the exact port, and the constructor already
|
||||
takes two optional funcs so the marginal cost is one more) or add the
|
||||
row. A comment is not the register.
|
||||
|
||||
### C.4 — R6: the corrected `enter_world` banner is now accurate
|
||||
|
||||
Re-verified independently against the pseudo-C, not merely re-read:
|
||||
|
||||
- `:93797` @0x004550EC and `:93824` @0x00455095 both sit inside
|
||||
**`SmartBox::HandleCreateObject` @0x00454C80**. ✓
|
||||
- `CObjectMaint::CreateObject` is invoked at @0x00454FD8 *inside* that
|
||||
function — a callee, not the enclosing scope. ✓
|
||||
- @0x004550EC is in the `if (arg3 != this->player_id)` **non-player**
|
||||
branch; only @0x00455095 follows `SmartBox::init_player` +
|
||||
`CellManager::ChangePosition` in the player branch. ✓
|
||||
- The load-bearing negative still holds: the `Position*` overload
|
||||
@0x00516310 has exactly those two callers and the `int` overload
|
||||
@0x00516170 is reached only from @0x00516327 — `enter_world` is not on
|
||||
the portal path.
|
||||
|
||||
The banner now states all four facts correctly. This citation is safe for
|
||||
future sessions to cite.
|
||||
|
||||
---
|
||||
|
||||
## §D — R4/A5: still open, and it does NOT block. Here is why the call changed.
|
||||
|
||||
Round 1 rated this MAJOR on the premise (taken from contract §3.4) that
|
||||
the sink *"writes no pose"*, making the suffix the render entity's only
|
||||
mover. **That premise was wrong, and the A10 correction is right.** I
|
||||
verified `LiveEntityRuntime.TryApplyRuntimePlacementPlace`
|
||||
(`LiveEntityRuntime.cs:1386-1420`): a `Place` receipt calls
|
||||
`entity.SetPosition(projection.WorldPosition)`, sets `entity.Rotation`,
|
||||
sets `entity.ParentCellId = token.ExactCellId`, and calls
|
||||
`_spatial.RebucketLiveEntity` — `commitPose` defaults true and is passed
|
||||
`false` only for `WithdrawalRestored` (`:1364-1372`). The canonical
|
||||
receipt IS the mover; the suffix's writes are redundant repeats, exactly
|
||||
as the rewritten class comment now says. The comment is verified true.
|
||||
|
||||
With that established, the coverage picture is materially different from
|
||||
round 1:
|
||||
|
||||
- the sink's Place-receipt render-entity reframe + rebucket —
|
||||
**covered** (`RuntimePlacementPresentationSinkTests.Place_ReframesAndRebucketsExactSidecarWithoutMutatingRuntimePhysics`);
|
||||
- the sink's **portal gate discriminating** — **covered**
|
||||
(`PortalPlace_RequiresExactCurrentTransitHostAndSequence`, which drives
|
||||
a valid authority and a mismatched sequence);
|
||||
- the suffix's own entity write + destination bucket ordering —
|
||||
**covered** (the two `ConcretePlacement_*` tests, adapted to the new
|
||||
signature against a real `LiveEntityRuntime`/`WorldEntity`/`GpuWorldState`);
|
||||
- the **canonical body** pose and cell after an App-driven portal commit
|
||||
— **now covered**: the four weakened assertions were restored against
|
||||
`harness.Movement.Controller.Position`/`.CellId`, including A6's
|
||||
superseded-destination discriminator, and the App harness now runs a
|
||||
real Runtime rather than a fake placement.
|
||||
|
||||
What remains uncovered is narrower than "the presentation suite was not
|
||||
built": no single test composes teleport-controller → canonical commit →
|
||||
real sink → suffix; the **local-player collision shadow** after a portal
|
||||
commit (#312's own layer) is unasserted; the T8 overwrite ordering is
|
||||
unasserted; and the refused-Place test asserts body/LoginComplete/IsActive
|
||||
but not the render entity.
|
||||
|
||||
That is a genuine gap and it is #312-adjacent, so it must be recorded and
|
||||
closed in C5 — the implementer flagged it explicitly rather than claiming
|
||||
closure, which is the right behaviour. But it is no longer a
|
||||
"nothing asserts the only mover" hole, and it does not block this slice.
|
||||
|
||||
---
|
||||
|
||||
## §E — new findings
|
||||
|
||||
### N1 — MEDIUM — `PendingCount == 0` does not mean "the portal committed"; R1's failure mode survives on a narrow path
|
||||
|
||||
`LocalPlayerTeleportController.cs:653-657`
|
||||
|
||||
```csharp
|
||||
if (_awaitingDeferredWake)
|
||||
{
|
||||
if (_acceptedPositionDrive.PendingCount != 0)
|
||||
return false;
|
||||
_awaitingDeferredWake = false;
|
||||
_placementCommitted = true; // ← inference
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
and identically `HeadlessSessionWorldProjection.cs:818`
|
||||
(`committed = _acceptedPositionDrive.PendingCount == 0;`).
|
||||
|
||||
`PendingCount` is `_pending is null ? 0 : 1`
|
||||
(`RuntimeAcceptedPositionDriveController.cs:330`) — a shared, arm-agnostic
|
||||
counter. Three `Advance` paths clear a **portal** pending, and only one
|
||||
of them commits:
|
||||
|
||||
| `Advance` site | commits? | logs? |
|
||||
|---|---|---|
|
||||
| `:918` completed-wake, authority current | yes → `ReconcileAndAcknowledgePortal` | yes |
|
||||
| `:918` completed-wake, authority stale | body committed by `RetryDeferred`, **suffix skipped** | `AbandonedAtWake` |
|
||||
| `:973` **watch died** — "most likely a subsequent accepted Position's merge-time Forget" | **no** | **no line at all** |
|
||||
| `:1020` `!IsPlacementCurrent` on the prepare-retry | **no** | **no line at all** |
|
||||
|
||||
On the last two the caller latches `_placementCommitted = true` for a
|
||||
placement that never happened: the sequencer is released, `Place` fires,
|
||||
`ObserveMaterialized` acks a materialization that did not occur, and the
|
||||
player is revealed at the **origin**. That is exactly R1's shape,
|
||||
re-entered through the deferred door.
|
||||
|
||||
Reachability is genuinely low — both hosts now gate the attempt behind
|
||||
collision readiness (`_worldReveal.Evaluate(...).IsReady` graphically,
|
||||
`_collision.IsReady(...)` headless), so a park is rare — and the
|
||||
Runtime-layer test `PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale`
|
||||
proves at least one non-committing convergence exists
|
||||
(`Assert.Equal(0, drive.PendingCount)` with no reconcile). Nothing tests
|
||||
the caller's inference against it.
|
||||
|
||||
The fix is small: have the drive report a portal-specific terminal
|
||||
outcome (it already distinguishes them well enough to log
|
||||
`AbandonedAtWake`) instead of the caller inferring commit from a shared
|
||||
counter. Also give the two silent branches a probe line — under D-T8 they
|
||||
are portal-arrival attempts that ended.
|
||||
|
||||
### N2 — LOW/MEDIUM — headless `IsUnhydratable` is now hardcoded false
|
||||
|
||||
`HeadlessSessionWorldProjection.cs:874` reports `IsUnhydratable: false`
|
||||
unconditionally, where it previously reported `!ready`. The old value was
|
||||
itself a conflation (not-yet-resident ≠ unhydratable), so this is not a
|
||||
regression in meaning — but headless can now never report an
|
||||
unhydratable destination, so a genuinely unhydratable claim spins in
|
||||
`PumpPortalCompletion` forever instead of taking AD-2's stated "loud
|
||||
unhydratable-placement path". Either derive the real predicate or state
|
||||
in the method's doc that headless does not model it.
|
||||
|
||||
### N3 — MEDIUM — a transient headless condition is now fatal
|
||||
|
||||
`HeadlessSessionWorldProjection.cs:854` throws `InvalidOperationException`
|
||||
on the `default:` arm, which covers **`NotApplicable`** as well as
|
||||
`Rejected`. `NotApplicable` is returned by
|
||||
`TryExecuteAcceptedPortalArrival` for `record.PhysicsBody is null` and
|
||||
for an active initial-Create residence
|
||||
(`RuntimeAcceptedPositionDriveController.cs:243-251`) — hydration-race
|
||||
conditions, not "the reveal is stale". The exception message asserts a
|
||||
diagnosis ("the reveal itself is stale or the local player has no
|
||||
canonical body, neither recoverable by waiting") that is true for
|
||||
`Rejected` and not established for `NotApplicable`.
|
||||
|
||||
Refusing to fake success is right; converting a possibly-transient
|
||||
condition into a process-killing throw on the host that must survive
|
||||
K4's 30-session / two-hour endurance profile is the wrong end of that
|
||||
trade. Split the arm: throw on `Rejected`, treat `NotApplicable` as a
|
||||
retryable wait with a bounded attempt budget (or a loud log plus
|
||||
`IsCollisionReady: false`).
|
||||
|
||||
### N4 — LOW — `RunsTeleportHook` over-gates the frame commit (§C.1)
|
||||
|
||||
### N5 — LOW — the plan-doc correction contradicts itself
|
||||
|
||||
`docs/plans/2026-08-02-placement-cutover.md` now opens *"This line was
|
||||
accurate for both halves at the time it was written"*, explains that
|
||||
route 3 added the consumer and producer together, and then closes with
|
||||
*"'Zero producing call sites; the adapter does not exist' was accurate
|
||||
only for the producer half"* — which contradicts the opening sentence.
|
||||
The substance is right (the type, `IsValid`, `Pending.Portal`, the sinks'
|
||||
gates and `BeginAcceptedPlacementCore`'s gate pre-dated route 3; the arm
|
||||
did not); the paragraph needs one pass so a future reader can cite it.
|
||||
|
||||
---
|
||||
|
||||
## §E.2 — R8 residual
|
||||
|
||||
`LogPortalArrivalAttempt` now fires on every Runtime-side exit under the
|
||||
gate's own `ACDREAM_PROBE_LOCAL_TELEPORT`, and a live `Contention`
|
||||
refusal was observed — R8's substance is closed and **§9's gate is now
|
||||
passable as specified**: `leash=armed` reads from
|
||||
`ConstraintManager.IsConstrained`, which `ConstrainTo` @0x00556240 sets
|
||||
unconditionally, so a committed arrival prints `armed`.
|
||||
|
||||
Two App-side refusal causes still never reach Runtime and therefore emit
|
||||
no `[local-tp]` line: `cause=stale-reveal`
|
||||
(`LocalPlayerTeleportController.cs:666`, the `CanPlacePortalDestination`
|
||||
preflight) and `cause=host-token-unavailable` (`:737`). Both log through
|
||||
`PhysicsDiagnostics.LogTeleport`, gated by the *different*
|
||||
`ACDREAM_PROBE_TELEPORT`. Under the pinned gate environment these are
|
||||
invisible. Either route them through `LogLocalTeleportArrival` or add
|
||||
`ACDREAM_PROBE_TELEPORT=1` to §9's environment line.
|
||||
|
||||
---
|
||||
|
||||
## §F — gate evidence
|
||||
|
||||
- **Release build**: green, 0 warnings / 0 errors.
|
||||
- **`AcDream.Runtime.Tests`**: 1,164 passed / 0 failed / **0 skipped**.
|
||||
- **`AcDream.Headless.Tests`**: 85 passed / 0 failed / **0 skipped**.
|
||||
- **`LocalPlayerTeleportControllerTests`**: 21 passed / 0 failed / **0 skipped**.
|
||||
- No `Skip` attribute in any touched test file. No test weakened: the
|
||||
four round-1 weakenings were restored with stronger targets (the real
|
||||
canonical body rather than the deleted fake's captured argument), and
|
||||
three genuinely new discriminators were added (`RefusedPlace_…`,
|
||||
`PortalCommitted_UnderServerControlSendsNoMovementEvent`,
|
||||
`PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale`,
|
||||
plus the headless park test).
|
||||
- Per process rule 3, none of this is evidence of correctness — it is
|
||||
evidence that nothing regressed while the above holes remain.
|
||||
|
||||
---
|
||||
|
||||
## §G — what must land to pass
|
||||
|
||||
1. **N1** — stop inferring commit from `PendingCount`; report a
|
||||
portal-specific terminal outcome, and log the two silent
|
||||
non-committing `Advance` branches.
|
||||
2. **R7** — add the AD register row for the autonomy approximation, or
|
||||
thread the raw `AutonomyLevel` through and make it exact. Binding
|
||||
project rule; not an implementer judgment call.
|
||||
3. **N3** — do not throw on `NotApplicable`; split it from `Rejected`.
|
||||
|
||||
Cheap follow-ups, non-blocking: N2, N4 (gate only the UnStick/UnConstrain/
|
||||
re-arm block on `RunsTeleportHook`), N5, and R8's two App-side causes.
|
||||
|
||||
Record as a dated, named C5 item: R4/A5's residual — the end-to-end
|
||||
composition test, the local-player collision shadow after a portal
|
||||
commit, and the T8 overwrite ordering.
|
||||
515
docs/research/2026-08-04-c4-route-3-retail-review.md
Normal file
515
docs/research/2026-08-04-c4-route-3-retail-review.md
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
# C4 route 3 — retail-conformance review (2026-08-04)
|
||||
|
||||
**Verdict: FAIL.**
|
||||
|
||||
Reviewer scope: the uncommitted working-tree diff at HEAD `cd3129e9`
|
||||
(`git diff HEAD` + untracked), branch
|
||||
`claude/acdream-physics-divergence-5aa784`. Review only — no edits made.
|
||||
|
||||
The retail *reading* in this slice is excellent. Every §1 claim in the
|
||||
pinned contract reproduces line-for-line in
|
||||
`docs/research/named-retail/acclient_2013_pseudo_c.txt` (§A below), both
|
||||
inversions are implemented in the right direction, the D-T3 duty map is
|
||||
complete, and the implementer's P1 finding on TransientState is not just
|
||||
correct — it retires a real pre-existing divergence.
|
||||
|
||||
The slice fails on **what happens when the placement does not commit**.
|
||||
`TeleportAnimEvent.Place` is one-shot, so every non-`Committed` outcome
|
||||
silently skips the placement, the presentation suffix, and the
|
||||
materialization acknowledgement while the animation stream marches on to
|
||||
reveal the world and fire LoginComplete anyway. The headless host has the
|
||||
same hole with the extra property that it *asserts a materialization that
|
||||
did not happen*. Neither is covered by a test, because the App-layer
|
||||
presentation suite the contract made mandatory (§8 items 8/9/10, closing
|
||||
route 2's B2 gap) was not written — the existing App assertions were
|
||||
weakened instead. And the one probe field the connected gate keys on
|
||||
(`leash=armed`) can never be true as coded.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### R1 — MAJOR — `TeleportAnimEvent.Place` is one-shot; there is no re-attempt driver, and the reveal completes anyway
|
||||
|
||||
`src/AcDream.Core/World/TeleportAnimSequencer.cs:136-141`
|
||||
|
||||
```csharp
|
||||
case TeleportAnimState.Tunnel:
|
||||
if (worldReady)
|
||||
{
|
||||
evts.Add(TeleportAnimEvent.Place);
|
||||
Advance(TeleportAnimState.TunnelContinue, enterTunnel: false);
|
||||
```
|
||||
|
||||
The state advances in the **same tick** the event is emitted. `Place`
|
||||
never fires again for that reveal.
|
||||
|
||||
`src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:505-522`
|
||||
|
||||
```csharp
|
||||
case TeleportAnimEvent.Place:
|
||||
if (!_worldReveal.CanPlacePortalDestination(...)) return;
|
||||
if (!TryExecuteCanonicalPortalPlacement(sequence)) return; // :513
|
||||
...
|
||||
_placement.Place(_pendingRotation); // :517
|
||||
...
|
||||
_worldReveal.ObserveMaterialized(...); // :520
|
||||
```
|
||||
|
||||
`TryExecuteCanonicalPortalPlacement` returns `true` **only** on
|
||||
`RuntimeAcceptedPositionExecutionStatus.Committed`
|
||||
(`LocalPlayerTeleportController.cs:597-598`). Every other status —
|
||||
`Contention`, `Rejected`, `NotApplicable`, and notably `DeferredCell` —
|
||||
returns `false` and the `Tick` returns.
|
||||
|
||||
Consequences, all reachable:
|
||||
|
||||
- `_placement.Place` never runs → no `entity.SetPosition` /
|
||||
`ParentCellId` / `RebucketLiveEntity`, no `NotifyTeleported()`, no
|
||||
camera reset, no `_spatial.Reconcile()`.
|
||||
- `_worldReveal.ObserveMaterialized` never runs →
|
||||
`RuntimeWorldTransitState.AcknowledgePortalMaterialized` never fires →
|
||||
`Materialized` stays false.
|
||||
- The **next** tick still advances the sequencer:
|
||||
`TunnelContinue` → `TunnelFadeOut` → `PlayExitSound`
|
||||
(`RevealWorldViewport`) → `WorldFadeIn` → `FireLoginComplete`
|
||||
(`_mode.EnterWorld()` + `SendLoginComplete()` + `_worldReveal.Complete()`
|
||||
+ `ResetTransit`).
|
||||
- `RuntimeWorldTransitState.Complete` then trips
|
||||
`FailInvariant("portal-complete-before-materialized")`
|
||||
(`RuntimeWorldTransitState.cs:701-706`) and returns `false`;
|
||||
`WorldRevealCoordinator.Complete()` (`:268-276`) **discards** that
|
||||
`false`. `ResetTransit(clearSession:false)` then calls
|
||||
`_transit.EndTeleport()` + `_worldReveal.Cancel()`, so the ledger
|
||||
converges — but the reveal is recorded cancelled, not completed, with
|
||||
one invariant failure logged.
|
||||
|
||||
User-visible outcome on `Contention`/`Rejected`: the player is revealed
|
||||
into the destination world **standing at the origin position**, with
|
||||
LoginComplete sent. On `DeferredCell`: the body commits later at the
|
||||
collision-generation wake (`Advance` → `ReconcileAndAcknowledgePortal`),
|
||||
but the presentation suffix, camera reset, rebucket, and materialization
|
||||
ack are gone forever.
|
||||
|
||||
This is exactly what contract §4 items 4 and 5 forbid ("a refused
|
||||
placement must NOT … must not advance the anim-event stream's terminal
|
||||
events"; "never a half-state", "never a silent wedge in portal space")
|
||||
and it is the D-T5/P4 obligation the contract flagged in advance: *"if
|
||||
the Place anim event is one-shot, the re-attempt must be driven by the
|
||||
same Tick predicate that produced it, and THAT mechanism must be stated
|
||||
in the commit."* It is one-shot, and no mechanism was added.
|
||||
|
||||
The comment shipped in its place is false. `LocalPlayerTeleportController.cs:566-572`:
|
||||
|
||||
> "On any refusal this returns `false` without mutating anything — the
|
||||
> D-T5 refusal shape: … the transit's own cancellation/supersession
|
||||
> machinery is the authority on what happens next."
|
||||
|
||||
The transit is not the authority on what happens next. The animation
|
||||
sequencer is, and it does not wait.
|
||||
|
||||
**Correct behaviour:** either re-drive the Place edge from the same
|
||||
`ready`-gated Tick predicate until it commits (holding the sequencer in
|
||||
`Tunnel` — which is what retail's `blocking_for_cells` hold is), or
|
||||
cancel the reveal explicitly on refusal so the player is never revealed
|
||||
without a committed placement. Retail has no third option: it places
|
||||
unconditionally and immediately (`SmartBox::TeleportPlayer` @0x00453910)
|
||||
and only the *simulation* waits on prefetch.
|
||||
|
||||
---
|
||||
|
||||
### R2 — MAJOR — headless discards the arm's status and acknowledges a materialization that did not happen
|
||||
|
||||
`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:775`
|
||||
|
||||
```csharp
|
||||
_ = _acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
|
||||
destination,
|
||||
authority);
|
||||
```
|
||||
|
||||
The status is dropped on the floor. `PrepareDestination` then
|
||||
unconditionally returns a ready readiness report, and
|
||||
`RuntimeLiveEntitySessionController.TryCompletePortal` continues its
|
||||
fully-synchronous suffix: `AcknowledgeDestinationReadiness` →
|
||||
`AcknowledgePortalMaterialized` → `SimulationReleaseProjected` →
|
||||
`Complete` → `SendGameAction(LoginComplete)` → `EndTeleport`.
|
||||
|
||||
So on any refusal or park, the headless host **fires
|
||||
`AcknowledgePortalMaterialized` for a placement that never committed** —
|
||||
contract §4 item 5 and D-T5 rows 1/2 both state in terms that the
|
||||
materialization ack must fire only from the committed outcome. The bot
|
||||
reports a completed teleport while standing where it started, with no
|
||||
log line of any kind (see R8).
|
||||
|
||||
Retail contradiction is indirect but real: retail's
|
||||
`SmartBox::PlayerPositionUpdated` @0x00453870 clears
|
||||
`waiting_for_teleport` **inside the same call that performed
|
||||
`SetPositionSimple`** (@0x00453924 → @0x0045389A). The "wait is over"
|
||||
edge is downstream of the placement in retail; here it can precede a
|
||||
placement that never occurred.
|
||||
|
||||
---
|
||||
|
||||
### R3 — MAJOR — the D-T8 probe's `leash` field can never read `armed`; the connected gate as pinned is unpassable
|
||||
|
||||
`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs:694`
|
||||
|
||||
```csharp
|
||||
leashArmed: controller.PositionManager?.IsFullyConstrained() ?? false,
|
||||
```
|
||||
|
||||
Retail `ConstraintManager::IsFullyConstrained` @0x005560D0 is
|
||||
`constraint_distance_max * 0.9 < constraint_pos_offset` — "has strained
|
||||
past 90 % of the leash", the predicate `jump_is_allowed` reads. It is not
|
||||
"is the leash armed". The acdream port says so explicitly
|
||||
(`src/AcDream.Core/Physics/Motion/ConstraintManager.cs:79-89`).
|
||||
|
||||
Retail `ConstraintManager::ConstrainTo` @0x00556240 (pseudo-C
|
||||
:353528-353537) ends with
|
||||
`constraint_pos_offset = Position::distance(anchor, physics_obj->m_position)`;
|
||||
acdream mirrors it at `ConstraintManager.cs:65-71`. Because
|
||||
`RearmConstraintLeashAtCurrentPosition`
|
||||
(`PlayerMovementController.cs:1836-1845`) anchors at the body's **own**
|
||||
`CellPosition`, that distance is 0. `max * 0.9 < 0` is false.
|
||||
|
||||
Therefore every committed portal arrival prints `leash=unarmed`. The
|
||||
contract's §9 pass criterion — *"Pass requires ALL of … `leash=armed`"* —
|
||||
cannot be met, and a future reader hitting `leash=unarmed` would chase a
|
||||
phantom missing leash (the exact 4b-3 A1 defect class the contract
|
||||
warned about, inverted).
|
||||
|
||||
The correct observable is `ConstraintManager.IsConstrained`, which the
|
||||
Runtime test itself uses
|
||||
(`RuntimeAcceptedPositionDriveControllerTests`,
|
||||
`Assert.True(controller.PositionManager.Constraint!.IsConstrained)`).
|
||||
`PositionManager` does not currently surface it; it needs to.
|
||||
|
||||
---
|
||||
|
||||
### R4 — MAJOR — the mandatory App-layer presentation suite is missing, and the existing App assertions were weakened
|
||||
|
||||
`tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs`
|
||||
gained **zero** new `[Fact]`s. All five new tests in the diff are in
|
||||
`tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs`.
|
||||
|
||||
What changed in the App file is assertion *strength*, downward:
|
||||
|
||||
```diff
|
||||
- Assert.Equal(new Vector3(7f, 8f, 9f), harness.Placement.Position);
|
||||
+ Assert.True(harness.Placement.Called);
|
||||
```
|
||||
|
||||
(and the same substitution at eight further sites). The interface change
|
||||
makes the literal old assertion impossible, which is fine — but the
|
||||
contract required the replacement coverage and named it as load-bearing:
|
||||
|
||||
- §8 item 8: "after a committed portal placement through the REAL sink +
|
||||
suffix, the render `WorldEntity` position/rotation/`ParentCellId` equal
|
||||
the resolved body, the draw bucket moved, the local-player shadow
|
||||
agrees, and the sink's Place receipt was consumed with a VALID portal
|
||||
authority … **route 2's B2 coverage gap … becomes load-bearing here and
|
||||
MUST close**."
|
||||
- §8 item 9: the T8 overwrite ordering (wire pose then resolved pose).
|
||||
- §8 item 10: refused Place edge presentation.
|
||||
|
||||
None exist. Since `RuntimePlacementPresentationSink.TryPublishPlace`
|
||||
writes no pose (contract §3.4, re-confirmed), the suffix in
|
||||
`LocalPlayerTeleportPlacement.Place` is now the render entity's **only**
|
||||
mover — and nothing asserts it moves the entity to the resolved pose.
|
||||
Combined with R1 (where that suffix is skipped entirely on refusal), this
|
||||
is the #312 shape verbatim: process rule 4, "tests must assert the layer
|
||||
that broke."
|
||||
|
||||
---
|
||||
|
||||
### R5 — MAJOR — headless dual-host parity (§8 item 6, D-T6) has no coverage, self-declared open
|
||||
|
||||
`tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs:413-422`
|
||||
(added in this diff):
|
||||
|
||||
> "no accepted-position drive controller is wired into this fixture's
|
||||
> projection … so the canonical portal arm this method now calls is a
|
||||
> no-op here by construction (`_acceptedPositionDrive` is null) … a
|
||||
> headless-host-specific committed-portal test is an open item, not
|
||||
> attempted here given this session's time budget."
|
||||
|
||||
D2 (`ResynchronizeLocalPlayerForPortalArrival`, ~40 non-comment lines and
|
||||
AD-42's last citation) was deleted and its replacement has **zero**
|
||||
headless test coverage. D-T6 pinned this: *"dual-host parity is a test
|
||||
obligation, not an aspiration."* The honest disclosure is appreciated and
|
||||
does not change the finding.
|
||||
|
||||
Related, and unremarked in the diff: the deleted method also performed
|
||||
`controller.LocalEntityId = record.LocalEntityId ?? 0u`. Verified safe —
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.cs:214` sets it at publication
|
||||
and the entity key is stable across a portal — but the drop deserves a
|
||||
line in the commit message.
|
||||
|
||||
---
|
||||
|
||||
### R6 — MEDIUM — the `enter_world` caller-sweep correction mis-states the retail record it is correcting
|
||||
|
||||
`docs/research/2026-07-16-portal-completion-pseudocode.md` §2.1 banner:
|
||||
|
||||
> "a caller sweep … shows both `CPhysicsObj::enter_world` call sites
|
||||
> living inside `CObjectMaint::CreateObject`'s player branch
|
||||
> (`SmartBox::init_player` + `CellManager::ChangePosition` immediately
|
||||
> precede it)"
|
||||
|
||||
Verified independently. The two call sites are pseudo-C :93797
|
||||
(@0x004550EC) and :93824 (@0x00455095). Both live inside
|
||||
**`SmartBox::HandleCreateObject` @0x00454C80**, not
|
||||
`CObjectMaint::CreateObject` — the latter is merely a *callee* at
|
||||
@0x00454FD8 inside that same function. And they are **not both in the
|
||||
player branch**: @0x004550EC is in the `if (arg3 != this->player_id)`
|
||||
NON-player branch (`PhysicsDesc::get_position` → `enter_world(var_bc, …)`
|
||||
for a newly created remote object); only @0x00455095 sits in the player
|
||||
branch after `init_player` + `ChangePosition`.
|
||||
|
||||
The load-bearing NEGATIVE is **CONFIRMED**: the `Position*` overload
|
||||
@0x00516310 has exactly those two callers, the `int` overload @0x00516170
|
||||
is reached only from @0x00516327, and neither is on the portal path.
|
||||
`SmartBox::TeleportPlayer` → `SetPositionSimple` is correct.
|
||||
|
||||
But this banner is explicitly a correction to the retail record that
|
||||
future sessions will cite, and it is wrong in two of its three factual
|
||||
clauses. Same defect class as "a register row asserting behaviour the
|
||||
code does not have," applied to a research doc — the exact reason the
|
||||
contract ordered the correction in-slice.
|
||||
|
||||
---
|
||||
|
||||
### R7 — MEDIUM — retail's post-teleport movement refresh is autonomy-gated; acdream's is not
|
||||
|
||||
Contract open question (c), answered.
|
||||
`CommandInterpreter::SendMovementEvent` @0x006B4680 (pseudo-C
|
||||
:700274-700313):
|
||||
|
||||
```
|
||||
if ((player != 0 && this->smartbox != 0) && CPhysicsObj::InqRawMotionState(player) != 0)
|
||||
if (this->autonomy_level != 0)
|
||||
MoveToStatePack::MoveToStatePack(...)
|
||||
SendMoveToStateEvent(...)
|
||||
```
|
||||
|
||||
Two gates: a non-null raw motion state, and **`autonomy_level != 0`**.
|
||||
Under server control retail sends nothing.
|
||||
|
||||
`RuntimeAcceptedPositionDriveController.cs:678-681` calls
|
||||
`_localPlayerOutbound.TrySendMovement(...)` unconditionally;
|
||||
`LocalPlayerOutboundController.TrySendMovement:187-229` gates only on a
|
||||
resolvable outbound position. The controller already holds
|
||||
`UsePositionFromServer` (retail's `UsePositionFromServer()`), and
|
||||
`_usePositionFromServer` is already a field on this very class — the
|
||||
autonomy fact is in scope.
|
||||
|
||||
Everything else about the port checks out: the message family is
|
||||
`MoveToState` (retail packs `InqRawMotionState` into `MoveToStatePack`),
|
||||
the contact byte is `Contact && OnWalkable` in both, exactly one is sent,
|
||||
and no `AutonomousPosition` goes out (retail's teleport branch returns
|
||||
before `SendPositionEvent` — verified at @0x004541C0). Order is right:
|
||||
`CommitCanonicalTeleportFrame` (hook tail) → `CancelAutoRun` →
|
||||
movement send, matching @0x004538AE → @0x004538B3 → tail-jump.
|
||||
|
||||
Either add the autonomy gate or file the delta as a register row.
|
||||
|
||||
---
|
||||
|
||||
### R8 — MEDIUM — D-T8 emits one line per *committed* arrival, not per attempt; refusals are invisible under the pinned gate environment
|
||||
|
||||
`PhysicsDiagnostics.LogLocalTeleportArrival` is called from exactly one
|
||||
site, `ReconcileAndAcknowledgePortal`
|
||||
(`RuntimeAcceptedPositionDriveController.cs:687-696`), reached only on
|
||||
`CommittedHostAcknowledgementPending`. Its `placementStatus` argument is
|
||||
the literal `"Committed"`.
|
||||
|
||||
The graphical refusal path logs via `PhysicsDiagnostics.LogTeleport`
|
||||
(`LocalPlayerTeleportController.cs:582-583`, `:598-599`), which is gated
|
||||
by **`ACDREAM_PROBE_TELEPORT`** (`PhysicsDiagnostics.cs:1160-1161`) — a
|
||||
different env var from the `ACDREAM_PROBE_LOCAL_TELEPORT` the gate pins.
|
||||
The headless refusal path logs nothing at all (R2).
|
||||
|
||||
Net: with the contract's pinned gate environment, a refusal produces zero
|
||||
output on either host. D-T8 specified "One line per portal-arrival
|
||||
attempt: cause … placement status", and §9 requires "zero
|
||||
`Refused`/`Contention` lines in ordinary play" — unobservable as built.
|
||||
Given R1, an unobserved refusal is precisely the failure that would ship.
|
||||
|
||||
---
|
||||
|
||||
### R9 — LOW — stale directional reference in a comment added by this diff
|
||||
|
||||
`src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:775`:
|
||||
"TryBeginPortal (below) drives …". `_worldReveal.TryBeginPortal` is
|
||||
called **above** this comment, at `:741`, in the same method. Process
|
||||
rule 6.
|
||||
|
||||
### R10 — LOW — `AD-131` does not exist
|
||||
|
||||
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2292`
|
||||
cites "AD-2/AD-131/#275". The AD section has 48 rows. The row is
|
||||
**AP-131** (`docs/architecture/retail-divergence-register.md:283`), which
|
||||
is what the contract itself says. Introduced by this diff, in the very
|
||||
comment the slice rewrote to fix a stale comment.
|
||||
|
||||
### R11 — LOW — the probe asserts more than it observes
|
||||
|
||||
`RuntimeAcceptedPositionDriveController.cs:693-695` hardcodes
|
||||
`hookTailRan: true` and `autorunCancelled: true`.
|
||||
`RuntimeLocalPlayerMovementState.CancelAutoRun():226-234` returns `false`
|
||||
when autorun was already off (correctly mirroring retail's
|
||||
`SetAutoRun` @0x006B4850, which acts only on a state *change* at
|
||||
@0x006B4871). The field reports the action ran, not the state changed —
|
||||
report the returned bool.
|
||||
|
||||
---
|
||||
|
||||
## §A — retail claims verified independently (do not re-derive)
|
||||
|
||||
All against `docs/research/named-retail/acclient_2013_pseudo_c.txt`.
|
||||
|
||||
| claim | where | result |
|
||||
|---|---|---|
|
||||
| `SmartBox::TeleportPlayer` @0x00453910 = `SetPositionSimple(player, dest, 1)` @0x00453924 + `PlayerPositionUpdated(this, 1, FLT_MAX)` @0x00453932, nothing else | :92514-92523 | **CONFIRMED** — the generic path, route 2's exact primitive, third route running |
|
||||
| `PlayerPositionUpdated` teleport arm order: `position_update_complete=0` @0x00453890, `waiting_for_teleport=0` @0x0045389A, `has_been_teleported=0` @0x004538A4, `teleport_hook` @0x004538AE, `cmdinterp->PlayerTeleported()` @0x004538B3, `set_viewer` @0x004538D5, `LScape::update_viewpoint` @0x004538E2, `CellManager::ChangePosition` @0x00453903 | :92469-92509 | **CONFIRMED**, exactly the contract's order |
|
||||
| `CommandInterpreter::PlayerTeleported` @0x006B32B0 = `SetAutoRun(0,1)` + tail-jump `SendMovementEvent` | :699036-699041 | **CONFIRMED**. New: `SetAutoRun` @0x006B4850 only acts when `(arg2==0) != (auto_run==0)` (@0x006B4871) — acdream's `CancelAutoRun` early-return matches |
|
||||
| **Inversion A** — local TELEPORT branch @0x0045415F: `TeleportPlayer(&var_48)` @0x00454168 → `ConstrainTo(arg2, &var_48, start, max)` @0x0045418A → `set_velocity(player, {0,0,0}, 1)` @0x004541B4 → return | :93013-93023 | **CONFIRMED**, including the WIRE-destination anchor |
|
||||
| FORCE_POSITION branch returns @0x0045409D before every `ConstrainTo` | :92925-92933 | **CONFIRMED** — route 2's no-re-arm rule intact and correctly left force-scoped |
|
||||
| **Inversion B** — the local hook runs AFTER the placement (from `PlayerPositionUpdated`), opposite to the remote arm's @0x005163EF | :92497 vs 4b-3's citation | **CONFIRMED** |
|
||||
| `enter_world` is NOT on the portal path | :93797, :93824 | **NEGATIVE CONFIRMED** — but the attribution in the new correction banner is wrong; see R6 |
|
||||
| **P1 (TransientState not re-seeded)** — retail `CPhysicsObj::SetPositionInternal(CTransition*)` @0x00515330 derives Contact from `collision_info.contact_plane_valid` @0x00515430, WaterContact from `contact_plane_is_water` @0x00515453, OnWalkable from `set_on_walkable(contact_plane.N.z vs floor_z)` @0x00515467-0x0051548E, Sliding from `sliding_normal_valid` @0x005154E1. **No unconditional `Contact\|OnWalkable` seed anywhere.** | :283484-283519 | **THE FINDING IS CORRECT AND IS A FIDELITY GAIN.** `PhysicsObjUpdate.CommitSetPositionContactPrefix` (`src/AcDream.Core/Physics/PhysicsObjUpdate.cs:154-176`) is that exact port, and runs inside the canonical commit (`RuntimeSetPositionState.cs:5038`). The old `SetPositionCore` seed (`PlayerMovementController.cs:1859-1862`) was the divergence; dropping it is right. The `Active` argument also holds — `PlayerMovementController.cs:2069` and `:2449` re-set it every frame. |
|
||||
| `ConstraintManager::ConstrainTo` @0x00556240 initializes `constraint_pos_offset = distance(anchor, m_position)` | :353528-353537 | **CONFIRMED** — acdream matches; also the basis for R3 |
|
||||
| `CommandInterpreter::SendMovementEvent` @0x006B4680 is autonomy-gated | :700274-700313 | **CONFIRMED** — see R7 |
|
||||
|
||||
## §B — implementation facts verified correct
|
||||
|
||||
- **D-T3 duty map (P1) is complete.** All nine `SetPositionCore` duties
|
||||
land in `CommitCanonicalTeleportFrame`
|
||||
(`PlayerMovementController.cs:1993-2041`) or the canonical commit, in
|
||||
`SetPositionCore`'s own order (StopCompletely → input/mouse resets →
|
||||
UnStick/UnConstrain/re-arm → edge resets → clock reset). Nothing
|
||||
silently dropped except the TransientState seed, which is correct
|
||||
(§A).
|
||||
- **Inversions implemented in the right directions.** The portal arm
|
||||
consumes the classifier's dormant LocalPlayer-teleport branch
|
||||
(`RuntimeAuthoritativePositionRouteClassifier.cs:336-356`) unchanged;
|
||||
`ConstrainPhase.AfterPositionOperation` + `ZeroVelocity: true` +
|
||||
`SendPositionImmediately: false` + `TeleportHookPhase.AfterPositionOperation`
|
||||
all flow through. The hook tail runs only from the committed receipt.
|
||||
- **The force arm is untouched.** The only route-2 edits are one
|
||||
force-scoping doc sentence (`:133-137`) and a non-`required`
|
||||
`Portal { get; init; }` on `Pending` that defaults empty. Zero route-2
|
||||
test expectation changes — §4 item 8's tripwire is clean.
|
||||
- **The synthetic `priorTeleport = accepted - 1`** in
|
||||
`ClassifyPortalArrival` is sound: `TeleportAdvanced` reads only the
|
||||
boolean `PhysicsTimestampGate.IsNewer(prev, accepted)`, the branch's
|
||||
resulting route does not depend on the previous stamp's magnitude, and
|
||||
wrap is safe at `accepted == 0`.
|
||||
- **AD-42 deleted** (row gone, header 49 → 48 rows), **AD-2 amended in
|
||||
place** with the deferred-place timing, the T8 tolerated-overwrite
|
||||
note, and the leash-anchor nuance. Plan gap-line correction present.
|
||||
Register rules satisfied.
|
||||
- **Release build green.** Focused
|
||||
`RuntimeAcceptedPositionDriveControllerTests`: 22 passed / 0 failed /
|
||||
**0 skipped**. No `Skip` attribute remains in any of the four touched
|
||||
test files, and no Runtime test was weakened — the five new ones are
|
||||
strong (the happy path pre-arms the leash at a *stale* anchor so a
|
||||
missing re-arm fails the `ConstraintPos` assertion; the refusal tests
|
||||
assert positive "nothing moved, transit still active, no packets"
|
||||
facts).
|
||||
|
||||
## §C — the production bug fix: correct, complete, no stale-destination hazard
|
||||
|
||||
Verified. `RuntimeWorldTransitState.TryBeginPortalReveal:159-182` sets
|
||||
`_hasAcceptedDestination = false` and `_acceptedDestination = default`
|
||||
the instant it claims the generation, and
|
||||
`TryGetAcceptedTeleportDestination:522-527` returns
|
||||
`_teleportActive && _hasAcceptedDestination`. So the pre-fix Place-time
|
||||
re-read was **guaranteed** to fail — the canonical portal arm was 100 %
|
||||
dead code, refusing with `cause=host-token-unavailable` before ever
|
||||
reaching Runtime. The diagnosis is right and this was a real production
|
||||
bug, not a fixture artifact.
|
||||
|
||||
The fix is the right lifetime:
|
||||
|
||||
- `_pendingDestination` is written in `AimDestination:781-784`, in the
|
||||
same statement block as `_pendingCell`/`_pendingRotation`, only after
|
||||
`TryBeginPortal` succeeded (`:741-748`) — so the four Aim-time
|
||||
snapshots are mutually consistent by construction.
|
||||
- **Cancellation:** `ResetTransit:801-804` clears all four; every
|
||||
cancellation path funnels through it.
|
||||
- **Supersession:** a second accepted destination re-enters
|
||||
`TryAimAcceptedDestination` → `AimDestination` →
|
||||
`WorldRevealCoordinator.TryBeginPortal` → `WithdrawHostForReplacement`
|
||||
+ a **new** generation, overwriting all four snapshots together. A
|
||||
superseded destination cannot survive.
|
||||
- **Staleness at Place:** three independent gates still validate —
|
||||
`CanPlacePortalDestination(_pendingRevealGeneration, sequence, _pendingCell)`
|
||||
(`:507-511`), the idempotent
|
||||
`TryRegisterHostProjection` re-derivation (generation ==
|
||||
`_snapshot.Generation`, cell == `_snapshot.DestinationCell`,
|
||||
`!Cancelled`, `!Completed` — `RuntimeWorldTransitState.cs:189-227`),
|
||||
and `BeginAcceptedPlacementCore`'s own
|
||||
`portal.Projection.DestinationCell == acceptedPosition.LandblockId`
|
||||
against the **latest merged** snapshot
|
||||
(`RuntimeSetPositionState.cs:1528-1534`).
|
||||
|
||||
The re-read was protecting nothing. `WorldRevealCoordinator.BeginHostLifetime`
|
||||
throws if the Aim-time registration fails, so the Place-time
|
||||
re-derivation is genuinely idempotent and can never mint a second host
|
||||
projection in production.
|
||||
|
||||
## §D — contract open questions, answered
|
||||
|
||||
**(b) leash anchor — keep the resolved anchor as shipped.** Retail's
|
||||
`constraint_pos` is write-only (never read by `adjust_offset`, confirmed
|
||||
in the port's own doc at `ConstraintManager.cs:41-44` and against ACE);
|
||||
the only downstream consumer of `ConstrainTo`'s inputs is
|
||||
`constraint_pos_offset = distance(anchor, m_position)`, which is the
|
||||
placement adjustment (centimetres) in retail and exactly 0 in acdream.
|
||||
Both are orders of magnitude inside the `0.9 * max` band, so no behaviour
|
||||
in the leash's brake taper can distinguish them. The AD-2 note is the
|
||||
right disposition; do **not** switch anchors.
|
||||
|
||||
**(c) `SendMovementEvent` shape — see R7.** Message family, contact
|
||||
derivation, count, and ordering are all correct; the missing
|
||||
`autonomy_level` gate is the one delta.
|
||||
|
||||
## §E — errors in the contract itself
|
||||
|
||||
1. **§4 item 3 / D-T5's re-attempt reasoning is the proximate cause of
|
||||
R1.** D-T5 offered "the anim event re-fires while `ready` holds" as
|
||||
the leading case and demoted the one-shot case to a parenthetical
|
||||
verify-and-state. It is one-shot. The contract should have read the
|
||||
sequencer before writing the row and pinned the driver. This is
|
||||
process rule 1 ("the contract causes the defect") recurring for the
|
||||
third documented time.
|
||||
2. **§9's `leash=armed` criterion is unachievable** with any
|
||||
`IsFullyConstrained`-shaped observable; the contract should have named
|
||||
`ConstraintManager.IsConstrained`. See R3.
|
||||
3. **§3.5 overstates the change:** "`AcknowledgePortalMaterialized` fires
|
||||
from the committed placement receipt instead of rubber-stamping after
|
||||
a host mutation." As built it still fires from the host Place edge,
|
||||
merely gated on a committed status. Substantively equivalent on the
|
||||
commit path; wording should be corrected so a future reader does not
|
||||
look for a receipt-driven ack that does not exist.
|
||||
4. **§1's `enter_world` row** carries the same wrong caller attribution
|
||||
the banner does (R6) — it says "its local-player caller is the initial
|
||||
login path only (@0x00455095)", which is right about that site but
|
||||
silently drops the *other* site @0x004550EC and mis-names the
|
||||
enclosing function in the derived correction.
|
||||
|
||||
## What must land before this can pass
|
||||
|
||||
1. A re-attempt (or explicit-cancel) mechanism for a non-`Committed`
|
||||
Place edge, with the driver named and tested — R1.
|
||||
2. Headless must consume the arm's status and must not acknowledge a
|
||||
materialization for a placement that did not commit — R2.
|
||||
3. Fix the probe's `leash` observable (and emit a line per *attempt*,
|
||||
under the gate's own env var) — R3, R8.
|
||||
4. Write the App-layer presentation suite (§8 items 8/9/10) closing route
|
||||
2's B2 gap, plus one headless committed-portal test — R4, R5.
|
||||
5. Correct the `enter_world` caller-sweep banner — R6.
|
||||
6. Gate the movement refresh on autonomy, or file the register row — R7.
|
||||
7. Comment/citation cleanups — R9, R10, R11.
|
||||
Loading…
Add table
Add a link
Reference in a new issue