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