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:
Erik 2026-08-05 03:57:37 +02:00
parent cd3129e9d6
commit e0f96a55bf
24 changed files with 5261 additions and 243 deletions

View file

@ -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. - 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. - 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 ## #317`TryCommitAuthoritativeVelocity`'s call site has no established retail basis
**Status:** OPEN **Status:** OPEN

File diff suppressed because one or more lines are too long

View file

@ -91,6 +91,34 @@ same commit) → docs/handoff commit. No workarounds; no fused slices.
and local-player owners. and local-player owners.
- `RuntimePortalPlacementAuthority` has zero producing call sites; the - `RuntimePortalPlacementAuthority` has zero producing call sites; the
adapter from `RuntimeWorldTransitState` does not exist. 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` / - The exact-Setup mover chain (`PrepareMover` /
`RuntimeSetPositionMoverPreparer.TryBuild` / `RuntimeSetPositionMoverPreparer.TryBuild` /
`IPreparedCollisionSource.ReadSetupCollision`) exists piecewise, unwired. `IPreparedCollisionSource.ReadSetupCollision`) exists piecewise, unwired.

View file

@ -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 ### 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: Named retail references:
- `CPhysicsObj::change_cell` at `0x00513390` - `CPhysicsObj::change_cell` at `0x00513390`
- `CPhysicsObj::update_object` at `0x00515D10` - `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::prepare_to_enter_world` at `0x00511FA0`
- `CPhysicsObj::set_hidden` at `0x00514C60` - `CPhysicsObj::set_hidden` at `0x00514C60`
Retail does not separate an accepted destination Position from the object's Retail does not separate an accepted destination Position from the object's
live cell pointer. `enter_world` runs `SetPosition`, which installs the object live cell pointer. `SetPositionSimple` installs the object in its destination
in its destination `CObjCell`, before the PartArray and MovementManager `CObjCell`, before the PartArray and MovementManager enter-world boundaries
enter-world boundaries complete. `update_object` then rejects only a parented complete. `update_object` then rejects only a parented object, a null `cell`,
object, a null `cell`, or a Frozen object; Hidden is not a reason to skip the or a Frozen object; Hidden is not a reason to skip the
ScriptManager/ParticleManager tail. ScriptManager/ParticleManager tail.
```text ```text
accepted portal destination becomes ready: accepted portal destination becomes ready:
player.enter_world(destination) SmartBox.TeleportPlayer(destination)
SetPosition(destination) SetPositionSimple(destination)
change_cell(destination CObjCell) change_cell(destination CObjCell)
PartArray.HandleEnterWorld() PartArray.HandleEnterWorld()
MovementManager.HandleEnterWorld() MovementManager.HandleEnterWorld()

View file

@ -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. |

View 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. |

View 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 25 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.

View 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.

View file

@ -586,7 +586,21 @@ internal sealed class SessionPlayerCompositionPhase
() => d.PlayerIdentity.ServerGuid, () => d.PlayerIdentity.ServerGuid,
() => d.PlayerController.Controller, () => d.PlayerController.Controller,
() => d.Character.UsePositionFromServer, () => d.Character.UsePositionFromServer,
() => liveSessionSource.CurrentSession); () => liveSessionSource.CurrentSession,
// C4 route 3: the portal arm's PlayerTeleported port needs the
// J5.4 autorun latch owner, one level above the raw controller.
() => d.PlayerController,
// A2/D-T2.4 review fix (2026-08-05): the SAME idempotent query
// the Place edge itself uses (WorldRevealCoordinator.
// CanPlacePortalDestination -> RuntimeWorldTransitState) lets a
// DeferredCell wake re-validate before reconciling instead of
// running the ack suffix against a reveal that ended or was
// superseded while the park sat outstanding.
isPortalAuthorityCurrent: portal => live.WorldTransit
.CanPlacePortalDestination(
portal.RevealGeneration,
portal.TeleportSequence,
portal.Projection.DestinationCell));
// C4 route 4b-2 (2026-08-04): the graphical remote-placement drive // C4 route 4b-2 (2026-08-04): the graphical remote-placement drive
// controller — route 4b-1's dormant owner, now driven by the remote // controller — route 4b-1's dormant owner, now driven by the remote
// far snap. Its service window is the graphical host's near-tier // far snap. Its service window is the graphical host's near-tier
@ -886,16 +900,17 @@ internal sealed class SessionPlayerCompositionPhase
live.WorldTransit, live.WorldTransit,
worldReveal, worldReveal,
new LocalPlayerTeleportPlacement( new LocalPlayerTeleportPlacement(
d.PhysicsEngine,
live.LiveEntities, live.LiveEntities,
d.PlayerIdentity, d.PlayerIdentity,
d.PlayerController, d.PlayerController,
d.PlayerHost, d.PlayerHost,
d.ChaseCameraInput, d.ChaseCameraInput,
d.WorldOrigin,
liveSpatialReconciler), liveSpatialReconciler),
new LocalPlayerTeleportSession(liveSessionSource), new LocalPlayerTeleportSession(liveSessionSource),
presentation); presentation,
// C4 route 3: the portal arm shares route 2's Runtime
// SetPosition drive controller.
acceptedPositionDrive);
LocalPlayerTeleportController CreateLocalTeleportWithTunnel( LocalPlayerTeleportController CreateLocalTeleportWithTunnel(
PortalTunnelPresentation portalTunnel) => PortalTunnelPresentation portalTunnel) =>

View file

@ -2273,13 +2273,28 @@ internal sealed class LiveEntityNetworkUpdateController
// Position resolves to NoPositionOperation (retail's airborne // Position resolves to NoPositionOperation (retail's airborne
// no-op — writes nothing at all) or Interpolate (retail's near // no-op — writes nothing at all) or Interpolate (retail's near
// InterpolateTo queue — no direct body write here) never receives // InterpolateTo queue — no direct body write here) never receives
// the generic write below. The local player never reaches this // the generic write below. C4 route 4b-2 routes the >=96 m far snap
// generic-remote code path at all. C4 route 4b-2 routes the >=96 m // and C4 route 4b-3 routes the teleport/cell-less classification
// far snap and C4 route 4b-3 routes the teleport/cell-less // through the canonical Runtime placement owner
// classification through the canonical Runtime placement owner
// (ApplyRemoteContactRouting); a rejected authority or payload, and // (ApplyRemoteContactRouting); a rejected authority or payload, and
// "no classification at all", take the stated UnroutedCatchUp // "no classification at all", take the stated UnroutedCatchUp
// policy (RuntimeRemoteFarSnapPosition.ResolveArm). // policy (RuntimeRemoteFarSnapPosition.ResolveArm).
//
// CORRECTED 2026-08-04 (C4 route 3, process rule 6 — this comment
// used to claim "the local player never reaches this generic-remote
// code path at all", which is FALSE): for the local player,
// `earlyRemoteRoute` is null and `OwnsSteadyState(null)` is false
// (RuntimeRemoteSteadyStatePosition.cs — both pattern matches fail
// on null), so every accepted local Apply — including the portal
// DESTINATION Position itself — reaches and runs the generic write
// + rebucket below, writing the raw wire pose onto the local
// player's WorldEntity while portal space still covers the
// viewport. This is pre-existing, tolerated (AD-2/AP-131/#275
// territory — see the D-T7 discussion in
// docs/research/2026-08-04-c4-route-3-contract.md), and overwritten
// by the committed portal Place receipt's presentation suffix
// (LocalPlayerTeleportPlacement.Place) once the canonical Runtime
// commit lands. Route 3 does not suppress it.
TryApplyGenericRemoteRenderPose( TryApplyGenericRemoteRenderPose(
entity, entity,

View file

@ -10,6 +10,8 @@ using AcDream.Core.Physics;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime; using AcDream.Runtime;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World; using AcDream.Runtime.World;
namespace AcDream.App.Streaming; namespace AcDream.App.Streaming;
@ -173,68 +175,67 @@ internal sealed class LocalPlayerTeleportStreamingOperations
internal interface ILocalPlayerTeleportPlacement internal interface ILocalPlayerTeleportPlacement
{ {
void Place(Vector3 position, uint cellId, Quaternion rotation); void Place(Quaternion rotation);
} }
/// <summary> /// <summary>
/// Commits the local player's deferred portal arrival. It owns the exact /// C4 route 3: acknowledges the local player's deferred portal arrival —
/// Place -&gt; root/controller/camera mutation -&gt; spatial reconcile edge. /// the canonical placement itself now runs through
/// <see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival"/>
/// (a portal arm sharing route 2's Runtime SetPosition owner), retiring the
/// duplicate Resolve/SetPosition authority this class used to own (D1;
/// docs/research/2026-08-04-c4-route-3-contract.md D-T4). This class runs
/// AFTER that commit succeeds. A10 review fix (2026-08-05): the render
/// <see cref="WorldEntity"/> pose write and rebucket this method performs
/// are REDUNDANT repeats of a mutation the canonical Place receipt already
/// made — <c>RuntimePlacementPresentationSink.TryApply</c> →
/// <c>LiveEntityRuntime.TryApplyRuntimePlacementPlace</c> already calls
/// <c>entity.SetPosition</c>/sets <c>Rotation</c>/<c>ParentCellId</c> and
/// rebuckets, synchronously, before <c>TryPublishPlace</c>'s OWN snapshot
/// even runs (proof obligation P2's ordering). This method's writes are
/// therefore harmless-but-duplicate, not the render entity's ONLY mover as
/// an earlier revision of this comment claimed; kept because they cost
/// nothing extra and this is also where the retail teleport_hook tail's
/// remaining local-player-visible actions run (target-watcher
/// notification, camera reset, spatial reconcile).
/// <paramref name="rotation"/> is the retained accepted destination's wire
/// orientation — the resolved body orientation was already committed
/// identically by <c>CommitCanonical</c> (retail's teleport branch does not
/// independently reorient the mover), so re-deriving it here would only add
/// a second copy of the same source of truth.
/// </summary> /// </summary>
internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlacement internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlacement
{ {
private readonly PhysicsEngine _physics;
private readonly LiveEntityRuntime _liveEntities; private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _identity; private readonly ILocalPlayerIdentitySource _identity;
private readonly IRuntimeLocalPlayerControllerSource _controller; private readonly IRuntimeLocalPlayerControllerSource _controller;
private readonly ILocalPlayerPhysicsHostSource _host; private readonly ILocalPlayerPhysicsHostSource _host;
private readonly ChaseCameraInputState _cameras; private readonly ChaseCameraInputState _cameras;
private readonly LiveWorldOriginState _origin;
private readonly ILiveSpatialReconcilePhase _spatial; private readonly ILiveSpatialReconcilePhase _spatial;
public LocalPlayerTeleportPlacement( public LocalPlayerTeleportPlacement(
PhysicsEngine physics,
LiveEntityRuntime liveEntities, LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource identity, ILocalPlayerIdentitySource identity,
IRuntimeLocalPlayerControllerSource controller, IRuntimeLocalPlayerControllerSource controller,
ILocalPlayerPhysicsHostSource host, ILocalPlayerPhysicsHostSource host,
ChaseCameraInputState cameras, ChaseCameraInputState cameras,
LiveWorldOriginState origin,
ILiveSpatialReconcilePhase spatial) ILiveSpatialReconcilePhase spatial)
{ {
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities)); _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_identity = identity ?? throw new ArgumentNullException(nameof(identity)); _identity = identity ?? throw new ArgumentNullException(nameof(identity));
_controller = controller ?? throw new ArgumentNullException(nameof(controller)); _controller = controller ?? throw new ArgumentNullException(nameof(controller));
_host = host ?? throw new ArgumentNullException(nameof(host)); _host = host ?? throw new ArgumentNullException(nameof(host));
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras)); _cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial)); _spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
} }
public void Place(Vector3 position, uint cellId, Quaternion rotation) public void Place(Quaternion rotation)
{ {
PlayerMovementController controller = _controller.Controller PlayerMovementController controller = _controller.Controller
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
"Teleport Place ran without the local player controller."); "Teleport Place ran without the local player controller.");
var resolved = _physics.Resolve(
position,
cellId,
Vector3.Zero,
controller.StepUpHeight);
var snapped = new Vector3(
resolved.Position.X,
resolved.Position.Y,
resolved.Position.Z);
uint playerGuid = _identity.ServerGuid; uint playerGuid = _identity.ServerGuid;
controller.SetPosition(
snapped,
resolved.CellId,
CellLocalForSeed(snapped, resolved.CellId));
// SnapToCell owns the retail Position frame and may normalize an
// outdoor land-cell index from the cell-local origin. Publish that
// canonical result, not the pre-snap resolver hint, to rendering.
if (_liveEntities.TryGetWorldEntity( if (_liveEntities.TryGetWorldEntity(
playerGuid, playerGuid,
out WorldEntity? entity)) out WorldEntity? entity))
@ -245,12 +246,12 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
// Retail CPhysicsObj::enter_world installs the object in its // Retail CPhysicsObj::enter_world installs the object in its
// destination CObjCell before hidden scripts/particles resume. // destination CObjCell before hidden scripts/particles resume.
// The accepted Position packet has already advanced FullCellId, // The canonical commit has already advanced FullCellId, but that
// but that wire fact alone does not move acdream's retained // fact alone does not move acdream's retained projection out of
// projection out of its source/pending GPU bucket. Commit both // its source/pending GPU bucket. Commit both halves of the
// halves of the spatial move here, while portal space still owns // spatial move here, while portal space still owns the
// the viewport, so CPhysicsObj::update_object's cell-gated tail // viewport, so CPhysicsObj::update_object's cell-gated tail can
// can advance the Hidden/UnHide PES chain at retail's boundary. // advance the Hidden/UnHide PES chain at retail's boundary.
if (!_liveEntities.RebucketLiveEntity(playerGuid, controller.CellId)) if (!_liveEntities.RebucketLiveEntity(playerGuid, controller.CellId))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
@ -260,9 +261,13 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
} }
// Retail teleport_hook tail @ 0x00514ED0 clears the local target and // Retail teleport_hook tail @ 0x00514ED0 clears the local target and
// notifies every watcher that this object teleported. // notifies every watcher that this object teleported. The body's
// constraint leash re-arm and orientation are already the canonical
// commit's job (RuntimeAcceptedPositionDriveController
// .ReconcileAndAcknowledgePortal -> PlayerMovementController
// .CommitCanonicalTeleportFrame), so this suffix only acknowledges
// the result into presentation.
_host.Host?.NotifyTeleported(); _host.Host?.NotifyTeleported();
controller.SetBodyOrientation(rotation);
_cameras.Legacy?.Update(controller.Position, controller.Yaw); _cameras.Legacy?.Update(controller.Position, controller.Yaw);
_cameras.Retail?.ResetViewerToPlayer(controller.Position, controller.Yaw); _cameras.Retail?.ResetViewerToPlayer(controller.Position, controller.Yaw);
@ -276,17 +281,6 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
$"live: teleport materialized - snapped to {controller.Position} " $"live: teleport materialized - snapped to {controller.Position} "
+ $"cell=0x{controller.CellId:X8}"); + $"cell=0x{controller.CellId:X8}");
} }
private Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
{
int landblockX = (int)((cellId >> 24) & 0xFFu);
int landblockY = (int)((cellId >> 16) & 0xFFu);
var origin = new Vector3(
(landblockX - _origin.CenterX) * 192f,
(landblockY - _origin.CenterY) * 192f,
0f);
return worldPosition - origin;
}
} }
internal interface ILocalPlayerTeleportSession internal interface ILocalPlayerTeleportSession
@ -397,11 +391,28 @@ internal sealed class LocalPlayerTeleportController
private readonly ILocalPlayerTeleportPlacement _placement; private readonly ILocalPlayerTeleportPlacement _placement;
private readonly ILocalPlayerTeleportSession _session; private readonly ILocalPlayerTeleportSession _session;
private readonly ILocalPlayerTeleportPresentation _presentation; private readonly ILocalPlayerTeleportPresentation _presentation;
private readonly RuntimeAcceptedPositionDriveController _acceptedPositionDrive;
private Vector3 _pendingPosition;
private uint _pendingCell; private uint _pendingCell;
private Quaternion _pendingRotation = Quaternion.Identity; private Quaternion _pendingRotation = Quaternion.Identity;
private long _pendingRevealGeneration; private long _pendingRevealGeneration;
private RuntimeTeleportDestination _pendingDestination;
private bool _hasPendingDestination;
/// <summary>
/// A1 review fix (2026-08-05): true once the canonical Runtime commit
/// for THIS teleport lifetime has actually happened. See the class doc
/// on <see cref="TryAdvancePortalCommit"/> for why this exists.
/// </summary>
private bool _placementCommitted;
/// <summary>
/// A1 review fix: true while a <c>DeferredCell</c> park is outstanding
/// for the local player's one possible pending operation. See
/// <see cref="TryAdvancePortalCommit"/>.
/// </summary>
private bool _awaitingDeferredWake;
private float _holdSeconds; private float _holdSeconds;
private long _lifetimeGeneration; private long _lifetimeGeneration;
private bool _disposed; private bool _disposed;
@ -415,7 +426,8 @@ internal sealed class LocalPlayerTeleportController
WorldRevealCoordinator worldReveal, WorldRevealCoordinator worldReveal,
ILocalPlayerTeleportPlacement placement, ILocalPlayerTeleportPlacement placement,
ILocalPlayerTeleportSession session, ILocalPlayerTeleportSession session,
ILocalPlayerTeleportPresentation presentation) ILocalPlayerTeleportPresentation presentation,
RuntimeAcceptedPositionDriveController acceptedPositionDrive)
{ {
_authority = authority ?? throw new ArgumentNullException(nameof(authority)); _authority = authority ?? throw new ArgumentNullException(nameof(authority));
_input = input ?? throw new ArgumentNullException(nameof(input)); _input = input ?? throw new ArgumentNullException(nameof(input));
@ -426,6 +438,8 @@ internal sealed class LocalPlayerTeleportController
_placement = placement ?? throw new ArgumentNullException(nameof(placement)); _placement = placement ?? throw new ArgumentNullException(nameof(placement));
_session = session ?? throw new ArgumentNullException(nameof(session)); _session = session ?? throw new ArgumentNullException(nameof(session));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation)); _presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
_acceptedPositionDrive = acceptedPositionDrive
?? throw new ArgumentNullException(nameof(acceptedPositionDrive));
} }
public bool IsActive => _transit.IsTeleportActive; public bool IsActive => _transit.IsTeleportActive;
@ -488,21 +502,41 @@ internal sealed class LocalPlayerTeleportController
bool haveDestination = _pendingCell != 0u; bool haveDestination = _pendingCell != 0u;
bool originReady = !_streaming.IsRecenterPending; bool originReady = !_streaming.IsRecenterPending;
bool ready = haveDestination bool dataReady = haveDestination
&& originReady && originReady
&& _worldReveal.Evaluate(_pendingCell).IsReady; && _worldReveal.Evaluate(_pendingCell).IsReady;
if (!IsCurrentLifetime(generation, sequence)) if (!IsCurrentLifetime(generation, sequence))
return; return;
if (haveDestination && !ready) // A1 review fix (2026-08-05, retail/architecture review): the
// sequencer's Tunnel -> TunnelContinue transition
// (TeleportAnimSequencer.cs:134-141) is unconditional and
// irreversible the instant it observes `worldReady` true; by the
// time a failed placement is discovered the stream has already left
// Tunnel with no path back, and TeleportAnimSequencer itself is
// untouched (stop condition 2 forbids sequencer timing changes). So
// the boolean fed into the sequencer must never mean "the data is
// ready" alone - it must mean "the canonical Runtime commit has
// ALREADY happened", checked/attempted fresh every tick via
// TryAdvancePortalCommit. This makes D-T5 row 2's "the NEXT Tick
// re-attempts the Place edge" real: the sequencer simply never
// leaves Tunnel while the commit keeps refusing, and by the time it
// finally does leave Tunnel and fire Place, TryAdvancePortalCommit
// has already made the Runtime side succeed - the Place-event
// handler below only ever runs the presentation suffix.
bool placementReady = dataReady && TryAdvancePortalCommit(sequence);
if (!IsCurrentLifetime(generation, sequence))
return;
if (haveDestination && !placementReady)
_holdSeconds += deltaSeconds; _holdSeconds += deltaSeconds;
_presentation.SetWaitCue( _presentation.SetWaitCue(
haveDestination haveDestination
&& !ready && !placementReady
&& _worldReveal.ObserveWait( && _worldReveal.ObserveWait(
TimeSpan.FromSeconds(_holdSeconds))); TimeSpan.FromSeconds(_holdSeconds)));
var (_, events) = _presentation.Tick(deltaSeconds, ready); var (_, events) = _presentation.Tick(deltaSeconds, placementReady);
if (!IsCurrentLifetime(generation, sequence)) if (!IsCurrentLifetime(generation, sequence))
return; return;
@ -511,17 +545,32 @@ internal sealed class LocalPlayerTeleportController
switch (teleportEvent) switch (teleportEvent)
{ {
case TeleportAnimEvent.Place: case TeleportAnimEvent.Place:
// TryAdvancePortalCommit above is the only path that
// makes `placementReady` (and, with the REAL sequencer,
// this event) true, so the canonical Runtime commit has
// ALREADY succeeded by construction on that path - this
// only runs the presentation suffix (D-T4). The
// _placementCommitted re-check stays defensive: it is
// the exact same shape of guard IsCurrentLifetime below
// already applies to every other step of this case, for
// a transit that goes stale between the gate above and
// this line being reached.
if (!_placementCommitted)
return;
// B7 review fix (2026-08-05): re-derived, not assumed -
// if the reveal was cancelled/superseded in the window
// between the commit above and this event being
// processed, ObserveMaterialized below would refuse but
// Place/the presentation suffix would already have run
// against a reveal that is no longer current. Same
// check TryExecuteCanonicalPortalPlacementCore itself
// gates on; idempotent to repeat here.
if (!_worldReveal.CanPlacePortalDestination( if (!_worldReveal.CanPlacePortalDestination(
_pendingRevealGeneration, _pendingRevealGeneration, sequence, _pendingCell))
sequence,
_pendingCell))
{ {
return; return;
} }
_placement.Place( _placement.Place(_pendingRotation);
_pendingPosition,
_pendingCell,
_pendingRotation);
if (!IsCurrentLifetime(generation, sequence)) if (!IsCurrentLifetime(generation, sequence))
return; return;
_worldReveal.ObserveMaterialized( _worldReveal.ObserveMaterialized(
@ -569,6 +618,193 @@ internal sealed class LocalPlayerTeleportController
_presentation.TickTunnel(deltaSeconds); _presentation.TickTunnel(deltaSeconds);
} }
/// <summary>
/// A1 review fix (2026-08-05): the one gate that decides whether the
/// anim sequencer is allowed to see <c>worldReady=true</c>. Returns
/// <see langword="true"/> ONLY once the canonical Runtime commit for
/// THIS teleport lifetime has actually happened — never speculatively,
/// never optimistically. Three states:
///
/// <list type="bullet">
/// <item><description>Already committed
/// (<see cref="_placementCommitted"/>) — returns
/// <see langword="true"/> immediately, every subsequent Tick.</description></item>
/// <item><description>A <c>DeferredCell</c> park is outstanding
/// (<see cref="_awaitingDeferredWake"/>) — polls
/// <see cref="RuntimeAcceptedPositionDriveController.PendingCount"/>
/// only to decide whether to re-attempt the Runtime call: Runtime's own
/// Begin would just refuse a second overlapping attempt with
/// <c>Contention</c> while a park is outstanding (the drive tracks at
/// most one pending operation for the local player), so retrying blind
/// would only add noise. B1 review fix (2026-08-05): once
/// <c>PendingCount</c> returns to 0 the park is DONE, but "done" is not
/// "committed" — the drive's own doc names a merge-time <c>Forget</c>
/// (an ordinary ACE broadcast arriving mid-park) as the EXPECTED way a
/// park resolves without committing, and A2's own abandon branches are
/// a second way. The OLD code inferred commit from the empty slot
/// alone; this now asks
/// <see cref="RuntimeAcceptedPositionDriveController.TryConsumePortalCommit"/>,
/// which the drive latches ONLY inside a REAL
/// <c>ReconcileAndAcknowledgePortal</c> call, keyed to this exact
/// reveal generation/sequence. A "no" here is NOT a failure — it just
/// means the park ended without placing, so <see cref="_awaitingDeferredWake"/>
/// clears and the method falls through to a fresh attempt below,
/// safely (nothing is pending anymore).</description></item>
/// <item><description>Neither — attempt the canonical placement fresh
/// this tick. <c>Committed</c> latches
/// <see cref="_placementCommitted"/>; <c>DeferredCell</c> latches
/// <see cref="_awaitingDeferredWake"/>; every other status (Contention,
/// Rejected, NotApplicable, or the transit no longer owning this
/// reveal) is the D-T5 refusal shape — nothing mutates, and the SAME
/// predicate retries automatically on the NEXT Tick, which is what
/// makes D-T5 row 2's "the next Tick re-attempts the Place edge"
/// mechanism real without ever touching
/// <see cref="AcDream.Core.World.TeleportAnimSequencer"/>.</description></item>
/// </list>
/// </summary>
private bool TryAdvancePortalCommit(ushort sequence)
{
if (_placementCommitted)
return true;
if (_awaitingDeferredWake)
{
if (_acceptedPositionDrive.PendingCount != 0)
return false;
_awaitingDeferredWake = false;
if (_acceptedPositionDrive.TryConsumePortalCommit(
_pendingRevealGeneration, sequence))
{
_placementCommitted = true;
return true;
}
// The park ended without placing (Forgotten, or abandoned by
// A2's re-validation). Fall through to the fresh-attempt path
// below in this SAME call — nothing is pending, so it is safe.
}
if (!_worldReveal.CanPlacePortalDestination(
_pendingRevealGeneration,
sequence,
_pendingCell))
{
PhysicsDiagnostics.LogTeleport(
"REFUSED", _pendingCell, "cause=stale-reveal");
// R8 residual fix (2026-08-05): this refusal previously only
// logged through PhysicsDiagnostics.LogTeleport, gated by the
// DIFFERENT ACDREAM_PROBE_TELEPORT flag — invisible under
// ACDREAM_PROBE_LOCAL_TELEPORT, the gate the rest of this
// route's arrival/commit lines use. No placement was attempted,
// so there is no resolved cell/leash/autorun fact to report.
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "stale-reveal",
placementStatus: "Refused",
portalGeneration: _pendingRevealGeneration,
teleportSequence: sequence,
destinationCell: _pendingCell,
resolvedCell: 0u,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return false;
}
RuntimeAcceptedPositionExecutionStatus status =
TryExecuteCanonicalPortalPlacementCore(sequence);
switch (status)
{
case RuntimeAcceptedPositionExecutionStatus.Committed:
_placementCommitted = true;
return true;
case RuntimeAcceptedPositionExecutionStatus.DeferredCell:
_awaitingDeferredWake = true;
return false;
default:
return false;
}
}
/// <summary>
/// C4 route 3 (D-T1/D-T2): builds the producer's
/// <see cref="RuntimePortalPlacementAuthority"/> from live transit facts
/// and drives the canonical Runtime portal arm. No new
/// <see cref="WorldRevealCoordinator"/> exposure is needed — the host
/// token is RE-DERIVED through the transit owner's idempotent
/// <c>TryRegisterHostProjection</c> (the same generation+cell returns
/// the token <see cref="WorldRevealCoordinator.TryBeginPortal"/> already
/// registered at Aim time; a stale generation, wrong cell, cancelled, or
/// completed reveal refuses), which makes a superseded token unobtainable
/// by construction.
///
/// <para>
/// The destination itself is <see cref="_pendingDestination"/> — the
/// value <see cref="AimDestination"/> captured at Aim time — and NOT a
/// fresh <c>_transit.TryGetAcceptedTeleportDestination</c> read.
/// <see cref="RuntimeWorldTransitState.TryBeginPortalReveal"/> atomically
/// CONSUMES the transit's one accepted-destination slot the instant Aim
/// claims the reveal generation (it clears
/// <c>_hasAcceptedDestination</c> so a stale destination can never be
/// re-claimed by a later portal) — by Place time that slot is already
/// empty, so re-querying it here always fails. This mirrors why
/// <see cref="_pendingCell"/>/<see cref="_pendingRotation"/>/
/// <see cref="_pendingRevealGeneration"/> are themselves Aim-time
/// snapshots rather than live transit reads.
/// </para>
///
/// <para>
/// A9 review fix (2026-08-05): <paramref name="sequence"/> is
/// <see cref="_pendingDestination"/>'s OWN
/// <see cref="RuntimeTeleportDestination.TeleportSequence"/>, not the
/// transit's separately-tracked <c>ActiveTeleportSequence</c> the caller
/// otherwise threads through — one source for the fact this method's
/// authority carries, asserted equal to the caller's copy so the two
/// can never silently diverge.
/// </para>
/// </summary>
private RuntimeAcceptedPositionExecutionStatus
TryExecuteCanonicalPortalPlacementCore(ushort sequence)
{
System.Diagnostics.Debug.Assert(
!_hasPendingDestination
|| _pendingDestination.TeleportSequence == sequence,
"The transit's active sequence and the Aim-time destination's "
+ "own sequence must never diverge (A9).");
if (!_hasPendingDestination
|| !_transit.TryRegisterHostProjection(
_pendingRevealGeneration,
_pendingCell,
out RuntimeWorldHostProjectionToken hostToken))
{
PhysicsDiagnostics.LogTeleport(
"REFUSED", _pendingCell, "cause=host-token-unavailable");
// R8 residual fix (2026-08-05): same rationale as the
// stale-reveal refusal above — route through
// LogLocalTeleportArrival too, so ACDREAM_PROBE_LOCAL_TELEPORT
// alone is enough to see every App-side refusal cause.
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "host-token-unavailable",
placementStatus: "Refused",
portalGeneration: _pendingRevealGeneration,
teleportSequence: sequence,
destinationCell: _pendingCell,
resolvedCell: 0u,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
RuntimeTeleportDestination destination = _pendingDestination;
var portal = new RuntimePortalPlacementAuthority(
Present: true,
RevealGeneration: _pendingRevealGeneration,
TeleportSequence: destination.TeleportSequence,
Projection: hostToken);
return _acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
destination,
portal);
}
public void ResetSession() public void ResetSession()
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@ -695,7 +931,6 @@ internal sealed class LocalPlayerTeleportController
if (!IsCurrentLifetime(generation, sequence)) if (!IsCurrentLifetime(generation, sequence))
return false; return false;
Vector3 worldPosition;
if (transition.ChangesStreamingCenter) if (transition.ChangesStreamingCenter)
{ {
bool isSealedDungeon = _streaming.IsSealedDungeon( bool isSealedDungeon = _streaming.IsSealedDungeon(
@ -709,19 +944,24 @@ internal sealed class LocalPlayerTeleportController
isSealedDungeon); isSealedDungeon);
if (!IsCurrentLifetime(generation, sequence)) if (!IsCurrentLifetime(generation, sequence))
return false; return false;
worldPosition = new Vector3(
position.Frame.Origin.X,
position.Frame.Origin.Y,
position.Frame.Origin.Z);
}
else
{
worldPosition = translated;
} }
// C4 route 3: the App-frame-translated `translated`/`worldPosition`
// vector is no longer carried past this point — the canonical
// portal arm resolves the placement through Runtime's OWN world
// frame (resolveWorldOffsetFromRuntimeFrame: true), using the
// cell-local `destination` Position captured HERE rather than an
// App-translated snapshot (trap T4). It must be captured here and
// NOT re-read from the transit at the Place edge:
// _worldReveal.TryBeginPortal (above) drives
// RuntimeWorldTransitState.TryBeginPortalReveal, which atomically
// CONSUMES the transit's one accepted-destination slot the instant
// it claims this reveal generation — a later
// TryGetAcceptedTeleportDestination call always finds it empty.
_pendingRotation = position.Frame.Orientation; _pendingRotation = position.Frame.Orientation;
_pendingPosition = worldPosition;
_pendingCell = position.ObjCellId; _pendingCell = position.ObjCellId;
_pendingDestination = destination;
_hasPendingDestination = true;
_holdSeconds = 0f; _holdSeconds = 0f;
PhysicsDiagnostics.LogTeleport( PhysicsDiagnostics.LogTeleport(
"AIM", "AIM",
@ -739,10 +979,13 @@ internal sealed class LocalPlayerTeleportController
{ {
long generation = checked(++_lifetimeGeneration); long generation = checked(++_lifetimeGeneration);
_pendingPosition = default;
_pendingCell = 0u; _pendingCell = 0u;
_pendingRotation = Quaternion.Identity; _pendingRotation = Quaternion.Identity;
_pendingRevealGeneration = 0; _pendingRevealGeneration = 0;
_pendingDestination = default;
_hasPendingDestination = false;
_placementCommitted = false;
_awaitingDeferredWake = false;
_holdSeconds = 0f; _holdSeconds = 0f;
_streaming.ResetRecenter(clearSession); _streaming.ResetRecenter(clearSession);

View file

@ -102,7 +102,22 @@ internal sealed class RuntimePlacementPresentationSink
projection.Token.Portal, projection.Token.Portal,
projection.Token.ExactCellId)) projection.Token.ExactCellId))
{ {
return false; // B2 review fix (2026-08-05): acknowledge-and-ignore, same shape
// as Discard/ExecutorCompleted/WithdrawalRestored above. A Place
// whose portal authority went stale (the transit ended or was
// superseded WHILE a DeferredCell park sat outstanding — the
// residual A1's readiness-hold does not close, since it only
// protects the ORDINARY in-flight case) must not be left
// refused at the FIFO head: RuntimePlacementProjectionSubscription
// .OnPlacement never calls Acknowledge on a `false` return, so a
// refused receipt wedges EVERY later entity's placement receipt
// behind it forever. This runs SYNCHRONOUSLY at publish
// (RuntimeAcceptedPositionDriveController's A2 re-validation, by
// contrast, only runs downstream of a receipt this gate ALREADY
// let through — it cannot protect this path). The canonical
// body already committed via RetryDeferred; there is simply no
// live presentation authority left to apply it to.
return true;
} }
if (!_liveEntities.TryApplyRuntimePlacementProjection(in projection)) if (!_liveEntities.TryApplyRuntimePlacementProjection(in projection))

View file

@ -1171,6 +1171,63 @@ public static class PhysicsDiagnostics
$"[tp-probe] {point,-6} id=0x{id:X8} t={Environment.TickCount64} {extra}")); $"[tp-probe] {point,-6} id=0x{id:X8} t={Environment.TickCount64} {extra}"));
} }
/// <summary>
/// C4 route 3 D-T8 (2026-08-04 — TEMPORARY, strip with the rest of the
/// physics-probe family once the connected gate is scored). One line per
/// local-player portal-arrival attempt from
/// <c>RuntimeAcceptedPositionDriveController.ReconcileAndAcknowledgePortal</c>
/// — the single Runtime chokepoint both the graphical and headless hosts
/// share, so this is dual-host parity evidence, not per-host guesswork.
/// Initial state from <c>ACDREAM_PROBE_LOCAL_TELEPORT=1</c>.
/// </summary>
public static bool ProbeLocalTeleportEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_LOCAL_TELEPORT") == "1";
/// <summary>
/// Which host process is running — set once at composition startup by
/// each host's own entry point (<c>SessionPlayerComposition</c> for
/// graphical, <c>HeadlessSessionHost</c> for headless). Runtime itself
/// stays presentation-agnostic (Slice K); this is a diagnostics-only
/// label so <see cref="LogLocalTeleportArrival"/> can report which
/// process produced a given line without threading a host parameter
/// through the drive controller's constructor.
/// </summary>
public static string LocalTeleportHostKind { get; set; } = "graphical";
/// <summary>
/// One <c>[local-tp]</c> line: cause, host, placement status, portal
/// generation/sequence, destination cell, resolved cell, and the three
/// D-T8 booleans confirming the reconcile suffix actually ran
/// (<paramref name="hookTailRan"/> = <c>CommitCanonicalTeleportFrame</c>
/// executed, <paramref name="leashArmed"/> = the constraint leash is
/// armed post-commit, <paramref name="autorunCancelled"/> =
/// <c>CancelAutoRun</c> ran). Self-guards on
/// <see cref="ProbeLocalTeleportEnabled"/>. <paramref name="cause"/> is
/// always <c>"portal"</c> today — ACE's recall/admin teleports arrive as
/// the identical TeleportAdvanced Position and are indistinguishable
/// from a doorway portal at this layer; the parameter exists so a future
/// wire-level cause signal has somewhere to land without a probe
/// signature change.
/// </summary>
public static void LogLocalTeleportArrival(
string cause,
string placementStatus,
long portalGeneration,
ushort teleportSequence,
uint destinationCell,
uint resolvedCell,
bool hookTailRan,
bool leashArmed,
bool autorunCancelled)
{
if (!ProbeLocalTeleportEnabled) return;
string hookTailText = hookTailRan ? "ran" : "skipped";
string leashText = leashArmed ? "armed" : "unarmed";
string autorunText = autorunCancelled ? "cancelled" : "unchanged";
Console.WriteLine(System.FormattableString.Invariant(
$"[local-tp] cause={cause} host={LocalTeleportHostKind} status={placementStatus} gen={portalGeneration} seq={teleportSequence} dest=0x{destinationCell:X8} resolved=0x{resolvedCell:X8} hookTail={hookTailText} leash={leashText} autorun={autorunText}"));
}
/// <summary> /// <summary>
/// A6.P3 issue #98 step-walk investigation (2026-05-23). When true, /// A6.P3 issue #98 step-walk investigation (2026-05-23). When true,
/// emits one <c>[step-walk]</c> line at selected points in the transition /// emits one <c>[step-walk]</c> line at selected points in the transition

View file

@ -97,16 +97,26 @@ internal sealed class HeadlessRuntimePlacementProjectionSink
if (projection.Kind is not RuntimePlacementProjectionKind.Place) if (projection.Kind is not RuntimePlacementProjectionKind.Place)
return false; return false;
return record.PositionAuthorityVersion if (record.PositionAuthorityVersion != token.PositionAuthorityVersion
== token.PositionAuthorityVersion || record.SpatialAuthorityVersion != token.SpatialAuthorityVersion
&& record.SpatialAuthorityVersion || record.PlacementCommitVersion != token.PlacementCommitVersion
== token.SpatialAuthorityVersion || record.FullCellId != token.ExactCellId)
&& record.PlacementCommitVersion {
== token.PlacementCommitVersion return false;
&& record.FullCellId == token.ExactCellId }
&& _runtime.TransitOwner.IsCurrentPlacementAuthority(
if (!_runtime.TransitOwner.IsCurrentPlacementAuthority(
token.Portal, token.Portal,
token.ExactCellId); token.ExactCellId))
{
// B2 review fix (2026-08-05): acknowledge-and-ignore, the same
// shape and reasoning as the graphical sink's identical fix
// (RuntimePlacementPresentationSink.TryApply) — a stale portal
// authority must not wedge the ordered FIFO for every entity.
return true;
}
return true;
} }
private static bool HasValidPortalShape( private static bool HasValidPortalShape(

View file

@ -3,6 +3,7 @@ using AcDream.Headless.Credentials;
using AcDream.Headless.Diagnostics; using AcDream.Headless.Diagnostics;
using AcDream.Headless.Policies; using AcDream.Headless.Policies;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime; using AcDream.Runtime;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics; using AcDream.Runtime.Physics;
@ -143,6 +144,16 @@ internal sealed class HeadlessSessionHost : IDisposable
private RuntimeAcceptedPositionDriveController? _acceptedPositionDrive; private RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private AcDream.Core.Net.WorldSession? _currentSession; private AcDream.Core.Net.WorldSession? _currentSession;
private HeadlessSessionWorldProjection? _worldProjection; private HeadlessSessionWorldProjection? _worldProjection;
/// <summary>
/// A1/A3 review fix (2026-08-05): retained so <see cref="Tick"/> can
/// pump <see cref="RuntimeLiveEntitySessionController.PumpPortalCompletion"/>
/// alongside <see cref="_worldProjection"/>'s own
/// <c>PumpFirstEntry</c> — a parked portal placement must retry on the
/// host's own per-tick cadence rather than the completion sequence
/// running unconditionally the instant it is first attempted. Reassigned
/// on every reconnect exactly like <see cref="_worldProjection"/>.
/// </summary>
private RuntimeLiveEntitySessionController? _entities;
/// <summary>C4 route 4b-1 (N3): the exact route <see cref="CreateEventRoute"/> /// <summary>C4 route 4b-1 (N3): the exact route <see cref="CreateEventRoute"/>
/// last constructed, so <see cref="Tick"/> can republish the canonical /// last constructed, so <see cref="Tick"/> can republish the canonical
/// placement FIFO every tick — mirrors the graphical host's per-frame /// placement FIFO every tick — mirrors the graphical host's per-frame
@ -332,6 +343,12 @@ internal sealed class HeadlessSessionHost : IDisposable
// collision-generation progress and freshly accepted Creates both // collision-generation progress and freshly accepted Creates both
// surface here, mirroring the graphical per-frame retry phase. // surface here, mirroring the graphical per-frame retry phase.
_worldProjection?.PumpFirstEntry(); _worldProjection?.PumpFirstEntry();
// A1/A3 review fix (2026-08-05): retry a parked portal completion
// (see RuntimeLiveEntitySessionController.PumpPortalCompletion) on
// the SAME per-tick cadence, after first-entry so a DeferredCell
// wake first-entry's own pump just resolved is picked up the same
// tick.
_entities?.PumpPortalCompletion();
// C4 route 4b-1 (N3): republish the canonical placement FIFO LAST, // C4 route 4b-1 (N3): republish the canonical placement FIFO LAST,
// same order as the graphical host's retry-lease callback (drives // same order as the graphical host's retry-lease callback (drives
// first, retry last) — a declined Place left at the FIFO head by // first, retry last) — a declined Place left at the FIFO head by
@ -621,6 +638,13 @@ internal sealed class HeadlessSessionHost : IDisposable
Radius: 0.48f, Radius: 0.48f,
Height: 1.835f, Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless)); RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
// D-T8 (temporary probe): labels every subsequent
// PhysicsDiagnostics.LogLocalTeleportArrival line from THIS
// process as headless — Runtime itself has no host-kind concept
// (Slice K keeps it presentation-agnostic), so this is a
// diagnostics-only label set once at composition time, not a
// Runtime dependency.
PhysicsDiagnostics.LocalTeleportHostKind = "headless";
// C4 route 2: one drive controller per host, mirroring // C4 route 2: one drive controller per host, mirroring
// _firstEntryDrive exactly — same persistent Runtime lifetime, // _firstEntryDrive exactly — same persistent Runtime lifetime,
// collision source, and clock. // collision source, and clock.
@ -633,7 +657,20 @@ internal sealed class HeadlessSessionHost : IDisposable
() => Runtime.PlayerIdentity.ServerGuid, () => Runtime.PlayerIdentity.ServerGuid,
() => Runtime.MovementOwner.Controller, () => Runtime.MovementOwner.Controller,
() => Runtime.CharacterOwner.UsePositionFromServer, () => Runtime.CharacterOwner.UsePositionFromServer,
() => _currentSession); () => _currentSession,
// C4 route 3: the portal arm's PlayerTeleported port needs
// the J5.4 autorun latch owner, one level above the raw
// controller.
() => Runtime.MovementOwner,
// A2/D-T2.4 review fix (2026-08-05): same wiring as the
// graphical composition (SessionPlayerComposition.cs) — the
// SAME idempotent query TryCompletePortal/PrepareDestination
// themselves use.
isPortalAuthorityCurrent: portal => Runtime.TransitOwner
.CanPlacePortalDestination(
portal.RevealGeneration,
portal.TeleportSequence,
portal.Projection.DestinationCell));
var projection = new HeadlessSessionWorldProjection( var projection = new HeadlessSessionWorldProjection(
Runtime, Runtime,
content, content,
@ -651,6 +688,7 @@ internal sealed class HeadlessSessionHost : IDisposable
Runtime.Generation.Value), Runtime.Generation.Value),
worldProjection, worldProjection,
_acceptedPositionDrive); _acceptedPositionDrive);
_entities = entities;
var route = new LiveSessionEventRouter( var route = new LiveSessionEventRouter(
session, session,
entities.CreateSink(), entities.CreateSink(),

View file

@ -8,6 +8,7 @@ using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics; using AcDream.Runtime.Physics;
using AcDream.Runtime.Session; using AcDream.Runtime.Session;
using AcDream.Runtime.World;
namespace AcDream.Headless.Hosting; namespace AcDream.Headless.Hosting;
@ -585,9 +586,6 @@ internal sealed class HeadlessCollisionNeighborhood
internal sealed class HeadlessSessionWorldProjection internal sealed class HeadlessSessionWorldProjection
: IRuntimeDirectWorldProjection : IRuntimeDirectWorldProjection
{ {
private const float DefaultRadius = 0.48f;
private const float DefaultHeight = 1.835f;
private readonly GameRuntime _runtime; private readonly GameRuntime _runtime;
private readonly IHeadlessCollisionNeighborhood _collision; private readonly IHeadlessCollisionNeighborhood _collision;
private readonly RuntimeFirstEntryDriveController? _firstEntry; private readonly RuntimeFirstEntryDriveController? _firstEntry;
@ -749,90 +747,228 @@ internal sealed class HeadlessSessionWorldProjection
controller.State = PlayerState.PortalSpace; controller.State = PlayerState.PortalSpace;
} }
/// <summary>
/// A1/A3 review fix (2026-08-05): true while a <c>DeferredCell</c> park
/// from a PRIOR call to this method is outstanding for the local
/// player's one possible pending drive operation. Mirrors
/// <c>LocalPlayerTeleportController._awaitingDeferredWake</c> on the
/// graphical side — avoids re-attempting
/// <c>TryExecuteAcceptedPortalArrival</c> while parked (Runtime's own
/// Begin would just refuse a second overlapping attempt with
/// <c>Contention</c>) by polling
/// <see cref="RuntimeAcceptedPositionDriveController.PendingCount"/>
/// instead.
/// </summary>
private bool _awaitingPortalWake;
/// <summary>
/// B3 review fix (2026-08-05): keys <see cref="_awaitingPortalWake"/> to
/// the exact reveal it was armed for. The graphical twin
/// (<c>_awaitingDeferredWake</c>) is naturally reset per teleport via
/// <c>ResetTransit</c>; this class is constructed per SESSION, not per
/// teleport, so without this a stale latch from reveal N could silently
/// steal reveal N+1's <c>PrepareDestination</c> call into polling a park
/// that belongs to a different, already-abandoned reveal — skipping the
/// new reveal's placement attempt entirely.
/// </summary>
private long _awaitingPortalWakeGeneration;
private ushort _awaitingPortalWakeSequence;
/// <summary>
/// N3 review fix (2026-08-05): bounds how many consecutive
/// <c>NotApplicable</c> attempts this host tolerates before treating the
/// condition as unrecoverable. <c>NotApplicable</c> covers hydration-race
/// transients (no canonical body yet, an active initial-Create residence
/// still owning the record) as well as a genuinely stale reveal — unlike
/// <c>Rejected</c>, it is not established to be permanent, and this host
/// must survive K4's 30-session / two-hour endurance profile without a
/// transient becoming fatal.
/// </summary>
private int _notApplicableRetryCount;
private const int NotApplicableRetryBudget = 50;
/// <summary>
/// C4 route 3 (D-T6): the portal-arrival placement runs through the
/// SAME canonical Runtime portal arm the graphical host drives
/// (<see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival"/>),
/// retiring the duplicate Resolve/ResolvePlacement/SetPosition authority
/// this method used to own directly (D2;
/// docs/research/2026-08-04-c4-route-3-contract.md D-T6).
///
/// <para>
/// A1/A3 review fix (2026-08-05): the first pass discarded the arm's
/// returned status entirely (<c>_ = ...</c>) and always reported
/// success, so <c>RuntimeLiveEntitySessionController.TryCompletePortal</c>
/// acknowledged a materialization that never happened on ANY refusal
/// (architecture review A3). This method now:
/// </para>
/// <list type="bullet">
/// <item><description>throws if no drive controller was wired — a
/// composition regression must not silently disable placement, never
/// pretend success (A3's second finding);</description></item>
/// <item><description>does not even ATTEMPT the placement until
/// <see cref="_collision"/> reports the destination resident — this
/// host's narrow collision window makes a premature attempt a
/// guaranteed <c>DeferredCell</c>, and <see cref="_collision"/>'s own
/// readiness IS the signal <see cref="IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition"/>'s
/// doc names as the precondition for a park to ever resolve;</description></item>
/// <item><description>reports <c>IsCollisionReady: false</c> — never
/// success — for every non-<c>Committed</c> outcome, so the caller's
/// retry loop (<c>RuntimeLiveEntitySessionController.PumpPortalCompletion</c>,
/// A1's headless-side fix) keeps calling this method instead of the
/// completion sequence running against an unplaced body; a genuine
/// <c>DeferredCell</c> is therefore never an error, only a wait — and
/// throws only for the two statuses that mean something is actually
/// wrong (<c>Rejected</c>/<c>NotApplicable</c> — the reveal itself is
/// stale, or the local player has no canonical body, neither of which
/// a headless bot can recover from by waiting).</description></item>
/// </list>
/// </summary>
public RuntimeDestinationReadiness PrepareDestination( public RuntimeDestinationReadiness PrepareDestination(
long revealGeneration, long revealGeneration,
RuntimeTeleportDestination destination) RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken portal)
{ {
_collision.CenterOn(destination.CellId); _collision.CenterOn(destination.CellId);
if (_runtime.EntityObjects.Entities.TryGetActive( if (_acceptedPositionDrive is null)
destination.EntityGuid,
out RuntimeEntityRecord record))
{ {
ResynchronizeLocalPlayerForPortalArrival(record); throw new InvalidOperationException(
"Headless portal placement requires a wired "
+ "RuntimeAcceptedPositionDriveController - a composition "
+ "regression must not silently disable placement (A3).");
} }
if (_runtime.MovementOwner.Controller is { } controller)
// B3: a latch armed for a DIFFERENT reveal must not be consulted
// for this one — fall through to a fresh attempt below instead.
if (_awaitingPortalWake
&& (_awaitingPortalWakeGeneration != revealGeneration
|| _awaitingPortalWakeSequence != destination.TeleportSequence))
{
_awaitingPortalWake = false;
}
bool committed;
if (_awaitingPortalWake)
{
if (_acceptedPositionDrive.PendingCount != 0)
{
committed = false;
}
else
{
// B1 review fix (2026-08-05): "not pending" is not
// "committed" — the drive's own doc names a merge-time
// Forget (an ordinary ACE broadcast arriving mid-park) as
// the EXPECTED way a park resolves without committing.
// TryConsumePortalCommit is the drive's OWN record of
// whether ITS commit actually happened for this exact
// reveal/sequence, latched only inside a real
// ReconcileAndAcknowledgePortal call — never inferred.
_awaitingPortalWake = false;
committed = _acceptedPositionDrive.TryConsumePortalCommit(
revealGeneration, destination.TeleportSequence);
// A "no" here falls through to committed=false below; the
// NEXT PrepareDestination call re-attempts fresh since
// _awaitingPortalWake is now false and nothing is pending.
}
}
else if (!_collision.IsReady(destination.CellId))
{
committed = false;
}
else
{
var authority = new RuntimePortalPlacementAuthority(
Present: true,
RevealGeneration: revealGeneration,
TeleportSequence: destination.TeleportSequence,
Projection: portal);
RuntimeAcceptedPositionExecutionStatus status =
_acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
destination,
authority);
switch (status)
{
case RuntimeAcceptedPositionExecutionStatus.Committed:
committed = true;
_notApplicableRetryCount = 0;
break;
case RuntimeAcceptedPositionExecutionStatus.DeferredCell:
_awaitingPortalWake = true;
_awaitingPortalWakeGeneration = revealGeneration;
_awaitingPortalWakeSequence = destination.TeleportSequence;
committed = false;
break;
case RuntimeAcceptedPositionExecutionStatus.Contention:
// Transient - some other operation still owns the
// entity's placement token. Retried next pump; never a
// hard error, matching the graphical arm's D-T5
// refusal shape.
committed = false;
break;
case RuntimeAcceptedPositionExecutionStatus.NotApplicable:
// N3 review fix (2026-08-05): NotApplicable covers
// hydration-race transients (record.PhysicsBody is
// null, or an active initial-Create residence still
// owns the record — RuntimeAcceptedPositionDriveController's
// own guard) as well as a genuinely stale reveal; unlike
// Rejected it is not established to be permanent.
// Bounded, loud retry rather than an immediate throw —
// this host must survive K4's 30-session/two-hour
// endurance profile without a transient becoming fatal.
_notApplicableRetryCount++;
PhysicsDiagnostics.LogTeleport(
"REFUSED",
destination.CellId,
$"cause=NotApplicable attempt={_notApplicableRetryCount}");
if (_notApplicableRetryCount > NotApplicableRetryBudget)
{
throw new InvalidOperationException(
"Headless portal placement stayed NotApplicable "
+ $"for {_notApplicableRetryCount} consecutive "
+ "attempts (no canonical body, or an active "
+ "initial-Create residence still owns the "
+ "record) - exceeded the bounded retry budget.");
}
committed = false;
break;
default:
throw new InvalidOperationException(
$"Headless portal placement refused with "
+ $"status={status} even though the destination's "
+ "collision neighborhood reported ready - the "
+ "reveal itself is stale, not recoverable by "
+ "waiting (contract §4 item 5 forbids "
+ "acknowledging a materialization that did not "
+ "happen).");
}
}
if (committed && _runtime.MovementOwner.Controller is { } controller)
controller.State = PlayerState.InWorld; controller.State = PlayerState.InWorld;
bool ready = _collision.IsReady(destination.CellId);
bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u; bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u;
return new RuntimeDestinationReadiness( return new RuntimeDestinationReadiness(
revealGeneration, revealGeneration,
destination.CellId, destination.CellId,
indoor, indoor,
IsUnhydratable: !ready, // N2 review fix (2026-08-05): hardcoded false, NOT DERIVED.
// AD-2's "loud unhydratable-placement path" (a claim beyond
// NumCells) is a graphical-only concept today —
// WorldRevealReadinessBarrier's render/composite-texture domains
// have no headless analogue, so there is no local predicate this
// no-window host could evaluate. A genuinely unhydratable
// destination therefore reports IsCollisionReady=false forever
// (via the bounded DeferredCell retry above) rather than taking
// AD-2's loud path — headless does not model unhydratable
// claims. If headless ever gains its own resident-cell-set
// concept, derive the real predicate here instead of leaving
// this hardcoded.
IsUnhydratable: false,
RequiredRenderRadius: indoor ? 0 : 1, RequiredRenderRadius: indoor ? 0 : 1,
IsRenderNeighborhoodReady: true, IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true, AreCompositeTexturesReady: true,
IsCollisionReady: ready); IsCollisionReady: committed);
}
/// <summary>
/// TODO-C4 (route 3): portal-arrival re-synchronization only. The
/// route-1/8 initial-entry hand-copy (controller construction + first
/// resolve/placement) was deleted at C3c — the first-entry conductor's
/// publication chain owns it — but the portal route is unflipped, so its
/// arrival re-resolve keeps today's exact behavior against the
/// already-published controller until C4 routes it through
/// RuntimePortalPlacementAuthority.
/// </summary>
private void ResynchronizeLocalPlayerForPortalArrival(
RuntimeEntityRecord record)
{
if (record.ServerGuid
!= _runtime.PlayerIdentity.ServerGuid
|| record.Snapshot.Position is not { } position
|| _runtime.MovementOwner.Controller is not { } controller)
{
return;
}
_collision.CenterOn(position.LandblockId);
Vector3 wirePosition = new(
position.PositionX,
position.PositionY,
position.PositionZ);
Quaternion orientation = new(
position.RotationX,
position.RotationY,
position.RotationZ,
position.RotationW);
ResolveResult resolved =
_runtime.EntityObjects.Physics.Engine.Resolve(
wirePosition,
position.LandblockId,
Vector3.Zero,
100f);
ResolveResult placement =
_runtime.EntityObjects.Physics.Engine.ResolvePlacement(
resolved.Position,
resolved.CellId,
DefaultRadius,
DefaultHeight,
controller.StepUpHeight,
controller.StepDownHeight,
ObjectInfoState.IsPlayer
| ObjectInfoState.EdgeSlide,
record.LocalEntityId ?? 0u);
if (placement.Ok)
resolved = placement;
controller.LocalEntityId = record.LocalEntityId ?? 0u;
controller.SetPosition(
resolved.Position,
resolved.CellId,
wirePosition);
controller.SetBodyOrientation(orientation);
} }
} }

View file

@ -1949,6 +1949,145 @@ public sealed class PlayerMovementController
UpdateCellId(_body.CellPosition.ObjCellId, "force-position"); UpdateCellId(_body.CellPosition.ObjCellId, "force-position");
} }
/// <summary>
/// C4 route 3: the controller-local half of a portal-teleport commit
/// whose body write, cell install, and orientation already happened
/// inside Runtime's canonical <c>RuntimeSetPositionState.CommitCanonical</c>
/// (retail <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0 with flags
/// <c>0x1012</c>, called from <c>SmartBox::TeleportPlayer</c> @0x00453910,
/// acclient_2013_pseudo_c.txt:284276/92528). Unlike
/// <see cref="CommitCanonicalForcePositionFrame"/> (which the FORCE_POSITION
/// branch's early return at @0x0045409D exempts from every
/// <c>ConstrainTo</c>), the local TELEPORT branch of
/// <c>SmartBox::HandleReceivedPosition</c> (@0x0045415F) DOES re-arm the
/// leash (@0x0045418A, anchored at the received destination) and DOES
/// zero velocity (@0x004541B4) — the inversion is deliberate, not a
/// missed exemption; see docs/research/2026-08-04-c4-route-3-contract.md
/// §2 Inversion A.
///
/// Performs every <see cref="SetPositionCore"/> duty NOT already covered
/// by the canonical commit (P1's duty map): render-lerp anchor reset,
/// <c>UpdateCellId</c> publication, the retail teleport_hook tail
/// (UnStick @0x00514eee / UnConstrain @0x00514f02 / re-arm @0x0045418A),
/// the retail StopCompletely full stop (0x00527e40, zeroes velocity and
/// resets fwd/sidestep/turn commands so input resumes at rest), the
/// input-edge/mouse press-edge reset, and the physics-clock reset for a
/// fresh <c>update_object</c> boundary. TransientState (Contact/OnWalkable/
/// Sliding/WaterContact) is deliberately NOT re-seeded here — the canonical
/// commit's <c>PhysicsObjUpdate.CommitSetPositionContactTransition</c>
/// already derives those bits from the SLIDE placement's OWN resolved
/// contact result, which is more retail-faithful than the old
/// <see cref="SetPositionCore"/>'s unconditional
/// <c>Contact|OnWalkable|Active</c> overwrite (that overwrite could mark a
/// portal arrival grounded even when the destination placement actually
/// resolved airborne). <c>Active</c> is untouched because a live in-world
/// local player already carries it; the canonical commit only sets it on
/// entry from a celless residence, which a portal arrival never is.
/// </summary>
/// <summary>
/// A4 review fix (2026-08-05): the two inversions this method embodies
/// are named, retail-cited facts on the classifier's
/// <c>RuntimeAuthoritativePositionRoute</c> — <c>ZeroVelocity</c> and
/// <c>ConstrainPhase.AfterPositionOperation</c> — and this method must
/// actually READ them rather than assume the LocalPlayer-teleport
/// branch's values are the only ones that will ever reach it. Both
/// parameters are the route's own facts, passed by the one caller
/// (<c>RuntimeAcceptedPositionDriveController.ReconcileAndAcknowledgePortal</c>);
/// a future classifier edit that changes either value now changes this
/// method's behaviour instead of silently disagreeing with it.
/// <para>
/// Coordinator note (round-3 closeout, 2026-08-05): the two parameters
/// are read, not hardcoded — but they are NOT equally load-bearing.
/// <paramref name="zeroVelocity"/> is read and applied, then
/// <see cref="StopCompletelyAtPhysicsObjectBoundary"/> runs
/// UNCONDITIONALLY on the very next line and zeroes velocity again — so
/// a <c>zeroVelocity: false</c> sabotage changes nothing observable
/// here; the field is proven read but not proven DISCRIMINATING.
/// <paramref name="rearmConstraintLeash"/> (<c>ConstrainAfterRouting</c>)
/// has no such unconditional fallback and IS the load-bearing one —
/// it alone decides whether the leash re-arms. Do not read this doc
/// comment as proving both fields equally; only the leash flag is.
/// </para>
/// </summary>
/// <param name="runTeleportHookTail">
/// N4 review fix (2026-08-05): before this parameter, the caller gated
/// the ENTIRE method call on <c>route.RunsTeleportHook</c> — but retail's
/// <c>SetPositionInternal</c> @0x00515330 does the frame/cell/stop/input-
/// reset/clock work UNCONDITIONALLY; only retail's <c>teleport_hook</c>
/// @0x00514ED0 (UnStick/UnConstrain/re-arm, mapped below) is itself
/// conditional on the hook phase. Gating the whole call meant a future
/// <see cref="AcDream.Runtime.Physics.RuntimeAuthoritativePositionRoute.TeleportHookPhase"/>
/// of <c>None</c> would silently skip the render-root <c>UpdateCellId</c>
/// publish too — the doorway-FLAP class. Today the portal route always
/// sets a non-None phase, so this parameter is always <c>true</c> in
/// production and there is no live behavior change; it exists so a
/// future <c>None</c> phase changes only the hook tail, not the frame
/// commit.
/// </param>
internal void CommitCanonicalTeleportFrame(
bool zeroVelocity,
bool rearmConstraintLeash,
bool runTeleportHookTail = true)
{
EnsurePublishedForRuntimeOperation();
_prevPhysicsPos = _body.Position;
_currPhysicsPos = _body.Position;
UpdateCellId(_body.CellPosition.ObjCellId, "teleport");
// Retail set_velocity(player, 0, 1) @0x004541B4 — route.ZeroVelocity.
if (zeroVelocity)
_body.Velocity = Vector3.Zero;
// Retail teleport idle is a FULL stop (StopCompletely 0x00527e40):
// resets fwd/sidestep/turn COMMANDS and zeroes velocity again so the
// motion interpreter cannot reconstruct the pre-teleport run vector
// the instant input resumes.
StopCompletelyAtPhysicsObjectBoundary();
_activeInputTurnCommand = null;
_activeInputTurnSpeed = 0f;
_activeInputTurnFromMouse = false;
_activeInputSidestepCommand = null;
_activeInputSidestepUsesRunHold = false;
_mouseLookActive = false;
_mouseTurnSamplePending = false;
_mouseTurnAdjustment = 0f;
_mouseMovementEventCandidate = false;
_mouseMovementEventPending = false;
// Retail teleport_hook @0x00514ed0 tears down any active stick/leash
// unconditionally, then HandleReceivedPosition's TELEPORT branch
// immediately re-arms the leash anchored to the just-committed
// position ONLY when ConstrainPhase is AfterPositionOperation
// (Inversion A — the opposite of
// CommitCanonicalForcePositionFrame's no-re-arm rule, itself
// route.ConstrainPhase.None for FORCE_POSITION). N4 review fix: this
// is the ONLY part of this method retail actually conditions on the
// teleport-hook phase — everything above runs unconditionally.
if (runTeleportHookTail)
{
PositionManager?.UnStick();
PositionManager?.UnConstrain();
if (rearmConstraintLeash)
RearmConstraintLeashAtCurrentPosition();
}
// Reset the edge tracker: the stop wiped the motion state, so keys
// still physically held must re-fire as press edges on the next
// Update (matches SetPositionCore's walking-straight-out-of-a-
// teleport behavior while W stays held).
_prevForwardHeld = false;
_prevBackwardHeld = false;
_prevStrafeLeftHeld = false;
_prevStrafeRightHeld = false;
_prevTurnLeftHeld = false;
_prevTurnRightHeld = false;
_prevRunHeld = false;
_hasInputSnapshot = false;
// Reset physics clock so any subsequent update_object calls start fresh.
_body.LastUpdateTime = 0.0;
_objectClock.ResetForEnterWorld();
}
private Vector3 ComputeRenderPosition() private Vector3 ComputeRenderPosition()
{ {
float alpha = Math.Clamp( float alpha = Math.Clamp(

View file

@ -130,6 +130,11 @@ internal enum RuntimeAcceptedPositionExecutionStatus : byte
/// early return (@0x0045409D) never reaches; the deleted /// early return (@0x0045409D) never reaches; the deleted
/// <c>PlayerMovementController.BlipPosition</c>'s re-arm was an unbacked /// <c>PlayerMovementController.BlipPosition</c>'s re-arm was an unbacked
/// deviation this route retires (docs/research/2026-08-03-c4-route-2-implementation-plan.md §1b). /// deviation this route retires (docs/research/2026-08-03-c4-route-2-implementation-plan.md §1b).
/// This no-re-arm rule is scoped to FORCE_POSITION only — C4 route 3's
/// portal arm (<see cref="TryExecuteAcceptedPortalArrival"/>) DOES re-arm,
/// because retail's local TELEPORT branch of the same function reaches
/// <c>ConstrainTo</c> @0x0045418A; see
/// docs/research/2026-08-04-c4-route-3-contract.md §2 Inversion A.
/// ///
/// One instance per host session route (graphical/headless), constructed /// One instance per host session route (graphical/headless), constructed
/// once per host process and reused across reconnects exactly like /// once per host process and reused across reconnects exactly like
@ -170,8 +175,26 @@ public sealed class RuntimeAcceptedPositionDriveController
/// begin: that marker stands for a packet whose placement was never /// begin: that marker stands for a packet whose placement was never
/// begun, so its ack is owed by the eventual re-issue's own terminal /// begun, so its ack is owed by the eventual re-issue's own terminal
/// outcome, not by the marker. /// outcome, not by the marker.
///
/// C4 route 3: always <c>false</c> for a portal pending — the
/// portal route's <see cref="RuntimeAuthoritativePositionRoute.SendPositionImmediately"/>
/// is always <c>false</c> (retail's teleport branch never sends
/// <c>AutonomousPosition</c>), so there is never an owed position
/// event to carry.
/// </summary> /// </summary>
internal required bool PositionEventOwed { get; init; } internal required bool PositionEventOwed { get; init; }
/// <summary>
/// C4 route 3: <see cref="RuntimePortalPlacementAuthority.Present"/>
/// when this descriptor is the trap T7 (2026-08-04 contract) portal
/// arm's own DeferredCell park — a SIBLING use of this same
/// retained-operation machinery, not a repurposed force pending.
/// <see cref="Advance"/> and <see cref="SubmitAndResolvePortal"/> are
/// the only writers/readers that branch on it; the force funnel
/// (<see cref="SettlePending"/>, <see cref="_newestForce"/>) never
/// sees or produces a portal pending.
/// </summary>
internal RuntimePortalPlacementAuthority Portal { get; init; }
} }
/// <summary> /// <summary>
@ -218,6 +241,40 @@ public sealed class RuntimeAcceptedPositionDriveController
private readonly Func<bool> _usePositionFromServer; private readonly Func<bool> _usePositionFromServer;
private readonly Func<WorldSession?> _session; private readonly Func<WorldSession?> _session;
/// <summary>
/// C4 route 3: the D-T3 <c>PlayerTeleported</c> port
/// (<c>CommandInterpreter::PlayerTeleported</c> @0x006B32B0 =
/// <c>SetAutoRun(0,1)</c> + <c>SendMovementEvent</c>) needs the J5.4
/// autorun latch owner, which lives one level above
/// <see cref="PlayerMovementController"/> and is not reachable from
/// <see cref="_localController"/>. Late-bound like every other
/// dependency here so this controller does not need to outlive a
/// specific movement-owner instance across a reconnect.
/// </summary>
private readonly Func<RuntimeLocalPlayerMovementState?> _localMovementState;
/// <summary>
/// A2 review fix (2026-08-05, D-T2.4): re-validates a retained portal
/// authority against the transit owner's CURRENT reveal before either
/// wake site in <see cref="Advance"/> acts on it. A <c>DeferredCell</c>
/// park commits asynchronously (<c>RuntimeSetPositionState.RetryDeferred</c>,
/// driven entirely by an unrelated collision-generation wake) — nothing
/// in this class can prevent that body-level commit once it starts. What
/// this predicate CAN prevent is running the reconcile/ack suffix (or a
/// stale resubmission) against a reveal that ended or was superseded
/// while the park was outstanding — exactly the D-T2.4 requirement this
/// slice's first pass never implemented (architecture review A2). Wired
/// by each host composition to
/// <c>RuntimeWorldTransitState.CanPlacePortalDestination</c> (the SAME
/// idempotent query <see cref="TryExecuteAcceptedPortalArrival"/>'s
/// caller already uses at the Place edge); left <see langword="null"/>
/// by fixtures that do not exercise the DeferredCell wake, in which case
/// every retained portal pending is treated as still current (today's
/// unconditional behaviour, preserved for callers that never park).
/// </summary>
private readonly Func<RuntimePortalPlacementAuthority, bool>?
_isPortalAuthorityCurrent;
/// <summary> /// <summary>
/// The drive's at-most-one in-flight placement for the local player. /// The drive's at-most-one in-flight placement for the local player.
/// Round 2 unified mechanism (2026-08-03): exactly THREE members write /// Round 2 unified mechanism (2026-08-03): exactly THREE members write
@ -244,7 +301,9 @@ public sealed class RuntimeAcceptedPositionDriveController
Func<uint> localPlayerServerGuid, Func<uint> localPlayerServerGuid,
Func<PlayerMovementController?> localController, Func<PlayerMovementController?> localController,
Func<bool> usePositionFromServer, Func<bool> usePositionFromServer,
Func<WorldSession?> session) Func<WorldSession?> session,
Func<RuntimeLocalPlayerMovementState?>? localMovementState = null,
Func<RuntimePortalPlacementAuthority, bool>? isPortalAuthorityCurrent = null)
{ {
_entityObjects = entityObjects _entityObjects = entityObjects
?? throw new ArgumentNullException(nameof(entityObjects)); ?? throw new ArgumentNullException(nameof(entityObjects));
@ -262,12 +321,53 @@ public sealed class RuntimeAcceptedPositionDriveController
_usePositionFromServer = usePositionFromServer _usePositionFromServer = usePositionFromServer
?? throw new ArgumentNullException(nameof(usePositionFromServer)); ?? throw new ArgumentNullException(nameof(usePositionFromServer));
_session = session ?? throw new ArgumentNullException(nameof(session)); _session = session ?? throw new ArgumentNullException(nameof(session));
_localMovementState = localMovementState ?? (static () => null);
_isPortalAuthorityCurrent = isPortalAuthorityCurrent;
_entityObjects.RegisterAcceptedPositionDriveOwnership( _entityObjects.RegisterAcceptedPositionDriveOwnership(
() => _pending is null ? 0 : 1); () => _pending is null ? 0 : 1);
} }
internal int PendingCount => _pending is null ? 0 : 1; internal int PendingCount => _pending is null ? 0 : 1;
/// <summary>
/// B1 review fix (2026-08-05): the drive's own record of the LAST portal
/// authority <see cref="ReconcileAndAcknowledgePortal"/> actually
/// committed — set only there, so this is never an inference. Both host
/// gates were latching "committed" from <see cref="PendingCount"/>
/// reaching zero, but that global (force-arm-shared) slot ALSO clears on
/// three non-committing paths (a merge-time <c>Forget</c> — the drive's
/// own doc names this the EXPECTED outcome of a park surviving one ACE
/// broadcast interval — and both of A2's new abandon branches), so
/// "not pending" never implied "this specific reveal placed". Consumed
/// exactly once per commit via <see cref="TryConsumePortalCommit"/>.
/// </summary>
private (long RevealGeneration, ushort TeleportSequence)? _lastCommittedPortal;
/// <summary>
/// B1 review fix: the host gate's ONLY correct way to learn "did MY
/// specific reveal commit" — never infer it from
/// <see cref="PendingCount"/>. Returns <see langword="true"/> and
/// consumes the fact exactly once when the drive's last portal commit
/// matches the caller's own (revealGeneration, teleportSequence); a
/// mismatch (nothing committed yet, a DIFFERENT reveal committed, or
/// this generation's park was abandoned/forgotten instead) returns
/// <see langword="false"/> without side effects, so the caller keeps
/// retrying or falls through to a fresh attempt.
/// </summary>
internal bool TryConsumePortalCommit(
long revealGeneration,
ushort teleportSequence)
{
if (_lastCommittedPortal is not { } committed
|| committed.RevealGeneration != revealGeneration
|| committed.TeleportSequence != teleportSequence)
{
return false;
}
_lastCommittedPortal = null;
return true;
}
/// <summary> /// <summary>
/// C3c-R1-style one-route-at-a-time latch (mirrors /// C3c-R1-style one-route-at-a-time latch (mirrors
/// <see cref="RuntimeFirstEntryDriveController.AttachRoute"/>): this /// <see cref="RuntimeFirstEntryDriveController.AttachRoute"/>): this
@ -308,10 +408,22 @@ public sealed class RuntimeAcceptedPositionDriveController
/// force observation dies with it: a reconnect re-merges its own /// force observation dies with it: a reconnect re-merges its own
/// positions, and a stale observation must never survive to authorize a /// positions, and a stale observation must never survive to authorize a
/// re-issue against a later session's record. /// re-issue against a later session's record.
/// <para>
/// Coordinator hygiene fix (round-3 closeout, 2026-08-05): also clears
/// <see cref="_lastCommittedPortal"/>. An unconsumed latch surviving a
/// session reset was harmless only because the transit's own
/// generation counter is monotonic across resets within one
/// <c>GameRuntime</c> lifetime, so a stale entry could never match a
/// later reveal's generation/sequence pair by construction — a
/// correctness argument resting on an invariant this method never
/// declared. Clearing it here lets the ledger converge to zero on
/// every reset instead of relying on that invariant to stay true.
/// </para>
/// </summary> /// </summary>
private void AbandonPending() private void AbandonPending()
{ {
_newestForce = null; _newestForce = null;
_lastCommittedPortal = null;
if (_pending is not { } pending) if (_pending is not { } pending)
return; return;
_pending = null; _pending = null;
@ -401,6 +513,411 @@ public sealed class RuntimeAcceptedPositionDriveController
return SubmitAndResolve(record, token, route); return SubmitAndResolve(record, token, route);
} }
/// <summary>
/// C4 route 3: executes the local player's portal arrival against the
/// canonical Runtime SetPosition owner. Retail
/// <c>SmartBox::TeleportPlayer</c> @0x00453910 =
/// <c>CPhysicsObj::SetPositionSimple(player, dest, 1)</c> — the SAME
/// generic primitive route 2 already routes through
/// <see cref="TryExecuteAcceptedLocalPosition"/> — plus
/// <c>PlayerPositionUpdated</c>. <paramref name="destination"/> must be
/// the transit's OWN retained accepted destination
/// (<c>RuntimeWorldTransitState.TryGetAcceptedTeleportDestination</c>),
/// never re-derived from live per-tick timestamps: by the time the Place
/// edge fires, the packet merged seconds ago and nothing is "advancing"
/// anymore (docs/research/2026-08-04-c4-route-3-contract.md D-T2.2).
/// </summary>
internal RuntimeAcceptedPositionExecutionStatus TryExecuteAcceptedPortalArrival(
in RuntimeTeleportDestination destination,
in RuntimePortalPlacementAuthority portal)
{
if (!portal.IsValid
|| !_entityObjects.Entities.TryGetActive(
_localPlayerServerGuid(), out RuntimeEntityRecord record)
|| record.PhysicsBody is null
|| record.Key is not { } key
// Route 1 owns an active initial-Create residence exactly like
// route 2's equivalent guard above — the residence executor's
// own tail action already carries any position it needs.
|| _entityObjects.TryGetInitialCreateResidence(record, out _))
{
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.NotApplicable,
portal,
resolvedCell: 0u);
return RuntimeAcceptedPositionExecutionStatus.NotApplicable;
}
RuntimeAuthoritativePositionRoute route = ClassifyPortalArrival(
record, key, destination, _generation());
if (!route.Accepted)
{
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.Rejected,
portal,
record.FullCellId);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
ulong acceptedVersion = record.PositionAuthorityVersion;
RuntimeEntityPlacementToken token =
setPosition.TryBeginExclusiveAuthoredPlacement(
record,
acceptedVersion,
route.OperationKind,
portal);
if (!token.IsValid)
{
// Either a concurrent placement authority already owns the
// entity, or Begin's own portal-vs-latest-cell gate refused
// (D-T5's Begin cell-mismatch edge — a second local Position
// merged between the offer and this Place edge). Neither is
// staleness; the caller's D-T5 refusal handling owns what
// happens next.
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.Contention,
portal,
record.FullCellId);
return RuntimeAcceptedPositionExecutionStatus.Contention;
}
return SubmitAndResolvePortal(record, token, route, portal);
}
/// <summary>
/// C4 route 3: the classifier's LocalPlayer-teleport route
/// (<see cref="RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition"/>,
/// <c>request.Authority.TeleportAdvanced</c> branch) built from the
/// retained destination rather than a live merge. Retail's
/// <c>PhysicsTimestampGate.IsNewer(PreviousTeleportSequence,
/// AcceptedTeleportSequence)</c> gate only needs to be TRUE — its exact
/// magnitude is not read anywhere past that boolean (the classifier's
/// resulting <see cref="RuntimeAuthoritativePositionRoute"/> for this
/// branch does not depend on the previous stamp's value, and the drive
/// controller's own `expectedPositionAuthorityVersion` — not this
/// authority's — gates Begin), so a synthetic strictly-older sequence
/// forces retail's exact branch without any second copy of the merge-time
/// timestamp pair having to survive from offer to Place.
/// </summary>
private static RuntimeAuthoritativePositionRoute ClassifyPortalArrival(
RuntimeEntityRecord record,
RuntimeEntityKey key,
in RuntimeTeleportDestination destination,
RuntimeGenerationToken generation)
{
ushort acceptedTeleport = destination.TeleportSequence;
ushort priorTeleport = unchecked((ushort)(acceptedTeleport - 1));
var authority = new RuntimeAuthoritativePositionAuthority(
generation,
key,
record.PositionAuthorityVersion,
destination.PositionSequence,
priorTeleport,
acceptedTeleport,
PositionTimestampDisposition.Apply);
bool hasAnimations = (record.Snapshot.MotionTableId
?? record.Snapshot.Physics?.MotionTableId) is { } motionTableId
&& motionTableId != 0u;
var wirePosition = new CreateObject.ServerPosition(
destination.CellId,
destination.Position.Frame.Origin.X,
destination.Position.Frame.Origin.Y,
destination.Position.Frame.Origin.Z,
destination.Position.Frame.Orientation.W,
destination.Position.Frame.Orientation.X,
destination.Position.Frame.Orientation.Y,
destination.Position.Frame.Orientation.Z);
var request = new RuntimeAcceptedPositionRouteRequest(
authority,
RuntimePositionEntityKind.LocalPlayer,
RuntimeAcceptedPositionSource.PositionEvent,
wirePosition,
PlacementFrame: null,
PositionPackVelocity: null,
CommittedCellId: record.FullCellId,
HasContact: false,
PlayerDistance: 0f,
UsePositionFromServer: false,
hasAnimations,
new RuntimePositionPlacementFacts(
record.FinalPhysicsState,
record.Snapshot.SetupTableId is not null));
return RuntimeAuthoritativePositionRouteClassifier
.ClassifyAcceptedPosition(request);
}
/// <summary>
/// C4 route 3 (trap T7): the portal SIBLING of <see cref="SubmitAndResolve"/>
/// — shares Begin/Submit/status handling, deliberately does NOT touch
/// <see cref="_newestForce"/> or route through <see cref="SettlePending"/>'s
/// force-shaped re-issue funnel. ACE sends one destination per teleport;
/// a portal placement that fails to commit is never re-applied.
/// </summary>
private RuntimeAcceptedPositionExecutionStatus SubmitAndResolvePortal(
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken token,
in RuntimeAuthoritativePositionRoute route,
in RuntimePortalPlacementAuthority portal)
{
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
RuntimeSetPositionMoverPreparationStatus status =
setPosition.TryPrepareAndSubmitAuthoredPlacement(
record,
token,
route.OperationKind,
route.SetPositionFlags,
_collisionSource,
_clock.SimulationTimeSeconds,
out RuntimeSetPositionOutcome outcome,
portal: portal,
resolveWorldOffsetFromRuntimeFrame: true);
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
{
if (status.IsRetryable())
{
RetainPending(setPosition, new Pending
{
Record = record,
Token = token,
Route = route,
AwaitingCommitWake = false,
PositionEventOwed = false,
Portal = portal,
});
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.Contention,
portal,
record.FullCellId);
return RuntimeAcceptedPositionExecutionStatus.Contention;
}
CancelToken(setPosition, token);
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.Rejected,
portal,
record.FullCellId);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
switch (outcome.Status)
{
case RuntimeSetPositionStatus.CommittedHostAcknowledgementPending:
ReconcileAndAcknowledgePortal(record, route, portal);
return RuntimeAcceptedPositionExecutionStatus.Committed;
case RuntimeSetPositionStatus.DeferredCell:
// D-T2.4: a park should be rare (the destination was already
// centered by the host before submit), but must never leak —
// same drain-stale-Withdraw-then-watch shape as the force arm.
while (setPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot parked)
&& parked.Token.Entity == token.Entity
&& parked.Kind is RuntimePlacementProjectionKind.Withdraw)
{
if (!setPosition.AcknowledgeProjection(parked.Token))
break;
}
if (!setPosition.WatchPlacementCompletion(token))
{
CancelToken(setPosition, token);
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.Rejected,
portal,
record.FullCellId);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
RetainPending(setPosition, new Pending
{
Record = record,
Token = token,
Route = route,
AwaitingCommitWake = true,
PositionEventOwed = false,
Portal = portal,
});
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
portal,
record.FullCellId);
return RuntimeAcceptedPositionExecutionStatus.DeferredCell;
default:
// Rejected/Cancelled — authority moved out from under this
// operation; the body never moved. Unlike the force arm,
// retail's teleport branch has no unconditional ack to send,
// so there is nothing left to do here.
CancelToken(setPosition, token);
LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus.Rejected,
portal,
record.FullCellId);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
}
/// <summary>
/// R8 review fix (2026-08-05): D-T8 specified "one line per
/// portal-arrival ATTEMPT", but the first pass logged only from
/// <see cref="ReconcileAndAcknowledgePortal"/> — reached solely on
/// <c>Committed</c> — so every refusal was invisible under the gate's
/// own pinned <c>ACDREAM_PROBE_LOCAL_TELEPORT</c> env var (the graphical
/// refusal path logged under the DIFFERENT <c>ACDREAM_PROBE_TELEPORT</c>,
/// and headless logged nothing at all). Every non-terminal/refusal exit
/// from <see cref="TryExecuteAcceptedPortalArrival"/> and
/// <see cref="SubmitAndResolvePortal"/> now emits through this one
/// helper; the richer hookTail/leash/autorun facts remain
/// <see cref="ReconcileAndAcknowledgePortal"/>'s own line on the
/// <c>Committed</c> path, since those three booleans are meaningless
/// before a commit.
/// </summary>
private static void LogPortalArrivalAttempt(
RuntimeAcceptedPositionExecutionStatus status,
in RuntimePortalPlacementAuthority portal,
uint resolvedCell)
{
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "portal",
placementStatus: status.ToString(),
portalGeneration: portal.RevealGeneration,
teleportSequence: portal.TeleportSequence,
destinationCell: portal.Projection.DestinationCell,
resolvedCell: resolvedCell,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
}
/// <summary>
/// A2/D-T2.4 review fix (2026-08-05): treats a portal authority as
/// current when no re-validation predicate was wired (today's
/// unconditional behaviour, preserved for fixtures that never park), and
/// otherwise defers to <see cref="_isPortalAuthorityCurrent"/> — which
/// production wires to the SAME
/// <c>RuntimeWorldTransitState.CanPlacePortalDestination</c> query the
/// App/headless Place edge itself uses.
/// </summary>
private bool IsPortalAuthorityCurrent(
in RuntimePortalPlacementAuthority portal) =>
_isPortalAuthorityCurrent is null || _isPortalAuthorityCurrent(portal);
/// <summary>
/// C4 route 3: the committed-portal-placement controller-local
/// reconciliation and outbound tail. Runs
/// <see cref="PlayerMovementController.CommitCanonicalTeleportFrame"/>
/// (the re-homed <c>SetPositionCore</c> duties, D-T3), then the
/// <c>PlayerTeleported</c> port (<c>CommandInterpreter::PlayerTeleported</c>
/// @0x006B32B0 = <c>SetAutoRun(0,1)</c> + <c>SendMovementEvent</c>) — two
/// named behaviour changes versus the deleted App/Headless placement
/// authorities: autorun now cancels on portal arrival, and exactly one
/// movement-event refresh goes out (never an <c>AutonomousPosition</c> —
/// the route's <c>SendPositionImmediately</c> is always false).
///
/// <para>
/// A4 review fix (2026-08-05): <paramref name="route"/>'s
/// <c>ZeroVelocity</c>/<c>ConstrainPhase</c>/<c>TeleportHookPhase</c> are
/// now READ, not assumed — <see cref="RuntimeAuthoritativePositionRoute.RunsTeleportHook"/>
/// gates whether the hook tail runs at all, and its
/// <c>ZeroVelocity</c>/<see cref="RuntimeAuthoritativePositionRoute.ConstrainAfterRouting"/>
/// drive <c>CommitCanonicalTeleportFrame</c>'s two conditional duties.
/// The LocalPlayer-teleport branch's values are unchanged today
/// (<c>AfterPositionOperation</c>/<c>AfterPositionOperation</c>/<c>true</c>),
/// so this is purely a wiring correction: a future classifier edit now
/// changes this method's behaviour instead of silently disagreeing with
/// it, and the contract's own §8 item 11 sabotage (force the classifier
/// onto <c>ConstrainPhase.None</c> — the leash must not re-arm) can
/// finally fail as designed.
/// </para>
/// </summary>
private void ReconcileAndAcknowledgePortal(
RuntimeEntityRecord record,
in RuntimeAuthoritativePositionRoute route,
in RuntimePortalPlacementAuthority portal)
{
// B1 review fix (2026-08-05): this method is called ONLY from the
// two sites that just observed Runtime's canonical
// CommittedHostAcknowledgementPending outcome for THIS portal
// authority (SubmitAndResolvePortal's first-attempt commit and
// Advance's re-validated deferred wake) — so the commit fact is
// true here regardless of whether the two guards below decline the
// REST of this method's App-level suffix work. Latching it FIRST,
// unconditionally, is what lets TryConsumePortalCommit replace the
// unsound PendingCount==0 inference both host gates used to make.
_lastCommittedPortal = (portal.RevealGeneration, portal.TeleportSequence);
if (record.ServerGuid != _localPlayerServerGuid())
return;
if (_localController() is not { } controller)
return;
// N4 review fix (2026-08-05): the frame/cell/stop/input-reset/clock
// commit runs UNCONDITIONALLY (retail's SetPositionInternal
// @0x00515330 has no hook-phase gate); only the UnStick/UnConstrain/
// re-arm tail inside it is conditioned on the hook phase, via
// runTeleportHookTail. Previously this whole call was skipped when
// RunsTeleportHook was false, which would have silently dropped the
// render-root UpdateCellId publish too (the doorway-FLAP class) the
// day a route ever sets TeleportHookPhase.None — today's portal
// route always sets a non-None phase, so this is a structural fix
// with no live behavior change yet.
bool hookTailRan = route.RunsTeleportHook;
controller.CommitCanonicalTeleportFrame(
zeroVelocity: route.ZeroVelocity,
rearmConstraintLeash: route.ConstrainAfterRouting,
runTeleportHookTail: hookTailRan);
bool autorunCancelled = _localMovementState()?.CancelAutoRun() ?? false;
// R7 review fix (2026-08-05): retail CommandInterpreter::SendMovementEvent
// @0x006B4680 (PlayerTeleported's tail-jump) gates on TWO facts — a
// non-null raw motion state (TryGetOutboundPosition/TryGetOutboundMotion
// already cover that) AND `autonomy_level != 0`. This call was
// unconditional. This is route 3's OWN call site only —
// LocalPlayerOutboundController.TrySendMovement is shared with
// route 2's DIFFERENT retail function
// (CommandInterpreter::SendPositionEvent) and is not touched.
//
// Known approximation, filed AP-144 (2026-08-05, R7 round-3 review —
// CLAUDE.md's register rule is binding, not an implementer's call):
// this class only has RuntimeCharacterState.UsePositionFromServer in
// scope (`AutonomyLevel != FullAutonomyLevel(2)`, retail's
// `autonomy_level != 2`), not the raw AutonomyLevel — so
// `!UsePositionFromServer` sends only when AutonomyLevel==2,
// whereas retail's actual gate (`autonomy_level != 0`) ALSO sends
// at AutonomyLevel==1. The two agree everywhere except that one mid
// level, currently unreachable because TrySetAutonomyLevel has zero
// production callers. Retire by threading the raw AutonomyLevel
// through this constructor (and both host compositions) and gating
// on `!= 0` directly instead of reusing UsePositionFromServer.
if (!_usePositionFromServer())
{
_localPlayerOutbound.TrySendMovement(
_session(),
controller,
controller.CapturePresentationResult());
}
// D-T8 probe (temporary): confirms the reconcile suffix actually
// ran its three named duties on THIS commit, not just that the
// commit was reached. R3 review fix: the leash observable is
// ConstraintManager.IsConstrained ("has a leash"), not
// IsFullyConstrained ("has strained past 90% of it") — the latter
// reads false immediately after ConstrainTo re-anchors at distance
// 0, so every committed arrival printed leash=unarmed as coded.
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "portal",
placementStatus: "Committed",
portalGeneration: portal.RevealGeneration,
teleportSequence: portal.TeleportSequence,
destinationCell: portal.Projection.DestinationCell,
resolvedCell: record.FullCellId,
hookTailRan: hookTailRan,
leashArmed: controller.PositionManager?.Constraint?.IsConstrained
?? false,
autorunCancelled: autorunCancelled);
}
/// <summary> /// <summary>
/// Host cadence pump: resolves a parked DeferredCell operation once its /// Host cadence pump: resolves a parked DeferredCell operation once its
/// destination landblock's collision generation eventually commits it /// destination landblock's collision generation eventually commits it
@ -461,6 +978,42 @@ public sealed class RuntimeAcceptedPositionDriveController
// Raced against a concurrent consumer; retry next pump. // Raced against a concurrent consumer; retry next pump.
return; return;
} }
// C4 route 3 (trap T7): a portal pending never enters the
// force funnel — SettlePending's _newestForce re-issue
// decision belongs to the force arm only. The deferred
// commit's own reconciliation is the portal wake's entire
// terminal action.
if (pending.Portal.Present)
{
_pending = null;
// A2/D-T2.4 re-validation: RetryDeferred already moved
// the body (asynchronously, outside this class's
// control — nothing here can prevent that). What this
// CAN prevent is running the reconcile/ack suffix
// against a reveal that ended or was superseded while
// the park sat outstanding, which would otherwise
// publish a Place receipt naming a dead portal
// authority (architecture review A2's FIFO-wedge
// shape).
if (!IsPortalAuthorityCurrent(pending.Portal))
{
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "portal",
placementStatus: "AbandonedAtWake",
portalGeneration: pending.Portal.RevealGeneration,
teleportSequence: pending.Portal.TeleportSequence,
destinationCell:
pending.Portal.Projection.DestinationCell,
resolvedCell: pending.Record.FullCellId,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return;
}
ReconcileAndAcknowledgePortal(
pending.Record, pending.Route, pending.Portal);
return;
}
// Retail order: the deferred commit's own reconciliation and // Retail order: the deferred commit's own reconciliation and
// ack come first, THEN the funnel decides whether a newer // ack come first, THEN the funnel decides whether a newer
// accepted force is still owed a placement (B1). // accepted force is still owed a placement (B1).
@ -480,9 +1033,39 @@ public sealed class RuntimeAcceptedPositionDriveController
} }
// The watch died — most likely a subsequent accepted Position's // The watch died — most likely a subsequent accepted Position's
// merge-time Forget. The funnel owns what happens next, including // merge-time Forget. A portal pending owes no re-issue and no
// this packet's still-unsent position event (retail acks whether // ack (SendPositionImmediately is always false for the portal
// or not the placement took — see SettlePending). // route), so it simply clears — matching the D-T5 refusal shape
// (nothing mutates; the transit's own cancellation/supersession
// machinery is the authority on what happens next).
//
// B1/N1 review fix (2026-08-05): this is the drive's own
// documented MODAL park outcome (ACE's 5-10 Hz broadcast Forgets
// any park surviving one interval — "the exact far-destination
// case the park exists to serve"), not a corner case. It does
// NOT set _lastCommittedPortal — nothing committed — so the host
// gate's TryConsumePortalCommit correctly reports "not yet" and
// either re-attempts fresh or converges through the transit's
// own cancellation, instead of the old PendingCount==0
// inference latching a false "committed".
if (pending.Portal.Present)
{
_pending = null;
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "portal",
placementStatus: "WatchDied",
portalGeneration: pending.Portal.RevealGeneration,
teleportSequence: pending.Portal.TeleportSequence,
destinationCell: pending.Portal.Projection.DestinationCell,
resolvedCell: pending.Record.FullCellId,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return;
}
// The funnel owns what happens next, including this packet's
// still-unsent position event (retail acks whether or not the
// placement took — see SettlePending).
SettlePending( SettlePending(
pending.Record, pending.Record,
pending.Token, pending.Token,
@ -493,14 +1076,54 @@ public sealed class RuntimeAcceptedPositionDriveController
if (setPosition.IsPlacementCurrent(pending.Token)) if (setPosition.IsPlacementCurrent(pending.Token))
{ {
_ = SubmitAndResolve(pending.Record, pending.Token, pending.Route); if (pending.Portal.Present
&& !IsPortalAuthorityCurrent(pending.Portal))
{
// A2/D-T2.4: unlike the AwaitingCommitWake branch above,
// this retry has NOT submitted yet — re-validating here
// genuinely prevents a stale commit rather than only
// suppressing its suffix.
_pending = null;
CancelToken(setPosition, pending.Token);
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "portal",
placementStatus: "AbandonedAtWake",
portalGeneration: pending.Portal.RevealGeneration,
teleportSequence: pending.Portal.TeleportSequence,
destinationCell: pending.Portal.Projection.DestinationCell,
resolvedCell: pending.Record.FullCellId,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return;
}
_ = pending.Portal.Present
? SubmitAndResolvePortal(
pending.Record, pending.Token, pending.Route, pending.Portal)
: SubmitAndResolve(pending.Record, pending.Token, pending.Route);
return; return;
} }
// The prepare-retry operation died the same way. (A re-issue retry // The prepare-retry operation died the same way. B1/N1: no commit,
// marker also lands here, carrying PositionEventOwed: false — its // no _lastCommittedPortal write — see the watch-died branch above.
// packet's placement was never begun, so its ack belongs to the if (pending.Portal.Present)
// eventual re-issue's terminal outcome.) {
_pending = null;
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "portal",
placementStatus: "PrepareRetryLost",
portalGeneration: pending.Portal.RevealGeneration,
teleportSequence: pending.Portal.TeleportSequence,
destinationCell: pending.Portal.Projection.DestinationCell,
resolvedCell: pending.Record.FullCellId,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return;
}
// (A re-issue retry marker also lands here, carrying
// PositionEventOwed: false — its packet's placement was never begun,
// so its ack belongs to the eventual re-issue's terminal outcome.)
SettlePending( SettlePending(
pending.Record, pending.Record,
pending.Token, pending.Token,

View file

@ -39,9 +39,18 @@ public interface IRuntimeDirectWorldProjection
void BeginTeleport(); void BeginTeleport();
/// <summary>
/// C4 route 3 (D-T6): <paramref name="portal"/> is the SAME host token
/// <see cref="RuntimeLiveEntitySessionController.TryCompletePortal"/>
/// just registered via <c>TryRegisterHostProjection</c> — the producer's
/// generation/sequence/projection are all already in scope here, so no
/// new <c>WorldRevealCoordinator</c>-style exposure is needed on this
/// side either.
/// </summary>
RuntimeDestinationReadiness PrepareDestination( RuntimeDestinationReadiness PrepareDestination(
long revealGeneration, long revealGeneration,
RuntimeTeleportDestination destination); RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken portal);
} }
/// <summary> /// <summary>
@ -490,6 +499,43 @@ public sealed class RuntimeLiveEntitySessionController
acknowledgeProjection: null, acknowledgeProjection: null,
out _); out _);
/// <summary>
/// A1/A3 review fix (2026-08-05): the generation/destination/projection
/// of an accepted portal reveal that registered its host projection but
/// has not yet actually placed the local player. Headless is
/// message-driven, not per-frame — <see cref="TryCompletePortal"/> used
/// to run the ENTIRE completion sequence (readiness ack, materialized
/// ack, complete, LoginComplete, EndTeleport) unconditionally in one
/// synchronous call, discarding the canonical portal arm's own status
/// (architecture review A3). A <c>DeferredCell</c> park is a NORMAL
/// headless outcome — <see cref="IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition"/>'s
/// doc explains why the narrow collision window makes a park real
/// rather than a dead end — so this field lets
/// <see cref="PumpPortalCompletion"/> retry on the host's own per-tick
/// cadence (<c>HeadlessSessionHost.Tick</c>) instead of either
/// completing a materialization that never happened or throwing on
/// every ordinary "destination not resident yet" park.
/// </summary>
private (long Generation,
RuntimeTeleportDestination Destination,
RuntimeWorldHostProjectionToken Projection)? _pendingPortalCompletion;
/// <summary>
/// B4 review fix (2026-08-05): the retry count for the CURRENT
/// <see cref="_pendingPortalCompletion"/>, reset whenever a NEW portal
/// begins. Graphical's equivalent wait has a user-visible cue (AD-2's
/// centered wait state) when a park runs long; headless had neither a
/// cue, a bound, nor a log — an indefinitely stuck park (a destination
/// landblock whose collision generation never publishes) was silent and
/// undiagnosable. This does not make the retry fatal — K4's 30-session
/// endurance profile must survive a legitimately slow-publishing
/// landblock — it only makes a stuck park OBSERVABLE via periodic log
/// lines instead of running forever in silence.
/// </summary>
private int _pendingPortalCompletionRetryCount;
private const int PendingPortalCompletionLogInterval = 100;
private void TryCompletePortal() private void TryCompletePortal()
{ {
RuntimeWorldTransitState transit = _runtime.TransitOwner; RuntimeWorldTransitState transit = _runtime.TransitOwner;
@ -517,11 +563,39 @@ public sealed class RuntimeLiveEntitySessionController
projection, projection,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered); RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
_pendingPortalCompletion = (generation, destination, projection);
_pendingPortalCompletionRetryCount = 0;
TryAdvancePortalCompletion();
}
/// <summary>
/// A1/A3 review fix: the retryable second half of
/// <see cref="TryCompletePortal"/>. Attempts the canonical placement
/// (via <see cref="_worldProjection"/>, which owns the drive controller)
/// exactly once per call; if it has not committed yet, this returns
/// having mutated nothing beyond what the attempt itself did (a
/// DeferredCell park, safely retryable by construction — see
/// <see cref="HeadlessSessionWorldProjection.PrepareDestination"/>'s own
/// doc), and <see cref="PumpPortalCompletion"/> calls this again on the
/// next host tick. Once <c>IsCollisionReady</c> comes back true — which
/// only happens after a genuine <c>Committed</c> status — the full
/// readiness/materialized/complete/LoginComplete/EndTeleport sequence
/// runs exactly as before this fix, unconditionally, in one call.
/// </summary>
private void TryAdvancePortalCompletion()
{
if (_pendingPortalCompletion is not { } pending)
return;
(long generation, RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken projection) = pending;
RuntimeWorldTransitState transit = _runtime.TransitOwner;
bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u; bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u;
RuntimeDestinationReadiness readiness = RuntimeDestinationReadiness readiness =
_worldProjection?.PrepareDestination( _worldProjection?.PrepareDestination(
generation, generation,
destination) destination,
projection)
?? new RuntimeDestinationReadiness( ?? new RuntimeDestinationReadiness(
generation, generation,
destination.CellId, destination.CellId,
@ -531,6 +605,32 @@ public sealed class RuntimeLiveEntitySessionController
IsRenderNeighborhoodReady: true, IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true, AreCompositeTexturesReady: true,
IsCollisionReady: true); IsCollisionReady: true);
if (!readiness.IsCollisionReady)
{
// Still parked - PrepareDestination attempted (or is waiting on
// an outstanding DeferredCell wake) and has not committed yet.
// Nothing acknowledged, nothing completed; PumpPortalCompletion
// retries next tick.
//
// B4 review fix: periodic diagnostic so an indefinitely-stuck
// park is observable instead of silent. Not bounded to a throw -
// a slow-publishing landblock is a legitimate transient this
// host must ride out (N3's lesson: don't make a transient
// fatal).
_pendingPortalCompletionRetryCount++;
if (_pendingPortalCompletionRetryCount % PendingPortalCompletionLogInterval == 0)
{
_log(
$"headless: portal completion still parked after "
+ $"{_pendingPortalCompletionRetryCount} retries "
+ $"generation={generation} cell=0x{destination.CellId:X8}");
}
return;
}
_pendingPortalCompletion = null;
_pendingPortalCompletionRetryCount = 0;
if (!transit.AcknowledgeDestinationReadiness( if (!transit.AcknowledgeDestinationReadiness(
readiness)) readiness))
{ {
@ -581,6 +681,14 @@ public sealed class RuntimeLiveEntitySessionController
+ $"cell=0x{destination.CellId:X8}"); + $"cell=0x{destination.CellId:X8}");
} }
/// <summary>
/// A1/A3 review fix: called from <c>HeadlessSessionHost.Tick</c>
/// alongside <c>HeadlessSessionWorldProjection.PumpFirstEntry</c> —
/// retries a parked portal completion on the host's own per-tick
/// cadence. A no-op whenever nothing is pending.
/// </summary>
public void PumpPortalCompletion() => TryAdvancePortalCompletion();
private static void Acknowledge( private static void Acknowledge(
RuntimeWorldTransitState transit, RuntimeWorldTransitState transit,
RuntimeWorldHostProjectionToken projection, RuntimeWorldHostProjectionToken projection,

View file

@ -1,15 +1,21 @@
using System.Numerics; using System.Numerics;
using AcDream.App.Input; using AcDream.App.Input;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.Update; using AcDream.App.Update;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime; using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World; using AcDream.Runtime.World;
using DatReaderWriter.Enums;
namespace AcDream.App.Tests.Streaming; namespace AcDream.App.Tests.Streaming;
@ -45,7 +51,7 @@ public sealed class LocalPlayerTeleportControllerTests
y: 34f, y: 34f,
z: 5f); z: 5f);
harness.Controller.OfferDestination( harness.OfferDestination(
destination, destination,
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
Assert.Empty(harness.Streaming.Reservations); Assert.Empty(harness.Streaming.Reservations);
@ -61,7 +67,7 @@ public sealed class LocalPlayerTeleportControllerTests
Assert.True(harness.Reveal.Snapshot.IsActive); Assert.True(harness.Reveal.Snapshot.IsActive);
Assert.Equal(RuntimePortalKind.Portal, harness.Reveal.Snapshot.Kind); Assert.Equal(RuntimePortalKind.Portal, harness.Reveal.Snapshot.Kind);
harness.Controller.OfferDestination( harness.OfferDestination(
destination, destination,
teleportTimestampAdvanced: false); teleportTimestampAdvanced: false);
Assert.Single(harness.Streaming.Reservations); Assert.Single(harness.Streaming.Reservations);
@ -74,7 +80,7 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Mode.Controller = null; harness.Mode.Controller = null;
harness.Controller.OnTeleportStarted(2); harness.Controller.OnTeleportStarted(2);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 2, 7f, 8f, 9f), Position(0x20210001u, 2, 7f, 8f, 9f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
@ -103,7 +109,7 @@ public sealed class LocalPlayerTeleportControllerTests
Assert.True(harness.Controller.IsActive); Assert.True(harness.Controller.IsActive);
harness.Mode.Controller = null; harness.Mode.Controller = null;
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 14, 7f, 8f, 9f), Position(0x20210001u, 14, 7f, 8f, 9f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
@ -118,12 +124,25 @@ public sealed class LocalPlayerTeleportControllerTests
Assert.Single(harness.Streaming.Reservations); Assert.Single(harness.Streaming.Reservations);
} }
// C4 route 3 root-caused (closed): the earlier "bare-fixture InvalidData"
// diagnosis was WRONG - these never reached
// RuntimeSetPositionMoverPreparer.TryBuild at all. The real defect was in
// LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement: it
// re-read _transit.TryGetAcceptedTeleportDestination at Place time, but
// RuntimeWorldTransitState.TryBeginPortalReveal (driven by AimDestination
// via WorldRevealCoordinator.TryBeginPortal) atomically CONSUMES that one
// accepted-destination slot the instant Aim claims the reveal generation
// - so the Place-time re-read always found it empty and refused with
// "cause=host-token-unavailable" before ever building a mover. Fixed by
// caching the destination at Aim time (_pendingDestination), mirroring
// the existing _pendingCell/_pendingRotation/_pendingRevealGeneration
// pattern in the same method. Production fix, not a fixture workaround.
[Fact] [Fact]
public void ControllerWithdrawnAfterAim_HoldsPresentationUntilModeRebuildsIt() public void ControllerWithdrawnAfterAim_HoldsPresentationUntilModeRebuildsIt()
{ {
var harness = new Harness(); var harness = new Harness();
harness.Controller.OnTeleportStarted(15); harness.Controller.OnTeleportStarted(15);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 15, 7f, 8f, 9f), Position(0x20210001u, 15, 7f, 8f, 9f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Mode.Controller = null; harness.Mode.Controller = null;
@ -131,7 +150,7 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.Tick(0.016f); harness.Controller.Tick(0.016f);
Assert.Equal(default, harness.Placement.Position); Assert.False(harness.Placement.Called);
Assert.Empty(harness.Presentation.WorldReadyValues); Assert.Empty(harness.Presentation.WorldReadyValues);
harness.Mode.RebuildOnEnter = () => harness.Mode.RebuildOnEnter = () =>
@ -143,15 +162,28 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.Tick(0.016f); harness.Controller.Tick(0.016f);
Assert.NotNull(harness.Mode.Controller); Assert.NotNull(harness.Mode.Controller);
Assert.Equal(new Vector3(7f, 8f, 9f), harness.Placement.Position); Assert.True(harness.Placement.Called);
// A6 review fix: assert the REAL canonical body resolved the
// offered destination (7,8,9), not just that the acknowledge-only
// suffix ran.
Assert.Equal(
new Vector3(7f, 8f, 9f),
harness.Movement.Controller!.Position);
Assert.Equal(0x20210001u, harness.Movement.Controller.CellId);
} }
[Fact] [Fact]
public void SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition() public void SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition()
{ {
var harness = new Harness(centerX: 0x20, centerY: 0x21); var harness = new Harness(centerX: 0x20, centerY: 0x21);
// 0x20210123 is an INDOOR cell (low word 0x0123 >= 0x0100) within the
// same landblock the constructor already committed. It still needs
// its own synthetic CellStruct - PhysicsEngine.IsSpawnCellReady
// requires DataCache.GetCellStruct to resolve for indoor cells, and
// a bare AddLandblock never populates one.
harness.AddSyntheticIndoorCell(0x20210123u);
harness.Controller.OnTeleportStarted(3); harness.Controller.OnTeleportStarted(3);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210123u, 3, 20f, 30f, 4f), Position(0x20210123u, 3, 20f, 30f, 4f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true; harness.Presentation.EmitPlaceWhenReady = true;
@ -159,21 +191,175 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.Tick(0.016f); harness.Controller.Tick(0.016f);
Assert.Empty(harness.Streaming.Recenters); Assert.Empty(harness.Streaming.Recenters);
Assert.Equal(new Vector3(20f, 30f, 4f), harness.Placement.Position); // C4 route 3: the canonical Runtime arm - not this fake - resolves
Assert.Equal(0x20210123u, harness.Placement.CellId); // and commits the destination; the acknowledge-only suffix carries
// no position/cell to capture (D-T4). The observable fact left at
// this layer is that the committed receipt reached the suffix.
Assert.True(harness.Placement.Called);
// A6/R4 review fix: assert the REAL canonical body resolved the
// offered destination, not just that the suffix ran.
Assert.Equal(
new Vector3(20f, 30f, 4f),
harness.Movement.Controller!.Position);
Assert.Equal(0x20210123u, harness.Movement.Controller.CellId);
}
/// <summary>
/// A1/R1 review fix (2026-08-05): the retail/architecture review's own
/// required test — "refused arm -&gt; the anim stream does not reach
/// RevealWorldViewport/FireLoginComplete with an unplaced body." Forces
/// a genuine Runtime-level Contention refusal (a competing operation
/// already owns the entity's placement token, same shape as the Runtime
/// layer's <c>PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation</c>)
/// and drives several ticks past where the OLD unconditional-march code
/// would have already fired LoginComplete. The player must still be
/// standing at the origin, in portal space, with the transit still
/// active — never released into the world unplaced.
/// </summary>
[Fact]
public void RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(50);
harness.OfferDestination(
Position(0x20210001u, 50, 11f, 12f, 13f),
teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true;
Vector3 positionBefore = harness.Movement.Controller!.Position;
// A competing operation already owns the entity's placement token -
// the canonical Runtime Begin call inside TryAdvancePortalCommit
// refuses with Contention, exactly like the Runtime-layer
// PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation test.
RuntimeEntityRecord record = harness.Lifetime.Entities
.TryGetActive(0x50000001u, out RuntimeEntityRecord active)
? active
: throw new InvalidOperationException("fixture entity missing");
RuntimeEntityPlacementToken displaced = harness.Lifetime.Physics
.SetPosition.TryBeginExclusiveAuthoredPlacement(
record,
record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative);
Assert.True(displaced.IsValid);
// Several ticks, well past where the pre-fix code would already
// have reached PlayExitSound -> WorldFadeIn -> FireLoginComplete
// and reset the transit (the tunnel's own retail timing is 2-5s;
// this drives 3x that in 0.1s steps).
for (int i = 0; i < 100; i++)
harness.Controller.Tick(0.1f);
Assert.False(harness.Placement.Called);
Assert.Equal(positionBefore, harness.Movement.Controller.Position);
Assert.True(harness.Controller.IsActive);
Assert.False(harness.Reveal.Snapshot.Completed);
Assert.Equal(0, harness.Session.LoginCompleteCount);
// Release the competing operation - the SAME `dataReady` predicate
// that already held the stream in Tunnel now lets the commit
// succeed on the very next tick (D-T5 row 2's "next Tick
// re-attempts the Place edge", made real without ever touching
// TeleportAnimSequencer).
RuntimePlacementCancellationReceipt cancellation = harness.Lifetime
.Physics.SetPosition.ForgetExactPlacement(displaced);
if (cancellation.IsValid)
harness.Lifetime.Physics.SetPosition.PublishCancellation(cancellation);
harness.Controller.Tick(0.1f);
Assert.True(harness.Placement.Called);
Assert.Equal(
new Vector3(11f, 12f, 13f),
harness.Movement.Controller.Position);
}
/// <summary>
/// B1 review fix (2026-08-05): the required test #1 — a DeferredCell
/// park killed by an ORDINARY merge-time <c>Forget</c> (not a
/// collision-generation commit) must NOT be latched as "committed".
/// The destination landblock is never committed
/// (<c>CommitLandblockCollision</c> is deliberately not called), so the
/// portal parks; an unrelated ordinary accepted Position for the same
/// entity then Forgets the outstanding operation exactly like a normal
/// 5-10 Hz ACE broadcast would — <c>RuntimeSetPositionState</c>'s own
/// doc names this the EXPECTED way a far-destination park resolves
/// without committing. Before the fix, <c>PendingCount</c> hitting 0
/// made the host gate latch <c>_placementCommitted = true</c>, release
/// the sequencer, and run the ENTIRE completion sequence
/// (materialize/reveal/LoginComplete) against the unmoved body.
/// </summary>
[Fact]
public void ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted()
{
var harness = new Harness();
const uint destinationLandblock = 0x40410000u;
harness.Controller.OnTeleportStarted(60);
harness.OfferDestination(
Position(destinationLandblock | 0x0001u, 60, 21f, 22f, 23f),
teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true;
// First tick: destinationLandblock's collision generation was never
// committed, so the canonical arm parks DeferredCell.
harness.Controller.Tick(0.016f);
Assert.False(harness.Placement.Called);
Assert.Equal(1, harness.AcceptedPositionDrive.PendingCount);
// An ordinary, UNRELATED accepted Position for the same entity -
// no new teleport, just a normal broadcast - Forgets the parked
// operation the same way ACE's 5-10 Hz cadence would.
harness.MergeOrdinaryPosition(
new Vector3(48f, 49f, 50f), 0x20210001u, teleportSequence: 60);
// Forget (inside TryApplyPosition) cancels the underlying
// RuntimeSetPositionState operation immediately, but the drive's
// OWN _pending cache only notices on its next Advance() pump - the
// App host's real per-frame retry lease does this; the test drives
// it explicitly.
harness.AcceptedPositionDrive.Advance();
Assert.Equal(0, harness.AcceptedPositionDrive.PendingCount);
// Drive well past where the pre-fix inference would have latched
// "committed" on the very next tick and then marched to
// LoginComplete.
for (int i = 0; i < 100; i++)
harness.Controller.Tick(0.1f);
// NOTE: the body's Position/CellId are NOT asserted unchanged here.
// The DeferredCell park's dormant stage (RuntimeSetPositionState's
// SubmitPreparedPlacementCore deferred-commit path,
// StageDormantCellFrame) already writes the staged destination onto
// the body's controller while body.InWorld stays false and the
// canonical placement has not committed - the reader-visible
// Position/CellId moving early is that benign staging mechanism, not
// evidence the placement committed (see the analogous headless test
// HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake
// for the same finding). The discriminators below - the presentation
// suffix never running, no materialize/reveal/LoginComplete, and the
// controller staying active in an incomplete transit - are what
// actually distinguish "committed" from "parked-then-forgotten".
Assert.False(harness.Placement.Called);
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
Assert.Equal(0, harness.Session.LoginCompleteCount);
Assert.True(harness.Controller.IsActive);
Assert.False(harness.Reveal.Snapshot.Completed);
} }
[Fact] [Fact]
public void CrossLandblockDestination_RecentersBeforeItCanBecomeReady() public void CrossLandblockDestination_RecentersBeforeItCanBecomeReady()
{ {
var harness = new Harness(centerX: 0x20, centerY: 0x21); var harness = new Harness(centerX: 0x20, centerY: 0x21);
harness.CommitLandblockCollision(0x30310000u);
// 0x30310100 is an INDOOR cell (low word 0x0100 >= 0x0100); see the
// AddSyntheticIndoorCell doc comment for why CommitLandblockCollision
// alone (terrain-only) cannot make it spawn-ready.
harness.AddSyntheticIndoorCell(0x30310100u);
harness.Mode.Controller!.SetPosition( harness.Mode.Controller!.SetPosition(
Vector3.Zero, Vector3.Zero,
0x20210001u, 0x20210001u,
Vector3.Zero); Vector3.Zero);
harness.Controller.OnTeleportStarted(4); harness.Controller.OnTeleportStarted(4);
harness.Streaming.RecenterPending = true; harness.Streaming.RecenterPending = true;
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x30310100u, 4, -2f, 8f, 9f), Position(0x30310100u, 4, -2f, 8f, 9f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true; harness.Presentation.EmitPlaceWhenReady = true;
@ -182,11 +368,17 @@ public sealed class LocalPlayerTeleportControllerTests
Assert.Equal((0x30, 0x31, true), Assert.Single(harness.Streaming.Recenters)); Assert.Equal((0x30, 0x31, true), Assert.Single(harness.Streaming.Recenters));
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues)); Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
Assert.Equal(default, harness.Placement.Position); Assert.False(harness.Placement.Called);
harness.Streaming.RecenterPending = false; harness.Streaming.RecenterPending = false;
harness.Controller.Tick(0.016f); harness.Controller.Tick(0.016f);
Assert.Equal(new Vector3(-2f, 8f, 9f), harness.Placement.Position); Assert.True(harness.Placement.Called);
// A6/R4 review fix: assert the REAL canonical body resolved the
// cross-landblock destination, not just that the suffix ran.
Assert.Equal(
new Vector3(-2f, 8f, 9f),
harness.Movement.Controller!.Position);
Assert.Equal(0x30310100u, harness.Movement.Controller.CellId);
} }
[Fact] [Fact]
@ -194,7 +386,7 @@ public sealed class LocalPlayerTeleportControllerTests
{ {
var harness = new Harness(worldReady: false); var harness = new Harness(worldReady: false);
harness.Controller.OnTeleportStarted(5); harness.Controller.OnTeleportStarted(5);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 5, 1f, 2f, 3f), Position(0x20210001u, 5, 1f, 2f, 3f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true; harness.Presentation.EmitPlaceWhenReady = true;
@ -202,14 +394,14 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.Tick(4.9f); harness.Controller.Tick(4.9f);
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues)); Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
Assert.False(harness.Presentation.WaitCueValues[^1]); Assert.False(harness.Presentation.WaitCueValues[^1]);
Assert.Equal(default, harness.Placement.Position); Assert.False(harness.Placement.Called);
harness.Controller.Tick(0.1f); harness.Controller.Tick(0.1f);
harness.Controller.Tick(30f); harness.Controller.Tick(30f);
Assert.All(harness.Presentation.WorldReadyValues, Assert.False); Assert.All(harness.Presentation.WorldReadyValues, Assert.False);
Assert.True(harness.Presentation.WaitCueValues[^1]); Assert.True(harness.Presentation.WaitCueValues[^1]);
Assert.Equal(default, harness.Placement.Position); Assert.False(harness.Placement.Called);
Assert.True(harness.Reveal.WaitCueShown); Assert.True(harness.Reveal.WaitCueShown);
} }
@ -219,7 +411,7 @@ public sealed class LocalPlayerTeleportControllerTests
var order = new List<string>(); var order = new List<string>();
var harness = new Harness(order: order); var harness = new Harness(order: order);
harness.Controller.OnTeleportStarted(8); harness.Controller.OnTeleportStarted(8);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 8, 4f, 5f, 6f), Position(0x20210001u, 8, 4f, 5f, 6f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
order.Clear(); order.Clear();
@ -236,7 +428,7 @@ public sealed class LocalPlayerTeleportControllerTests
{ {
var harness = new Harness(); var harness = new Harness();
harness.Controller.OnTeleportStarted(81); harness.Controller.OnTeleportStarted(81);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 81, 4f, 5f, 6f), Position(0x20210001u, 81, 4f, 5f, 6f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
long generation = harness.Reveal.Snapshot.Generation; long generation = harness.Reveal.Snapshot.Generation;
@ -245,7 +437,7 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.Tick(0.016f); harness.Controller.Tick(0.016f);
Assert.Equal(default, harness.Placement.Position); Assert.False(harness.Placement.Called);
Assert.Equal(0, harness.Reveal.PortalMaterializationCount); Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
} }
@ -255,7 +447,7 @@ public sealed class LocalPlayerTeleportControllerTests
var order = new List<string>(); var order = new List<string>();
var harness = new Harness(order: order); var harness = new Harness(order: order);
harness.Controller.OnTeleportStarted(9); harness.Controller.OnTeleportStarted(9);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 9, 4f, 5f, 6f), Position(0x20210001u, 9, 4f, 5f, 6f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Presentation.Enqueue(TeleportAnimEvent.Place);
@ -281,7 +473,7 @@ public sealed class LocalPlayerTeleportControllerTests
var order = new List<string>(); var order = new List<string>();
var harness = new Harness(order: order); var harness = new Harness(order: order);
harness.Controller.OnTeleportStarted(91); harness.Controller.OnTeleportStarted(91);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 91, 4f, 5f, 6f), Position(0x20210001u, 91, 4f, 5f, 6f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Streaming.ReservationEnds.Clear(); harness.Streaming.ReservationEnds.Clear();
@ -300,22 +492,35 @@ public sealed class LocalPlayerTeleportControllerTests
{ {
var harness = new Harness(); var harness = new Harness();
harness.Controller.OnTeleportStarted(10); harness.Controller.OnTeleportStarted(10);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 10, 1f, 1f, 1f), Position(0x20210001u, 10, 1f, 1f, 1f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Controller.OnTeleportStarted(11); harness.Controller.OnTeleportStarted(11);
harness.Controller.Tick(30f); harness.Controller.Tick(30f);
Assert.Equal(default, harness.Placement.Position); Assert.False(harness.Placement.Called);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell); Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 11, 2f, 2f, 2f), Position(0x20210001u, 11, 2f, 2f, 2f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f); harness.Controller.Tick(0.016f);
Assert.Equal(new Vector3(2f), harness.Placement.Position); Assert.True(harness.Placement.Called);
// A6 review fix: this is the specific test A6 named — the removed
// assertion was the ONLY thing proving the SECOND destination
// (2,2,2) placed and not the superseded first one (1,1,1).
// Assert.True(Placement.Called) alone cannot distinguish them. X/Y
// are exact; Z settles to the Harness's uniform terrain height (5)
// because both candidate wire Z values (1 and 2) sit BELOW the
// floor and the placement resolve's anti-penetration push-out is
// instant (unlike gravity settling from ABOVE the floor, which
// takes further physics ticks this test never runs) - X/Y alone
// already discriminate (1,1) from (2,2) unambiguously.
Assert.Equal(2f, harness.Movement.Controller!.Position.X);
Assert.Equal(2f, harness.Movement.Controller.Position.Y);
Assert.Equal(0x20210001u, harness.Movement.Controller.CellId);
} }
[Fact] [Fact]
@ -323,7 +528,7 @@ public sealed class LocalPlayerTeleportControllerTests
{ {
var harness = new Harness(); var harness = new Harness();
harness.Controller.OnTeleportStarted(12); harness.Controller.OnTeleportStarted(12);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 12, 1f, 2f, 3f), Position(0x20210001u, 12, 1f, 2f, 3f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
@ -359,7 +564,7 @@ public sealed class LocalPlayerTeleportControllerTests
{ {
var harness = new Harness(); var harness = new Harness();
harness.Controller.OnTeleportStarted(20); harness.Controller.OnTeleportStarted(20);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 20, 1f, 2f, 3f), Position(0x20210001u, 20, 1f, 2f, 3f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Streaming.ReservationEnds.Clear(); harness.Streaming.ReservationEnds.Clear();
@ -379,7 +584,7 @@ public sealed class LocalPlayerTeleportControllerTests
{ {
var harness = new Harness(); var harness = new Harness();
harness.Controller.OnTeleportStarted(30); harness.Controller.OnTeleportStarted(30);
harness.Controller.OfferDestination( harness.OfferDestination(
Position(0x20210001u, 30, 1f, 2f, 3f), Position(0x20210001u, 30, 1f, 2f, 3f),
teleportTimestampAdvanced: true); teleportTimestampAdvanced: true);
harness.Session.OnSend = () => harness.Controller.OnTeleportStarted(31); harness.Session.OnSend = () => harness.Controller.OnTeleportStarted(31);
@ -435,14 +640,22 @@ public sealed class LocalPlayerTeleportControllerTests
{ {
Controller = new PlayerMovementController(new PhysicsEngine()), Controller = new PlayerMovementController(new PhysicsEngine()),
}; };
controllerSlot.Controller.SetPosition(Vector3.Zero, cell, Vector3.Zero); var position = new Vector3(12f, 24f, 6f);
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
// C4 route 3: the canonical Runtime commit (RuntimeSetPositionState
// .CommitCanonical, driven by RuntimeAcceptedPositionDriveController
// .ReconcileAndAcknowledgePortal -> PlayerMovementController
// .CommitCanonicalTeleportFrame in production) already resolves the
// body/cell/orientation BEFORE this suffix runs. Reproduce that
// pre-state directly rather than re-resolving it here - Place() no
// longer resolves anything (D-T4).
controllerSlot.Controller.SetPosition(position, cell, position);
controllerSlot.Controller.SetBodyOrientation(rotation);
var cameras = new ChaseCameraInputState var cameras = new ChaseCameraInputState
{ {
Legacy = new ChaseCamera(), Legacy = new ChaseCamera(),
Retail = new RetailChaseCamera(), Retail = new RetailChaseCamera(),
}; };
var origin = new LiveWorldOriginState();
origin.SetPlaceholder(0x20, 0x21);
var spatial = new FakeSpatialReconcile(() => new PlacementSnapshot( var spatial = new FakeSpatialReconcile(() => new PlacementSnapshot(
entity.Position, entity.Position,
entity.ParentCellId ?? 0u, entity.ParentCellId ?? 0u,
@ -451,18 +664,14 @@ public sealed class LocalPlayerTeleportControllerTests
controllerSlot.Controller.CellId, controllerSlot.Controller.CellId,
controllerSlot.Controller.BodyOrientation)); controllerSlot.Controller.BodyOrientation));
var placement = new LocalPlayerTeleportPlacement( var placement = new LocalPlayerTeleportPlacement(
new PhysicsEngine(),
runtime, runtime,
identity, identity,
controllerSlot, controllerSlot,
new LocalPlayerPhysicsHostSlot(), new LocalPlayerPhysicsHostSlot(),
cameras, cameras,
origin,
spatial); spatial);
var position = new Vector3(12f, 24f, 6f);
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
placement.Place(position, cell, rotation); placement.Place(rotation);
Assert.Equal(entity.Position, controllerSlot.Controller.Position); Assert.Equal(entity.Position, controllerSlot.Controller.Position);
Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId); Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId);
@ -522,13 +731,18 @@ public sealed class LocalPlayerTeleportControllerTests
Vector3.Zero, Vector3.Zero,
sourceCell, sourceCell,
Vector3.Zero); Vector3.Zero);
// C4 route 3: reproduce the canonical Runtime commit's pre-state
// (body/cell already resolved) - Place() no longer resolves it.
var destinationPosition = new Vector3(12f, 24f, 6f);
controllerSlot.Controller.SetPosition(
destinationPosition,
destinationCell,
destinationPosition);
var cameras = new ChaseCameraInputState var cameras = new ChaseCameraInputState
{ {
Legacy = new ChaseCamera(), Legacy = new ChaseCamera(),
Retail = new RetailChaseCamera(), Retail = new RetailChaseCamera(),
}; };
var origin = new LiveWorldOriginState();
origin.SetPlaceholder(0x30, 0x31);
LiveEntityRecord? recordAtReconcile = null; LiveEntityRecord? recordAtReconcile = null;
var spatial = new FakeSpatialReconcile(() => var spatial = new FakeSpatialReconcile(() =>
{ {
@ -542,19 +756,14 @@ public sealed class LocalPlayerTeleportControllerTests
controllerSlot.Controller.BodyOrientation); controllerSlot.Controller.BodyOrientation);
}); });
var placement = new LocalPlayerTeleportPlacement( var placement = new LocalPlayerTeleportPlacement(
new PhysicsEngine(),
runtime, runtime,
identity, identity,
controllerSlot, controllerSlot,
new LocalPlayerPhysicsHostSlot(), new LocalPlayerPhysicsHostSlot(),
cameras, cameras,
origin,
spatial); spatial);
placement.Place( placement.Place(Quaternion.Identity);
new Vector3(12f, 24f, 6f),
destinationCell,
Quaternion.Identity);
Assert.NotNull(recordAtReconcile); Assert.NotNull(recordAtReconcile);
uint resolvedDestinationCell = controllerSlot.Controller.CellId; uint resolvedDestinationCell = controllerSlot.Controller.CellId;
@ -652,8 +861,26 @@ public sealed class LocalPlayerTeleportControllerTests
uint ControllerCell, uint ControllerCell,
Quaternion ControllerRotation); Quaternion ControllerRotation);
/// <summary>
/// C4 route 3: the Place edge now drives a REAL
/// <see cref="RuntimeAcceptedPositionDriveController"/> portal arm
/// against a REAL (bare, session-less) local-player canonical record -
/// the class this file's <c>LocalPlayerTeleportController</c> fake
/// wiring cannot substitute (it is a sealed Runtime type, not an
/// interface). <see cref="OfferDestination"/> also performs the SAME
/// upstream merge production runs before ever offering a destination
/// (<c>RuntimeEntityObjectLifetime.TryApplyPosition</c>, called by
/// <c>LiveEntityNetworkUpdateController.OnPosition</c> before
/// <c>OfferDestination</c>) so <c>BeginAcceptedPlacementCore</c>'s
/// portal-vs-latest-cell gate sees the SAME destination the offer
/// carries - exactly the production ordering, not a shortcut.
/// </summary>
private sealed class Harness private sealed class Harness
{ {
private const uint PlayerGuid = 0x50000001u;
private const uint HomeCell = 0x20210001u;
private ushort _mergePositionSequence = 1;
public readonly FakeAuthority Authority = new(); public readonly FakeAuthority Authority = new();
public readonly FakeInput Input = new(); public readonly FakeInput Input = new();
public readonly FakeMode Mode; public readonly FakeMode Mode;
@ -664,6 +891,17 @@ public sealed class LocalPlayerTeleportControllerTests
public readonly RuntimeWorldTransitState Transit; public readonly RuntimeWorldTransitState Transit;
public readonly WorldRevealCoordinator Reveal; public readonly WorldRevealCoordinator Reveal;
public readonly LocalPlayerTeleportController Controller; public readonly LocalPlayerTeleportController Controller;
public readonly RuntimeEntityObjectLifetime Lifetime;
public readonly RuntimeAcceptedPositionDriveController AcceptedPositionDrive;
/// <summary>
/// A6/R4/A5 review fix (2026-08-05): exposed so tests can assert the
/// REAL canonical body's resolved position/cell after a committed
/// portal placement, not just <c>Placement.Called</c> — the property
/// that let A6's superseded-teleport test lose its destination
/// discriminator (which of two destinations actually placed).
/// </summary>
public readonly RuntimeLocalPlayerMovementState Movement;
public IPreparedCollisionSource DiagnosticCollisionSource => new UnusedCollisionSource();
public Harness( public Harness(
int centerX = 0x20, int centerX = 0x20,
@ -688,6 +926,99 @@ public sealed class LocalPlayerTeleportControllerTests
invalidateCompositeTextures: () => { }, invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false, isSpawnClaimUnhydratable: _ => false,
streaming: Streaming); streaming: Streaming);
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
var heights = new byte[81];
Array.Fill(heights, (byte)5f);
var heightTable = new float[256];
for (int i = 0; i < heightTable.Length; i++)
heightTable[i] = i;
engine.AddLandblock(
HomeCell & 0xFFFF0000u,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime = new RuntimeEntityObjectLifetime(engine);
// The bare AddLandblock above only seeds the ENGINE's terrain -
// TryPrepareAndSubmitAuthoredPlacement's DeferredCell park keys
// off the SEPARATE collision-GENERATION ledger
// (RuntimeSetPositionState.BeginCollisionGeneration/
// CommitCollisionGeneration), which a bare AddLandblock never
// touches. Commit it too, or every placement parks forever.
Lifetime.Physics.SetPosition.BeginCollisionGeneration(
HomeCell & 0xFFFF0000u, 1UL);
Lifetime.Physics.SetPosition.CommitCollisionGeneration(
HomeCell & 0xFFFF0000u, 1UL, ready: true);
Lifetime.Physics.ObserveLocalWorldFrame(HomeCell, teleportAdvanced: false);
Lifetime.BindEventContext(
static () => new RuntimeGenerationToken(1UL),
static () => 1UL);
Movement = new RuntimeLocalPlayerMovementState();
RuntimeLocalPlayerMovementState movement = Movement;
var identity = new RuntimeLocalPlayerIdentityState
{
ServerGuid = PlayerGuid,
};
movement.AttachPhysicsPublication(new RuntimeLocalPlayerPhysicsPublicationState(
Lifetime.Entities, Lifetime.Physics, movement, identity));
Lifetime.LocalPlayerFirstEntry.BindPublication(movement.PhysicsPublication);
Lifetime.BindLiveInputs(() => false, () => movement.Controller?.Position);
var clock = new GameRuntimeClock();
// C4 route 3 fixture note: RuntimeFirstEntryDriveController's
// ctor subscribes to RuntimeEntityObjectLifetime
// .BindInitialResidenceBeginNotification - it must exist BEFORE
// RegisterEntityWithInitialResidence runs, or it misses the
// notification and never learns the residence exists at all
// (production constructs it once per host session, ahead of
// every Create).
var firstEntry = new RuntimeFirstEntryDriveController(
Lifetime,
clock,
new UnusedCollisionSource(),
() => PlayerMovementConstructionOptions.Fallback,
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f, 1.835f, RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
// Production's OnSpawned shape: RegisterEntityWithInitialResidence
// then ApplyAcceptedSpawn (RuntimeLiveEntitySessionController.cs).
RuntimeEntityRegistrationResult registration =
Lifetime.RegisterEntityWithInitialResidence(
Spawn(PlayerGuid, HomeCell), isLocalPlayer: true);
RuntimeEntityRecord canonical = registration.Canonical
?? throw new InvalidOperationException(
"fixture failed to register the local player");
Lifetime.ApplyAcceptedSpawn(
canonical,
canonical.CreateIntegrationVersion,
canonical.Snapshot,
replaceGeneration: registration.Inbound.Disposition
is CreateObjectTimestampDisposition.NewGeneration);
for (int attempt = 0; attempt < 8 && firstEntry.PendingCount != 0; attempt++)
{
firstEntry.DriveAll();
DrainPlacementFifo();
}
Assert.Equal(0, firstEntry.PendingCount);
Assert.True(
Lifetime.Entities.TryGetActive(PlayerGuid, out RuntimeEntityRecord seeded),
"fixture failed to register the local player");
Assert.False(
Lifetime.TryGetInitialCreateResidence(seeded, out _),
"fixture left the local player's initial-create residence open");
AcceptedPositionDrive = new RuntimeAcceptedPositionDriveController(
Lifetime,
clock,
new UnusedCollisionSource(),
new LocalPlayerOutboundController((_, _, _, _, _, _) => { }),
() => new RuntimeGenerationToken(1UL),
() => PlayerGuid,
() => movement.Controller,
() => false,
() => null,
() => movement);
Controller = new LocalPlayerTeleportController( Controller = new LocalPlayerTeleportController(
Authority, Authority,
Input, Input,
@ -697,7 +1028,258 @@ public sealed class LocalPlayerTeleportControllerTests
Reveal, Reveal,
Placement, Placement,
Session, Session,
Presentation); Presentation,
AcceptedPositionDrive);
}
/// <summary>
/// Merges the destination into the SAME canonical record the portal
/// arm reads from, exactly like production's upstream merge, then
/// offers it to the controller.
/// </summary>
public void OfferDestination(
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
_mergePositionSequence++;
var update = new WorldSession.EntityPositionUpdate(
destination.EntityGuid,
new CreateObject.ServerPosition(
destination.Position.ObjCellId,
destination.Position.Frame.Origin.X,
destination.Position.Frame.Origin.Y,
destination.Position.Frame.Origin.Z,
destination.Position.Frame.Orientation.W,
destination.Position.Frame.Orientation.X,
destination.Position.Frame.Orientation.Y,
destination.Position.Frame.Orientation.Z),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: _mergePositionSequence,
TeleportSequence: destination.TeleportSequence,
ForcePositionSequence: 0);
Lifetime.TryApplyPosition(
update,
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
acknowledgeProjection: null,
out _,
out _,
out _);
Controller.OfferDestination(destination, teleportTimestampAdvanced);
}
/// <summary>
/// B1 review fix test support (2026-08-05): merges an ORDINARY
/// accepted Position for the local player — no portal offer, no
/// transit involvement — the same
/// <c>RuntimeEntityObjectLifetime.TryApplyPosition</c> call every
/// real inbound ACE broadcast makes. Its doc names this the
/// mechanism that <c>Forget</c>s any outstanding SetPosition
/// operation for the entity, including a DeferredCell park — "the
/// exact far-destination case the park exists to serve".
/// </summary>
public void MergeOrdinaryPosition(
Vector3 position, uint cellId, ushort teleportSequence)
{
_mergePositionSequence++;
var update = new WorldSession.EntityPositionUpdate(
PlayerGuid,
new CreateObject.ServerPosition(
cellId,
position.X,
position.Y,
position.Z,
1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: _mergePositionSequence,
// The SAME already-accepted teleport sequence, not a
// regression - an ordinary ACE broadcast Apply during an
// active teleport still carries it unchanged; only a NEW
// portal advances it.
TeleportSequence: teleportSequence,
ForcePositionSequence: 0);
Lifetime.TryApplyPosition(
update,
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
acknowledgeProjection: null,
out _,
out _,
out _);
}
/// <summary>
/// Commits a SECOND landblock's collision so a cross-landblock
/// portal destination can resolve <c>Committed</c> instead of
/// parking <c>DeferredCell</c> - the constructor's own home
/// landblock is the only one committed by default.
/// </summary>
public void CommitLandblockCollision(uint landblockId)
{
var heights = new byte[81];
Array.Fill(heights, (byte)5f);
var heightTable = new float[256];
for (int i = 0; i < heightTable.Length; i++)
heightTable[i] = i;
Lifetime.Physics.SetPosition.BeginCollisionGeneration(landblockId, 1UL);
Lifetime.Physics.Engine.AddLandblock(
landblockId,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime.Physics.SetPosition.CommitCollisionGeneration(
landblockId, 1UL, ready: true);
}
/// <summary>
/// Registers a minimal synthetic indoor cell so a portal destination
/// whose low word is &gt;= 0x0100 can resolve <c>Committed</c> on its
/// FIRST placement attempt instead of parking <c>DeferredCell</c>
/// forever. <see cref="PhysicsEngine.IsSpawnCellReady"/> treats every
/// outdoor cell (low word &lt; 0x0100) as trivially ready, but an
/// indoor cell requires <c>DataCache.GetCellStruct</c> to resolve a
/// real entry - the bare fixture's <c>AddLandblock</c> calls only
/// ever pass an empty <c>CellSurface</c> list, so without this an
/// indoor destination cell is permanently un-spawnable regardless of
/// how many times collision generation is committed for its
/// landblock. Mirrors
/// <c>RuntimeSetPositionStateTests.AddSyntheticCell</c>.
/// </summary>
public void AddSyntheticIndoorCell(uint envCellId)
{
Lifetime.Physics.DataCache.RegisterCellStructForTest(
envCellId,
new CellPhysics
{
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
Portals = [new PortalInfo(0, 0, 0)],
CellBSP = new DatReaderWriter.Types.CellBSPTree
{
Root = new DatReaderWriter.Types.CellBSPNode
{
Type = BSPNodeType.Leaf,
},
},
});
Lifetime.Physics.DataCache.CellGraph.Add(
new AcDream.Core.World.Cells.EnvCell(
envCellId,
Matrix4x4.Identity,
Matrix4x4.Identity,
Vector3.Zero,
Vector3.One,
Array.Empty<AcDream.Core.World.Cells.CellPortal>(),
Array.Empty<uint>(),
seenOutside: false,
containmentBsp: null));
}
public void DrainPlacementFifo()
{
while (Lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot head))
{
if (!Lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token))
break;
}
}
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
{
var position = new CreateObject.ServerPosition(
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: null,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
null,
[],
[],
[],
null,
null,
"teleport-controller-fixture",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private sealed class UnusedCollisionSource : IPreparedCollisionSource
{
public PreparedAssetPresence ProbeCollision(
AcDream.Content.Pak.PakAssetType type, uint sourceFileId) =>
PreparedAssetPresence.Available;
public PreparedCollisionReadResult<FlatSetupCollision> ReadSetupCollision(
uint sourceFileId, CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatSetupCollision>.Missing;
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset> ReadGfxObjCollision(
uint sourceFileId, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult<FlatEnvCellTopology> ReadEnvCellTopology(
uint sourceFileId, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionSourceStats CollisionStats => default;
public void Dispose()
{
}
} }
} }
@ -800,22 +1382,29 @@ public sealed class LocalPlayerTeleportControllerTests
} }
} }
/// <summary>
/// C4 route 3: acknowledge-only per D-T4 - no position/cell arguments
/// (the canonical Runtime commit already resolved them; this suffix only
/// runs on a COMMITTED receipt, never authors one). <see cref="Called"/>
/// stands in for the deleted <c>Position</c>/<c>CellId</c> capture: it is
/// the fact that matters now (whether the arm committed and the Place
/// edge acknowledged it), never a value this fake could plausibly
/// mis-record.
/// </summary>
private sealed class FakePlacement : ILocalPlayerTeleportPlacement private sealed class FakePlacement : ILocalPlayerTeleportPlacement
{ {
private readonly List<string> _order; private readonly List<string> _order;
public FakePlacement(List<string> order) => _order = order; public FakePlacement(List<string> order) => _order = order;
public Vector3 Position; public bool Called;
public uint CellId;
public Quaternion Rotation; public Quaternion Rotation;
public Action? OnPlace; public Action? OnPlace;
public void Place(Vector3 position, uint cellId, Quaternion rotation) public void Place(Quaternion rotation)
{ {
_order.Add("placement"); _order.Add("placement");
Position = position; Called = true;
CellId = cellId;
Rotation = rotation; Rotation = rotation;
OnPlace?.Invoke(); OnPlace?.Invoke();
} }

View file

@ -463,8 +463,22 @@ public sealed class RuntimePlacementPresentationSinkTests
Assert.True(replacement.IsSpatiallyVisible); Assert.True(replacement.IsSpatiallyVisible);
} }
/// <summary>
/// B2 review fix (2026-08-05): a Place whose portal authority is stale
/// (the transit ended or was superseded while a park sat outstanding)
/// must be ACKNOWLEDGED-AND-IGNORED, not refused. Before the fix this
/// test asserted <c>TryApply</c> returns <c>false</c> for both the
/// superseded-host and wrong-sequence cases — but
/// <c>RuntimePlacementProjectionSubscription.OnPlacement</c> only calls
/// <c>Acknowledge</c> when <c>TryApply</c> returns <c>true</c>; a
/// <c>false</c> here leaves the stale receipt at the FIFO head
/// PERMANENTLY, wedging every later entity's placement receipt behind
/// it. The entity's world position must still never move to the stale
/// receipt's coordinates — "ignore" means the receipt is retired
/// without being applied, not silently accepted as real.
/// </summary>
[Fact] [Fact]
public void PortalPlace_RequiresExactCurrentTransitHostAndSequence() public void PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored()
{ {
Fixture fixture = Fixture.Create(); Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell)); LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
@ -491,7 +505,10 @@ public sealed class RuntimePlacementPresentationSinkTests
{ {
WorldPosition = new Vector3(80f, 81f, 82f), WorldPosition = new Vector3(80f, 81f, 82f),
}; };
Assert.False(fixture.Sink.TryApply(in superseded)); // Acknowledged (true) - the stale-authority Place is retired from
// the FIFO head, not left wedged - but the position never moves to
// the stale receipt's coordinates.
Assert.True(fixture.Sink.TryApply(in superseded));
Assert.Equal(current.WorldPosition, record.WorldEntity.Position); Assert.Equal(current.WorldPosition, record.WorldEntity.Position);
RuntimePlacementProjectionSnapshot wrongSequence = current with RuntimePlacementProjectionSnapshot wrongSequence = current with
@ -502,7 +519,7 @@ public sealed class RuntimePlacementPresentationSinkTests
}, },
WorldPosition = new Vector3(90f, 91f, 92f), WorldPosition = new Vector3(90f, 91f, 92f),
}; };
Assert.False(fixture.Sink.TryApply(in wrongSequence)); Assert.True(fixture.Sink.TryApply(in wrongSequence));
Assert.Equal(current.WorldPosition, record.WorldEntity.Position); Assert.Equal(current.WorldPosition, record.WorldEntity.Position);
} }

View file

@ -383,10 +383,20 @@ public sealed class HeadlessSessionHostTests
record.Snapshot, record.Snapshot,
replaceGeneration: false)); replaceGeneration: false));
var collision = new FixtureCollisionNeighborhood(); var collision = new FixtureCollisionNeighborhood();
// R5/A7 review fix (2026-08-05): the drive controller is now wired
// (was omitted in the first pass, leaving the canonical portal arm
// a no-op by construction here and dual-host parity with zero
// coverage). Mirrors production composition
// (HeadlessSessionHost.cs's own construction order): the SAME
// RuntimeAcceptedPositionDriveController drives both hosts through
// the identical TryExecuteAcceptedPortalArrival entry point.
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
CreateAcceptedPositionDrive(runtime);
var projection = new HeadlessSessionWorldProjection( var projection = new HeadlessSessionWorldProjection(
runtime, runtime,
collision, collision,
firstEntry); firstEntry,
acceptedPositionDrive);
projection.ProjectSpawn(record, isLocalPlayer: true); projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller = PlayerMovementController controller =
@ -410,6 +420,15 @@ public sealed class HeadlessSessionHostTests
projection.BeginTeleport(); projection.BeginTeleport();
Assert.Equal(PlayerState.PortalSpace, controller.State); Assert.Equal(PlayerState.PortalSpace, controller.State);
// R5/A7: the destination cell 0xA9B40001 is in the SAME landblock
// (0xA9B40000) whose collision generation the test already
// committed above, so the canonical portal arm resolves
// Committed synchronously - no DeferredCell park needed to exercise
// the real headless placement path. A1's headless retry loop
// (RuntimeLiveEntitySessionController.PumpPortalCompletion) is
// covered separately by
// HeadlessPortalDeferredCellCommitsOnPumpAfterCollisionGenerationWake
// below.
RuntimeDestinationReadiness readiness = RuntimeDestinationReadiness readiness =
projection.PrepareDestination( projection.PrepareDestination(
revealGeneration: 7, revealGeneration: 7,
@ -422,13 +441,393 @@ public sealed class HeadlessSessionHostTests
new Position( new Position(
0xA9B40001u, 0xA9B40001u,
new Vector3(96f, 97f, 50f), new Vector3(96f, 97f, 50f),
Quaternion.Identity))); Quaternion.Identity)),
// R5/A7: a real, valid token - `default` was fine for the
// old no-op arm but RuntimePortalPlacementAuthority.IsValid
// now genuinely gates TryExecuteAcceptedPortalArrival on it.
new RuntimeWorldHostProjectionToken(7, 0xA9B40001u));
Assert.True(readiness.IsCollisionReady); Assert.True(readiness.IsCollisionReady);
Assert.False(readiness.IsUnhydratable); Assert.False(readiness.IsUnhydratable);
Assert.Equal(PlayerState.InWorld, controller.State); Assert.Equal(PlayerState.InWorld, controller.State);
Assert.Equal(3, collision.CenterCount); Assert.Equal(2, collision.CenterCount);
Assert.Equal(0xA9B40001u, collision.LastCell); Assert.Equal(0xA9B40001u, collision.LastCell);
// A4/dual-host parity: the canonical placement actually committed -
// the body moved to the destination Position, not just the
// collision-neighborhood bookkeeping that CenterCount/LastCell
// alone would have proven even with the earlier no-op arm. Z
// settles 0.005 above the wire value (the foot sphere's bottom
// sits at origin + 0.475 - 0.48, LoadedSetupCollisionSource's own
// doc comment, ISSUES.md #285) - X/Y are exact, Z is asserted
// within that settle tolerance.
Assert.Equal(96f, controller.Position.X);
Assert.Equal(97f, controller.Position.Y);
Assert.Equal(50f, controller.Position.Z, 0.01f);
// The resolved outdoor sub-cell index is derived from X/Y within
// the landblock (not the wire placeholder 0xA9B40001), same as the
// FIRST ProjectPosition assertion above (":406-409") only checks
// landblock+indoor-vs-outdoor, not the exact sub-cell.
Assert.Equal(0xA9B40000u, controller.CellId & 0xFFFF0000u);
Assert.True((controller.CellId & 0xFFFFu) < 0x0100u);
}
/// <summary>
/// A1/R5/A7 review fix (2026-08-05): headless has no per-frame anim
/// sequencer the way the graphical host does, so its OWN equivalent of
/// A1's "hold until committed" mechanism is
/// <c>HeadlessSessionWorldProjection.PrepareDestination</c>'s
/// <c>_awaitingPortalWake</c> polling. This proves it end to end: a
/// destination in a landblock whose collision generation is NOT yet
/// committed parks (<c>IsCollisionReady: false</c>, body unmoved, no
/// throw — DeferredCell is a normal headless outcome per
/// <c>PrepareDestination</c>'s own doc), and once the destination
/// landblock's collision generation commits, the SAME park resolves on
/// a later attempt WITHOUT a second concurrent Begin (Runtime's own
/// Begin would refuse that with Contention if this class re-attempted
/// blindly instead of polling <c>PendingCount</c>).
/// </summary>
[Fact]
public void HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake()
{
var operations = new FixtureSessionOperations();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
using var host = new HeadlessSessionHost(
Descriptor(),
credential,
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
GameRuntime runtime = host.Runtime;
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
const uint player = 0x50000009u;
const uint destinationLandblock = 0xAAB40000u;
runtime.PlayerIdentity.ServerGuid = player;
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
0xA9B40000u, 1UL);
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
0xA9B40000u, 1UL, ready: true);
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
CreateFirstEntryDrive(runtime);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
record.CreateIntegrationVersion,
record.Snapshot,
replaceGeneration: false));
var collision = new FixtureCollisionNeighborhood();
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
CreateAcceptedPositionDrive(runtime);
var projection = new HeadlessSessionWorldProjection(
runtime,
collision,
firstEntry,
acceptedPositionDrive);
projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
runtime.MovementOwner.Controller);
controller.SetPosition(
new Vector3(48f, 49f, 50f),
0xA9B40001u);
projection.ProjectPosition(
record,
isLocalPlayer: true,
PositionTimestampDisposition.Apply);
projection.BeginTeleport();
var destination = new RuntimeTeleportDestination(
player,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 1,
ForcePositionSequence: 0,
new Position(
destinationLandblock | 0x0001u,
new Vector3(10f, 10f, 50f),
Quaternion.Identity));
var projectionToken = new RuntimeWorldHostProjectionToken(
7, destinationLandblock | 0x0001u);
// Begin's own portal-vs-latest-cell gate (D-T5) requires the
// destination's landblock to match the record's LATEST MERGED
// Position, not just the transit's retained destination - mirror
// what the real inbound Position handler already does before
// TryCompletePortal ever runs (LiveEntityNetworkUpdateController's
// App-side equivalent).
Assert.True(runtime.EntityObjects.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
player,
new CreateObject.ServerPosition(
destination.Position.ObjCellId,
destination.Position.Frame.Origin.X,
destination.Position.Frame.Origin.Y,
destination.Position.Frame.Origin.Z,
destination.Position.Frame.Orientation.W,
destination.Position.Frame.Orientation.X,
destination.Position.Frame.Orientation.Y,
destination.Position.Frame.Orientation.Z),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 3,
TeleportSequence: destination.TeleportSequence,
ForcePositionSequence: 0),
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
acknowledgeProjection: null,
out _,
out _,
out _));
// First attempt: destinationLandblock's collision generation was
// never begun/committed, so the canonical arm parks DeferredCell.
// Must NOT throw (a park is normal, not an error). The dormant
// stage (RuntimeSetPositionState's SubmitPreparedPlacementCore
// deferred-commit path) already stages the body's Position/CellId
// at the destination while it waits (StageDormantCellFrame,
// body.InWorld=false) - the reader-visible Position moving early is
// that mechanism, not evidence the placement committed; only
// PlayerState/IsCollisionReady distinguish "staged" from
// "committed" here.
RuntimeDestinationReadiness parked = projection.PrepareDestination(
revealGeneration: 7, destination, projectionToken);
Assert.False(parked.IsCollisionReady);
Assert.Equal(PlayerState.PortalSpace, controller.State);
// A SECOND attempt while still parked must not double-Begin -
// Runtime's own Begin would refuse a genuine second attempt with
// Contention, but PrepareDestination's _awaitingPortalWake polls
// PendingCount instead of re-attempting, so this must also report
// not-ready without throwing.
RuntimeDestinationReadiness stillParked =
projection.PrepareDestination(
revealGeneration: 7, destination, projectionToken);
Assert.False(stillParked.IsCollisionReady);
// Commit the destination landblock's collision generation and pump
// the drive's wake (mirrors HeadlessSessionHost.Tick's own
// PumpFirstEntry -> _acceptedPositionDrive.Advance() ordering).
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
destinationLandblock, 1UL);
var heights = new byte[81];
Array.Fill(heights, (byte)50);
var heightTable = new float[256];
for (int index = 0; index < heightTable.Length; index++)
heightTable[index] = index;
runtime.EntityObjects.Physics.Engine.AddLandblock(
destinationLandblock,
new TerrainSurface(heights, heightTable),
[],
[],
worldOffsetX: 0f,
worldOffsetY: 0f);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
destinationLandblock, 1UL, ready: true);
// RuntimeAcceptedPositionDriveControllerTests.CommitLandblockCollision's
// exact proven-working shape: the wake path's
// resolveWorldOffsetFromRuntimeFrame requires the destination
// landblock's world-frame offset to already be resolvable.
runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
destinationLandblock | 0x0001u,
teleportAdvanced: false);
// Drain the placement projection FIFO AFTER committing (exact order
// from RuntimeAcceptedPositionDriveControllerTests.DrainPlacementFifo's
// call site: commit collision -> drain FIFO -> Advance) - the
// deferred park's Withdraw notification is published as part of the
// collision-generation commit, not before it.
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot head))
{
if (!runtime.EntityObjects.Physics.SetPosition
.AcknowledgeProjection(head.Token))
{
break;
}
}
acceptedPositionDrive.Advance();
RuntimeDestinationReadiness committed =
projection.PrepareDestination(
revealGeneration: 7, destination, projectionToken);
Assert.True(committed.IsCollisionReady);
Assert.Equal(PlayerState.InWorld, controller.State);
Assert.Equal(10f, controller.Position.X);
Assert.Equal(10f, controller.Position.Y);
Assert.Equal(destinationLandblock, controller.CellId & 0xFFFF0000u);
}
/// <summary>
/// B1 review fix (2026-08-05): headless's required test #2 — the same
/// unsound-commit-inference defect
/// <see cref="HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake"/>
/// proves the HAPPY path for, exercised on the FORGOTTEN path instead. A
/// DeferredCell park killed by an ordinary, unrelated accepted Position
/// merge (exactly the ACE 5-10 Hz broadcast <c>RuntimeSetPositionState</c>'s
/// own doc names as the expected way a far-destination park resolves
/// without committing) must leave <c>PrepareDestination</c> reporting
/// NOT ready, <see cref="PlayerState.PortalSpace"/> unchanged, and the
/// body never moved to the destination - before the fix,
/// <c>PendingCount</c> hitting 0 made <c>PrepareDestination</c> infer
/// "committed" and run the full readiness/materialize/LoginComplete
/// sequence against an unmoved body.
/// </summary>
[Fact]
public void HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted()
{
var operations = new FixtureSessionOperations();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
using var host = new HeadlessSessionHost(
Descriptor(),
credential,
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
GameRuntime runtime = host.Runtime;
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
const uint player = 0x50000009u;
const uint destinationLandblock = 0xAAB40000u;
runtime.PlayerIdentity.ServerGuid = player;
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
0xA9B40000u, 1UL);
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
0xA9B40000u, 1UL, ready: true);
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
CreateFirstEntryDrive(runtime);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
record.CreateIntegrationVersion,
record.Snapshot,
replaceGeneration: false));
var collision = new FixtureCollisionNeighborhood();
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
CreateAcceptedPositionDrive(runtime);
var projection = new HeadlessSessionWorldProjection(
runtime,
collision,
firstEntry,
acceptedPositionDrive);
projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
runtime.MovementOwner.Controller);
controller.SetPosition(
new Vector3(48f, 49f, 50f),
0xA9B40001u);
projection.ProjectPosition(
record,
isLocalPlayer: true,
PositionTimestampDisposition.Apply);
projection.BeginTeleport();
var destination = new RuntimeTeleportDestination(
player,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 1,
ForcePositionSequence: 0,
new Position(
destinationLandblock | 0x0001u,
new Vector3(10f, 10f, 50f),
Quaternion.Identity));
var projectionToken = new RuntimeWorldHostProjectionToken(
7, destinationLandblock | 0x0001u);
Assert.True(runtime.EntityObjects.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
player,
new CreateObject.ServerPosition(
destination.Position.ObjCellId,
destination.Position.Frame.Origin.X,
destination.Position.Frame.Origin.Y,
destination.Position.Frame.Origin.Z,
destination.Position.Frame.Orientation.W,
destination.Position.Frame.Orientation.X,
destination.Position.Frame.Orientation.Y,
destination.Position.Frame.Orientation.Z),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 3,
TeleportSequence: destination.TeleportSequence,
ForcePositionSequence: 0),
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
acknowledgeProjection: null,
out _,
out _,
out _));
// First attempt: destinationLandblock's collision generation was
// never begun/committed, so the canonical arm parks DeferredCell.
RuntimeDestinationReadiness parked = projection.PrepareDestination(
revealGeneration: 7, destination, projectionToken);
Assert.False(parked.IsCollisionReady);
Assert.Equal(PlayerState.PortalSpace, controller.State);
Assert.Equal(1, acceptedPositionDrive.PendingCount);
// An ordinary, UNRELATED accepted Position for the same entity - no
// new teleport, just a normal broadcast at the SAME already-accepted
// teleport sequence - Forgets the parked operation the same way
// ACE's 5-10 Hz cadence would (RuntimeSetPositionState.Forget, called
// from TryApplyPosition for every accepted, non-Rejected Position).
Assert.True(runtime.EntityObjects.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
player,
new CreateObject.ServerPosition(
0x20210001u, 48f, 49f, 50f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 4,
TeleportSequence: destination.TeleportSequence,
ForcePositionSequence: 0),
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
acknowledgeProjection: null,
out _,
out _,
out _));
// Forget (inside TryApplyPosition) cancels the underlying
// RuntimeSetPositionState operation immediately, but the drive's OWN
// _pending cache only notices on its next Advance() pump - the real
// host does this every HeadlessSessionHost.Tick via PumpFirstEntry;
// the test drives it explicitly, same as the App-level equivalent.
acceptedPositionDrive.Advance();
Assert.Equal(0, acceptedPositionDrive.PendingCount);
// Drive well past where the pre-fix inference would have latched
// "committed" on the very next PrepareDestination call and then
// marched to the full readiness/materialize/LoginComplete sequence.
for (int i = 0; i < 10; i++)
{
RuntimeDestinationReadiness stillNotReady =
projection.PrepareDestination(
revealGeneration: 7, destination, projectionToken);
Assert.False(stillNotReady.IsCollisionReady);
}
Assert.Equal(PlayerState.PortalSpace, controller.State);
} }
[Fact] [Fact]

View file

@ -1207,6 +1207,580 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
AssertConverged(runtime); AssertConverged(runtime);
} }
#region C4 route 3 - portal arm
/// <summary>
/// The main D-T2/D-T3 happy path, asserting the positive facts §8 item 2
/// requires rather than only <c>InWorld</c>/clock: the body moved to the
/// resolved destination, velocity zeroed (retail
/// <c>set_velocity(player, 0, 1)</c> @0x004541B4), the leash re-armed
/// EXACTLY ONCE at the resolved position (Inversion A — proven by
/// pre-arming at a stale anchor first, so a stale anchor surviving would
/// fail the assertion), autorun cancelled (the <c>PlayerTeleported</c>
/// port), and exactly one outbound movement event with ZERO
/// AutonomousPosition packets (route's <c>SendPositionImmediately</c> is
/// always false).
/// </summary>
/// <summary>
/// R7 review fix (2026-08-05): retail <c>CommandInterpreter::SendMovementEvent</c>
/// @0x006B4680 (<c>PlayerTeleported</c>'s tail-jump) gates on
/// <c>autonomy_level != 0</c> — under server control (this class's
/// <c>_usePositionFromServer</c>, retail's <c>UsePositionFromServer()</c>)
/// retail sends nothing. Everything else about the commit (body move,
/// leash re-arm, autorun cancel) is unaffected by autonomy; only the
/// wire send is gated.
/// </summary>
[Fact]
public void PortalCommitted_UnderServerControlSendsNoMovementEvent()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
(RuntimeEntityRecord record, PlayerMovementController controller) =
EnterLocalPlayer(runtime);
Assert.True(runtime.CharacterOwner.TrySetAutonomyLevel(0u));
Assert.True(runtime.CharacterOwner.UsePositionFromServer);
const ushort teleportSequence = 6;
var destinationPosition = new Vector3(31f, 33f, SpawnHeight);
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
destinationPosition, SpawnLandblock | 0x0001u, teleportSequence);
MergeAccepted(runtime, controller, destinationUpdate);
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
// assertion below fails first - see PortalHostConvergenceGuard's doc.
using var portalHostGuard = new PortalHostConvergenceGuard(
runtime, portal.RevealGeneration, portal.Projection);
RuntimeAcceptedPositionDriveController drive =
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
RuntimeAcceptedPositionExecutionStatus status =
drive.TryExecuteAcceptedPortalArrival(destination, portal);
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Committed, status);
Assert.Equal(destinationPosition, controller.Position);
Assert.False(runtime.MovementOwner.AutoRunActive);
Assert.Empty(gameActions);
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
AssertConverged(runtime);
}
[Fact]
public void PortalCommitted_MovesBodyArmsLeashOnceCancelsAutorunAndSendsExactlyOneMovementEvent()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
(RuntimeEntityRecord record, PlayerMovementController controller) =
EnterLocalPlayer(runtime);
// Pre-arm the leash at a STALE anchor - a re-arm that merely leaves
// the OLD anchor in place (i.e. no re-arm at all) would fail the
// ConstraintPos assertion below.
var staleAnchor = new Position(
SpawnLandblock | 0x0001u,
new Vector3(1f, 1f, SpawnHeight),
Quaternion.Identity);
controller.PositionManager!.ConstrainTo(staleAnchor, 1f, 2f);
Assert.True(controller.PositionManager.Constraint!.IsConstrained);
runtime.MovementOwner.Execute(RuntimeMovementCommand.ToggleRunLock);
Assert.True(runtime.MovementOwner.AutoRunActive);
const ushort teleportSequence = 5;
var destinationPosition = new Vector3(30f, 32f, SpawnHeight);
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
destinationPosition, SpawnLandblock | 0x0001u, teleportSequence);
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
MergeAccepted(runtime, controller, destinationUpdate);
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
Assert.True(timestamps.TeleportAdvanced);
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
// assertion below fails first - see PortalHostConvergenceGuard's doc.
using var portalHostGuard = new PortalHostConvergenceGuard(
runtime, portal.RevealGeneration, portal.Projection);
RuntimeAcceptedPositionDriveController drive =
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
RuntimeAcceptedPositionExecutionStatus status =
drive.TryExecuteAcceptedPortalArrival(destination, portal);
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Committed, status);
Assert.Equal(destinationPosition, controller.Position);
Assert.Equal(Vector3.Zero, controller.BodyVelocity);
Assert.True(controller.PositionManager.Constraint!.IsConstrained);
Assert.Equal(
controller.Position,
controller.PositionManager.Constraint.ConstraintPos.Frame.Origin);
Assert.False(runtime.MovementOwner.AutoRunActive);
// Exactly one outbound wire packet total: the movement-event refresh.
// Zero AutonomousPosition - the portal route never sends one.
Assert.Single(gameActions);
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
AssertConverged(runtime);
}
/// <summary>
/// D-T1's own safety property, proved live: a portal authority whose
/// generation the transit no longer recognizes (this generation was
/// never begun) is structurally invalid, so the arm returns
/// <c>NotApplicable</c> without writing anything - the D-T5 refusal
/// shape. A superseded token is unobtainable by construction; this
/// exercises the same <c>IsValid</c> gate a stale re-derivation would
/// fail on.
/// </summary>
[Fact]
public void PortalProducerInvalidAuthority_ArmDoesNotRunAndNothingMutates()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
(RuntimeEntityRecord record, PlayerMovementController controller) =
EnterLocalPlayer(runtime);
Vector3 positionBefore = controller.Position;
RuntimeAcceptedPositionDriveController drive =
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
var destination = new RuntimeTeleportDestination(
PlayerGuid,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 5,
ForcePositionSequence: 0,
new Position(
SpawnLandblock | 0x0001u,
new Vector3(30f, 32f, SpawnHeight),
Quaternion.Identity));
// Present but structurally invalid: RevealGeneration 0 fails
// RuntimePortalPlacementAuthority.IsValid outright - the exact shape
// a stale-generation TryRegisterHostProjection re-derivation refusal
// would leave the producer holding (default).
var invalidAuthority = new RuntimePortalPlacementAuthority(
Present: true,
RevealGeneration: 0,
TeleportSequence: 5,
Projection: default);
RuntimeAcceptedPositionExecutionStatus status =
drive.TryExecuteAcceptedPortalArrival(destination, invalidAuthority);
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.NotApplicable, status);
Assert.Equal(positionBefore, controller.Position);
Assert.Empty(gameActions);
AssertConverged(runtime);
}
/// <summary>
/// D-T5's genuinely new edge: transit pins the FIRST accepted
/// destination per generation, while <c>BeginAcceptedPlacementCore</c>
/// validates the portal's destination cell against the LATEST merged
/// snapshot. A second local Position merging a DIFFERENT landblock
/// between the offer and the Place edge makes Begin refuse. Positive
/// half: nothing mutates and the transit stays exactly as it was
/// (D-T5's "no half-state" invariant).
/// </summary>
[Fact]
public void PortalBeginCellMismatch_RefusesWithoutMutatingBodyOrTransit()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
(RuntimeEntityRecord record, PlayerMovementController controller) =
EnterLocalPlayer(runtime);
const ushort teleportSequence = 6;
var destinationPosition = new Vector3(30f, 32f, SpawnHeight);
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
destinationPosition, SpawnLandblock | 0x0001u, teleportSequence);
MergeAccepted(runtime, controller, destinationUpdate);
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
// assertion below fails first - see PortalHostConvergenceGuard's doc.
using var portalHostGuard = new PortalHostConvergenceGuard(
runtime, portal.RevealGeneration, portal.Projection);
// A SECOND local Position merges a DIFFERENT landblock after the
// portal offer/registration but before Place - record.Snapshot's
// latest accepted position no longer matches the portal's
// destination cell. SAME teleport sequence (an ordinary in-flight
// Apply, not a fresh teleport) so PhysicsTimestampGate admits it.
const uint otherLandblock = 0x02020000u;
(PositionTimestampDisposition secondDisposition, _) = MergeAccepted(
runtime,
controller,
PortalDestinationUpdate(
new Vector3(1f, 1f, SpawnHeight),
otherLandblock | 0x0001u,
teleportSequence,
positionSequence: 3));
Assert.Equal(PositionTimestampDisposition.Apply, secondDisposition);
Vector3 positionBefore = controller.Position;
RuntimeAcceptedPositionDriveController drive =
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
RuntimeAcceptedPositionExecutionStatus status =
drive.TryExecuteAcceptedPortalArrival(destination, portal);
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Contention, status);
Assert.Equal(positionBefore, controller.Position);
Assert.Empty(gameActions);
// The transit is untouched by the refusal - still active, still
// holding the SAME accepted destination, not cancelled.
Assert.True(runtime.TransitOwner.IsTeleportActive);
Assert.False(runtime.TransitOwner.Snapshot.Cancelled);
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
AssertConverged(runtime);
}
[Fact]
public void PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
(RuntimeEntityRecord record, PlayerMovementController controller) =
EnterLocalPlayer(runtime);
Vector3 positionBefore = controller.Position;
const ushort teleportSequence = 7;
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
new Vector3(30f, 32f, SpawnHeight), SpawnLandblock | 0x0001u, teleportSequence);
MergeAccepted(runtime, controller, destinationUpdate);
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
// assertion below fails first - see PortalHostConvergenceGuard's doc.
using var portalHostGuard = new PortalHostConvergenceGuard(
runtime, portal.RevealGeneration, portal.Projection);
RuntimeEntityPlacementToken displaced = runtime.EntityObjects.Physics
.SetPosition.TryBeginExclusiveAuthoredPlacement(
record,
record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative);
Assert.True(displaced.IsValid);
RuntimeAcceptedPositionDriveController drive =
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
RuntimeAcceptedPositionExecutionStatus status =
drive.TryExecuteAcceptedPortalArrival(destination, portal);
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Contention, status);
Assert.Equal(positionBefore, controller.Position);
Assert.Empty(gameActions);
RuntimePlacementCancellationReceipt cancellation = runtime.EntityObjects
.Physics.SetPosition.ForgetExactPlacement(displaced);
if (cancellation.IsValid)
{
runtime.EntityObjects.Physics.SetPosition
.PublishCancellation(cancellation);
}
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
AssertConverged(runtime);
}
/// <summary>
/// D-T2.4: a portal DeferredCell park reuses the SAME retained-operation
/// machinery the force arm uses, but on wake it commits WITHOUT the force
/// funnel's re-issue decision (trap T7 - <see cref="_newestForce"/> is
/// never touched) and WITHOUT a double commit however many times
/// <see cref="RuntimeAcceptedPositionDriveController.Advance"/> is
/// pumped afterward.
///
/// <para>Root-cause note (closed): an earlier revision of this test
/// asserted the wrong post-commit position (the FORCE arm's
/// cross-landblock +192/+192 delta) and asserted zero outbound game
/// actions after a commit that legitimately sends one. Because both
/// were WRONG, the assertion failure fired before this method's own
/// <c>ConvergePortalHost</c> cleanup call ever ran, which left the
/// portal's host projection unconverged - and <c>StartedRuntime</c>'s
/// `using`-triggered <c>Dispose()</c> then threw "hosts=1, pending=1"
/// while unwinding, MASKING the real (first) failure as a teardown
/// defect. The actual cause: an accepted Position merge with
/// <c>TeleportAdvanced</c> (this is one - #283) rebases
/// <c>RuntimePhysicsState</c>'s world-frame center onto the DESTINATION
/// landblock immediately, before the placement itself ever resolves -
/// exactly as <c>ObserveLocalWorldFrame</c>'s doc comment states ("only
/// an accepted teleport moves it afterward"). So by the time this
/// deferred park's retry runs, the destination landblock IS the frame
/// center and the correct expected offset is zero, not the FORCE
/// arm's stale-frame cross-landblock delta. No production code
/// changed to fix this; both fixes were test-assertion corrections.</para>
/// </summary>
[Fact]
public void PortalDeferredCell_ParksThenCommitsExactlyOnceOnTheCollisionGenerationWake()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
(RuntimeEntityRecord record, PlayerMovementController controller) =
EnterLocalPlayer(runtime);
RuntimeAcceptedPositionDriveController drive =
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
const uint deferredLandblock = 0x02020000u;
var deferredPosition = new Vector3(10f, 10f, SpawnHeight);
const ushort teleportSequence = 8;
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
deferredPosition, deferredLandblock | 0x0001u, teleportSequence);
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps mergeTimestamps) =
MergeAccepted(runtime, controller, destinationUpdate);
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
// #283: RuntimePhysicsState.ObserveLocalWorldFrame rebases the world
// frame center to the DESTINATION landblock the instant an accepted
// Position merges with TeleportAdvanced (RuntimeEntityObjectLifetime
// .TryApplyPosition calls it with teleportAdvanced: timestamps
// .TeleportAdvanced) - by design, "only an accepted teleport moves
// it afterward" (RuntimePhysicsState.cs doc comment). This merge IS
// that accepted teleport, so by the time the placement itself runs,
// TryGetWorldFrameOffset(deferredLandblock) is already (0,0) - the
// frame is centered ON the destination, not still on SpawnLandblock.
Assert.True(mergeTimestamps.TeleportAdvanced);
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
BeginPortal(runtime, deferredLandblock | 0x0001u, teleportSequence, destinationUpdate);
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
// assertion below fails first - see PortalHostConvergenceGuard's doc.
using var portalHostGuard = new PortalHostConvergenceGuard(
runtime, portal.RevealGeneration, portal.Projection);
RuntimeAcceptedPositionExecutionStatus parked =
drive.TryExecuteAcceptedPortalArrival(destination, portal);
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.DeferredCell, parked);
Assert.Empty(gameActions);
Assert.Equal(1, drive.PendingCount);
drive.Advance();
Assert.Equal(1, drive.PendingCount);
CommitLandblockCollision(runtime, deferredLandblock);
DrainPlacementFifo(runtime);
drive.Advance();
Assert.Equal(0, drive.PendingCount);
// Zero offset, not +192/+192: the accepted-teleport merge above
// already rebased the world frame onto deferredLandblock (#283),
// so the placement resolves in a frame ALREADY centered on the
// destination. Asserting the old cross-landblock delta here would
// be asserting a stale frame the merge already retired.
Assert.Equal(deferredPosition, controller.Position);
// The committed portal placement sends its ONE outbound movement
// event exactly like PortalCommitted_* asserts (D-T3's
// PlayerTeleported port; never an AutonomousPosition -
// SendPositionImmediately is always false for the portal route).
Assert.Single(gameActions);
// No re-issue path exists for a portal pending (trap T7): further
// pumps are pure no-ops, never a second commit or a stray ack.
drive.Advance();
drive.Advance();
Assert.Single(gameActions);
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
AssertConverged(runtime);
}
/// <summary>
/// A2/D-T2.4 review fix (2026-08-05): the wake path must re-validate
/// the portal authority before committing, so a park that resolves
/// AFTER something has made the authority stale (transit ended or was
/// superseded) does not run the reconcile/ack suffix — architecture
/// review A2's FIFO-wedge shape ("a Place receipt whose Token.Portal
/// still names the ended reveal"). The stub predicate below always
/// reports stale, standing in for that condition without needing to
/// actually end the transit mid-park.
/// </summary>
[Fact]
public void PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
(RuntimeEntityRecord record, PlayerMovementController controller) =
EnterLocalPlayer(runtime);
RuntimeAcceptedPositionDriveController drive =
CreateAcceptedPositionDrive(
runtime,
out List<byte[]> gameActions,
isPortalAuthorityCurrent: static _ => false);
const uint deferredLandblock = 0x02020000u;
var deferredPosition = new Vector3(10f, 10f, SpawnHeight);
const ushort teleportSequence = 9;
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
deferredPosition, deferredLandblock | 0x0001u, teleportSequence);
MergeAccepted(runtime, controller, destinationUpdate);
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
BeginPortal(runtime, deferredLandblock | 0x0001u, teleportSequence, destinationUpdate);
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
// assertion below fails first - see PortalHostConvergenceGuard's doc.
using var portalHostGuard = new PortalHostConvergenceGuard(
runtime, portal.RevealGeneration, portal.Projection);
RuntimeAcceptedPositionExecutionStatus parked =
drive.TryExecuteAcceptedPortalArrival(destination, portal);
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.DeferredCell, parked);
Assert.Equal(1, drive.PendingCount);
CommitLandblockCollision(runtime, deferredLandblock);
DrainPlacementFifo(runtime);
drive.Advance();
// The park resolved (PendingCount converges to zero either way —
// the wake always retires its retained operation) but because the
// stub reports the authority stale, NO reconcile/ack ran: zero
// outbound movement events, autorun untouched. The underlying body
// commit (RetryDeferred) is a SEPARATE, asynchronous mechanism this
// class cannot prevent (see the class doc on
// IsPortalAuthorityCurrent) — it still lands.
Assert.Equal(0, drive.PendingCount);
Assert.Empty(gameActions);
Assert.Equal(deferredPosition, controller.Position);
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
AssertConverged(runtime);
}
private static WorldSession.EntityPositionUpdate PortalDestinationUpdate(
Vector3 position,
uint landblockId,
ushort teleportSequence,
ushort positionSequence = 2) =>
new(
PlayerGuid,
new CreateObject.ServerPosition(
landblockId,
position.X,
position.Y,
position.Z,
1f,
0f,
0f,
0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: positionSequence,
TeleportSequence: teleportSequence,
ForcePositionSequence: 0);
/// <summary>
/// Mirrors <see cref="RuntimePlacementPresentationSinkTests.Fixture.BeginPortal"/>'s
/// shape against <see cref="GameRuntime.TransitOwner"/> directly - F751 →
/// offer → begin reveal → register the host token exactly like the
/// graphical/headless producers (D-T1).
/// </summary>
private static (RuntimePortalPlacementAuthority Portal, RuntimeTeleportDestination Destination)
BeginPortal(
GameRuntime runtime,
uint destinationCell,
ushort teleportSequence,
in WorldSession.EntityPositionUpdate destinationUpdate)
{
RuntimeWorldTransitState transit = runtime.TransitOwner;
Assert.True(transit.TryQueueTeleportStart(teleportSequence));
Assert.True(transit.ActivateQueuedTeleport());
var destination = new RuntimeTeleportDestination(
PlayerGuid,
InstanceSequence: 1,
PositionSequence: destinationUpdate.PositionSequence,
TeleportSequence: teleportSequence,
ForcePositionSequence: 0,
new Position(
destinationCell,
new Vector3(
destinationUpdate.Position.PositionX,
destinationUpdate.Position.PositionY,
destinationUpdate.Position.PositionZ),
Quaternion.Identity));
Assert.True(transit.OfferTeleportDestination(
destination,
teleportTimestampAdvanced: true));
Assert.True(transit.TryBeginPortalReveal(
teleportSequence,
destinationCell,
out long generation));
Assert.True(transit.TryRegisterHostProjection(
generation,
destinationCell,
out RuntimeWorldHostProjectionToken host));
return (
new RuntimePortalPlacementAuthority(true, generation, teleportSequence, host),
destination);
}
/// <summary>
/// Test-only host-side convergence: cancels the reveal generation and
/// drains the 4-stage host acknowledgement suffix
/// (<see cref="RuntimeWorldTransitState.RequireTerminalHostProjection"/>'s
/// SimulationReleaseProjected + DestinationReservationReleased +
/// TerminalProjected) so <see cref="RuntimeWorldTransitState.ResetSession"/>
/// does not throw during <c>StartedRuntime.Dispose()</c> teardown. A REAL
/// host (graphical/headless) always runs this suffix itself
/// (<c>LocalPlayerTeleportController.ResetTransit</c> /
/// <c>RuntimeLiveEntitySessionController.TryCompletePortal</c>'s
/// <c>Complete</c> path); this fixture stands in for that host exactly
/// like <c>DrainPlacementFifo</c> stands in for the placement
/// subscription.
/// </summary>
private static void ConvergePortalHost(
GameRuntime runtime,
long generation,
RuntimeWorldHostProjectionToken projection)
{
RuntimeWorldTransitState transit = runtime.TransitOwner;
if (!transit.Snapshot.Cancelled && !transit.Snapshot.Completed)
transit.Cancel(generation);
transit.AcknowledgeHostProjection(new RuntimeWorldHostAcknowledgement(
projection, RuntimeWorldHostAcknowledgementStage.SimulationReleaseProjected));
transit.AcknowledgeHostProjection(new RuntimeWorldHostAcknowledgement(
projection, RuntimeWorldHostAcknowledgementStage.DestinationReservationReleased));
transit.AcknowledgeHostProjection(new RuntimeWorldHostAcknowledgement(
projection, RuntimeWorldHostAcknowledgementStage.TerminalProjected));
transit.EndTeleport();
}
/// <summary>
/// B8/A8 review fix (2026-08-05): a plain trailing-statement call to
/// <see cref="ConvergePortalHost"/> is masked whenever an assertion
/// EARLIER in the same test body fails first - the real failure never
/// reaches xUnit because <c>StartedRuntime.Dispose()</c>'s own
/// <c>RuntimeWorldTransitState.ResetSession</c> throws a SECOND,
/// unrelated-looking "hosts=1, pending=1" exception while unwinding,
/// which is the one that actually surfaces. A <c>using var</c> local of
/// this guard, declared immediately after <see cref="BeginPortal"/>
/// returns, disposes on EVERY exit path (normal return AND exception
/// unwind) via C#'s own <c>using</c> semantics - equivalent to a
/// try/finally wrapping the rest of the method without the nesting.
/// <see cref="ConvergePortalHost"/> is idempotent against an already-
/// converged host (every step it performs is a no-op past the first
/// successful run), so a test that ALSO calls it explicitly on its own
/// success path is safe to leave as-is; this guard exists purely to
/// guarantee the call still happens when that explicit call is never
/// reached.
/// </summary>
private readonly struct PortalHostConvergenceGuard : IDisposable
{
private readonly GameRuntime _runtime;
private readonly long _generation;
private readonly RuntimeWorldHostProjectionToken _projection;
public PortalHostConvergenceGuard(
GameRuntime runtime,
long generation,
RuntimeWorldHostProjectionToken projection)
{
_runtime = runtime;
_generation = generation;
_projection = projection;
}
public void Dispose() =>
ConvergePortalHost(_runtime, _generation, _projection);
}
#endregion
private static void AssertConverged(GameRuntime runtime) private static void AssertConverged(GameRuntime runtime)
{ {
RuntimeEntityObjectOwnershipSnapshot ownership = RuntimeEntityObjectOwnershipSnapshot ownership =
@ -1353,7 +1927,8 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
/// </summary> /// </summary>
private static RuntimeAcceptedPositionDriveController CreateAcceptedPositionDrive( private static RuntimeAcceptedPositionDriveController CreateAcceptedPositionDrive(
GameRuntime runtime, GameRuntime runtime,
out List<byte[]> gameActions) out List<byte[]> gameActions,
Func<RuntimePortalPlacementAuthority, bool>? isPortalAuthorityCurrent = null)
{ {
var captured = new List<byte[]>(); var captured = new List<byte[]>();
gameActions = captured; gameActions = captured;
@ -1372,7 +1947,15 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
() => runtime.PlayerIdentity.ServerGuid, () => runtime.PlayerIdentity.ServerGuid,
() => runtime.MovementOwner.Controller, () => runtime.MovementOwner.Controller,
() => runtime.CharacterOwner.UsePositionFromServer, () => runtime.CharacterOwner.UsePositionFromServer,
() => liveSession); () => liveSession,
// C4 route 3: the portal arm's PlayerTeleported port needs the
// autorun latch owner. Unused on the force arm this factory has
// always served, so every existing route-2 test is unaffected.
() => runtime.MovementOwner,
// A2/D-T2.4 review fix (2026-08-05): defaults to null (existing
// callers unaffected - every retained portal pending is treated
// as current, today's unconditional behaviour).
isPortalAuthorityCurrent);
} }
/// <summary> /// <summary>
@ -1614,7 +2197,8 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
public RuntimeDestinationReadiness PrepareDestination( public RuntimeDestinationReadiness PrepareDestination(
long revealGeneration, long revealGeneration,
RuntimeTeleportDestination destination) => RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken portal) =>
new( new(
revealGeneration, revealGeneration,
destination.CellId, destination.CellId,

View file

@ -901,7 +901,8 @@ public sealed class RuntimeLiveEntitySessionControllerTests
public RuntimeDestinationReadiness PrepareDestination( public RuntimeDestinationReadiness PrepareDestination(
long revealGeneration, long revealGeneration,
RuntimeTeleportDestination destination) RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken portal)
{ {
PrepareCount++; PrepareCount++;
LastDestination = destination; LastDestination = destination;