diag(physics): remote landing-edge probe; record the two live jump defects

The user live-tested route 4a and reported two defects on player remotes: a
remote holds the falling animation after landing before finally landing, and a
remote jumping onto a house plants on the roof where retail slides off, then
blips to the slid-down position.

Neither is a route 4a regression. Do NOT revert 44830a0e — reverting would
restore the per-packet render slam 4a removed without touching either defect.

Bug B's root cause is identified and already covered by open issue #32, whose
text names both symptoms in one sentence. Both landing sites assert
TransientState |= Contact | OnWalkable unconditionally, where retail derives it
from the contact plane — CPhysicsObj::SetPositionInternal @0x00515330
(`if (contact_plane.N.z < floor_z) set_on_walkable(0) else set_on_walkable(1)`).
A steep roof is contact but NOT on_walkable; asserting both suppresses the slide
response, so the body sits until the server's positions walk 4 m away and
AP-87's threshold snaps it. That is the blip. Verified byte-identical pre-4a via
`git show 19d95094:`.

Bug B's *visible shape* IS 4a's: pre-4a every packet slammed the render entity
to the wire pose, so a stuck body flickered toward the true sliding position
5-10x per second — jitter rather than a clean hold.

Bug A stops at the goal's stop-condition rather than getting a speculative fix.
Three hypotheses with non-overlapping fixes; picking wrong means changing a
retail-ported gate on a guess. Retail's mechanism is already fully decoded, so
what is missing is OUR runtime state — no cdb trace against retail is needed.

Adds ACDREAM_PROBE_REMOTE_LANDING (PhysicsDiagnostics, read once at startup per
the diagnostic-owner rule, one bool check when off). It logs both landing sites
immediately before HitGround, and — the most diagnostic signal — emits a
separate line when a site is reached but the gravity gate is about to no-op,
which is hypothesis 1 (a wholesale Body.State write wiping the transient Gravity
bit mid-air, exactly AP-81's stated risk). Temporary instrumentation, marked for
stripping once the evidence is in.

Evidence recorded rather than new bugs filed: #32 gains the observation, the
root cause and the #173/AD-10 dependency caveat; AP-87 gains a live instance of
its stated risk; AD-10's stale file:line is corrected to RemoteMotionCombiner
with a note that its terrain-only normal cannot see a house roof at all.

Also files #308 — a SECOND flaky test, distinct from #302, which was twice
misattributed to it before being written down. #302 is a GC-allocation assertion
in App.Tests; #308 is a wall-clock deadline loop in Core.Net.Tests that fails
only under full-suite CPU contention (0 failures in 4 isolated runs). Conflating
them hides one, and an agent told to "ignore the known flake" would wave through
a real transport regression.

Gates: complete Release solution 10,938 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-04 01:10:03 +02:00
parent dda1e2a03a
commit eeec4fb42a
6 changed files with 366 additions and 2 deletions

View file

@ -582,6 +582,28 @@ it. Do #297 FIRST — #298 depends on it.
allocation count, matching how the other allocation gates in the repo are allocation count, matching how the other allocation gates in the repo are
written. Found while independently verifying the #297 gate. written. Found while independently verifying the #297 gate.
- **#308 — OPEN — `NakEmissionTests.LossSoak_TwoPercentBidirectional_ZeroMessageLoss_LedgersConverge`
is a SECOND, load-sensitive flake — distinct from #302. LOW.**
`tests/AcDream.Core.Net.Tests/Transport/` — a wall-clock-driven randomized
packet-loss soak with a `DateTime.UtcNow < deadline` loop. Observed failing
twice on 2026-08-03/04, **both times only inside a full-solution run**, and
0 failures in 4 consecutive isolated runs of `AcDream.Core.Net.Tests` alone.
That profile points at CPU contention starving the deadline loop under the
full suite, not at transport logic.
**Filed because it was twice misattributed to #302 before being written
down.** They are different tests in different assemblies with different
mechanisms: #302 is a `GC.GetAllocatedBytesForCurrentThread()` assertion in
`AcDream.App.Tests` sensitive to JIT tiering; this one is a wall-clock
deadline in `AcDream.Core.Net.Tests` sensitive to machine load. Conflating
them hides one of the two, and an agent instructed to "ignore the known flake"
will wave through a real transport regression.
Fix shape: drive the soak from a virtual/injected clock or an iteration count
rather than wall-clock, matching how the deterministic transport suites are
written. Do not simply widen the deadline — that hides load regressions
instead of removing the dependency. Note Campaign N's transport work is the
SSOT here; read `claude-memory/project_network_transport_digest.md` before
touching it.
- **#303 — OPEN — `LiveEntityPvpBitfieldSync` lives in App but touches only - **#303 — OPEN — `LiveEntityPvpBitfieldSync` lives in App but touches only
Runtime-owned state. INFO/shape.** Runtime-owned state. INFO/shape.**
`src/AcDream.App/Physics/LiveEntityPvpBitfieldSync.cs` reads `src/AcDream.App/Physics/LiveEntityPvpBitfieldSync.cs` reads
@ -9333,6 +9355,63 @@ anchors include `CTransition::edge_slide`, `CTransition::cliff_slide`,
**Acceptance:** Synthetic and real-DAT tests cover wall-slide, roof-edge slide, **Acceptance:** Synthetic and real-DAT tests cover wall-slide, roof-edge slide,
cliff/precipice slide, failed step-up/step-down, and the jump-clears-edge case. cliff/precipice slide, failed step-up/step-down, and the jump-clears-edge case.
**2026-08-04 live route 4a test — Bug B (remote roof-plant half-state, one of
the two symptoms this row already named) confirmed and root-caused:** the
user's two-client test reproduced exactly the "lands on roof in falling
animation, can't slide off" half-state this row already describes, and this
time as a REMOTE jumping onto a house: it plants on the roof, then blips to a
slid-down position after drifting away rather than sliding smoothly.
Root cause: the player-remote landing block
(`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs`, the
`if (rmState.Airborne)` transition) and its per-tick twin
(`src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:~493-551`) both
assert `Body.TransientState |= Contact | OnWalkable` UNCONDITIONALLY on
landing. Retail derives `on_walkable` from the contact plane instead —
`CPhysicsObj::SetPositionInternal` (named symbol @0x00515330, pseudo-C
:283501-283509):
```
if (contact_plane.N.z < floor_z)
set_on_walkable(0);
else
set_on_walkable(1);
```
A steep roof is Contact (the sphere is touching it) but NOT on_walkable (its
normal.Z is below `floor_z`) — retail keeps sliding it. Forcing both bits true
suppresses the slide response outright; the body then sits planted on the
roof until AP-87's 4 m drift-snap backstop (`docs/architecture/retail-
divergence-register.md` row AP-87) fires and blips it to the server's
already-slid-down position — the visible "plant, then teleport" the user
reported. This code is byte-identical to the pre-C4-route-4a version (verified
via `git show 19d95094:src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs`,
which shows the identical unconditional `TransientState |= Contact |
OnWalkable` at the same landing site) — **this is not a route 4a regression;
do not revert `44830a0e`.**
Fixing `OnWalkable` alone at the landing block may not be sufficient to
reproduce retail's slide, because retail's slide response also depends on two
other pieces that are either incomplete or unverified for remotes:
- **#173** (this file) shipped the remote collision-velocity reflect
(`CPhysicsObj::handle_all_collisions` pc:282699-282715) but its dedicated
visual gate was folded into the Campaign P matrix scenario 8 and that gate
has not actually been run/confirmed yet — the reflect path this fix needs
is unverified in practice, not just untested in isolation.
- **AD-10** (`docs/architecture/retail-divergence-register.md`) — remote
slope projection samples ONLY the terrain normal
(`PhysicsEngine.SampleTerrainNormal`, consumed by
`src/AcDream.Core/Physics/RemoteMotionCombiner.cs`), which cannot see
building/EnvCell geometry at all. A house roof has no terrain normal to
project against, so even a corrected `OnWalkable` would need a real
contact-plane-derived slide, not the terrain-only approximation AD-10
already flags as a divergence.
No fix has been applied for this observation — the investigation stopped
here per project policy (no workarounds without approval) and instrumented a
probe (`ACDREAM_PROBE_REMOTE_LANDING`, `PhysicsDiagnostics.cs`) instead. See
`docs/research/2026-08-04-remote-landing-investigation.md` for the companion
Bug A (falling-animation-lingers) hypothesis set and the probe's decision
table.
--- ---
## #35 — [DONE 2026-04-30] Retail debugger toolchain (cdb + PDB GUID matching) ## #35 — [DONE 2026-04-30] Retail debugger toolchain (cdb + PDB GUID matching)

View file

@ -119,7 +119,7 @@ readiness/requeue adaptation. See
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the required Near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud unhydratable-placement path. `RuntimeWorldTransitState` owns the shared reveal generation, accepted readiness, transit correlation, and exact generation/cell-scoped host-acknowledgement suffix. `WorldRevealCoordinator` is a graphical adapter holding only App resource receipts; normalized Runtime checkpoints observe ownership without defining another readiness path. **Slice E3 refinement (2026-07-24):** the same generation now publishes an immediate `WorldGenerationQuiescence` edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. **Slice E4 refinement (2026-07-24):** accepted render/physics/static publication may span update frames through retained exact cursors, but reveal still consumes only the completed spatial/render-ready generation; building and EnvCell snapshots remain invisible until complete and the final spatial identity swap stays observer-atomic. **Slice E5 refinement (2026-07-24):** the reveal generation owns one exact destination reservation across every typed budget dimension. Stale completion cannot consume or clear its replacement, and hydratable incomplete content is never force-revealed; portal transit retains the DAT tunnel and centered retail wait cue until readiness converges. The hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.Runtime/World/RuntimeWorldTransitState.cs`; `src/AcDream.App/Streaming/WorldRevealCoordinator.cs`; `src/AcDream.App/Streaming/WorldGenerationQuiescence.cs`; `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs`; `src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs`; `src/AcDream.App/Streaming/LandblockPresentationPipeline.cs`; `src/AcDream.App/Streaming/StreamingController.cs`; `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs`; `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail leaving `SmartBox::position_update_complete` false while `CellManager::blocking_for_cells` is set: neither initial login nor portal arrival may reveal or continue simulating an old/partial collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. | Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, missing scenery, or a still-active old generation; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit remains in the authored tunnel and presents the centered wait cue after five seconds. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::UseTime` 0x004D6E30; `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 | | AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the required Near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud unhydratable-placement path. `RuntimeWorldTransitState` owns the shared reveal generation, accepted readiness, transit correlation, and exact generation/cell-scoped host-acknowledgement suffix. `WorldRevealCoordinator` is a graphical adapter holding only App resource receipts; normalized Runtime checkpoints observe ownership without defining another readiness path. **Slice E3 refinement (2026-07-24):** the same generation now publishes an immediate `WorldGenerationQuiescence` edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. **Slice E4 refinement (2026-07-24):** accepted render/physics/static publication may span update frames through retained exact cursors, but reveal still consumes only the completed spatial/render-ready generation; building and EnvCell snapshots remain invisible until complete and the final spatial identity swap stays observer-atomic. **Slice E5 refinement (2026-07-24):** the reveal generation owns one exact destination reservation across every typed budget dimension. Stale completion cannot consume or clear its replacement, and hydratable incomplete content is never force-revealed; portal transit retains the DAT tunnel and centered retail wait cue until readiness converges. The hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.Runtime/World/RuntimeWorldTransitState.cs`; `src/AcDream.App/Streaming/WorldRevealCoordinator.cs`; `src/AcDream.App/Streaming/WorldGenerationQuiescence.cs`; `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs`; `src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs`; `src/AcDream.App/Streaming/LandblockPresentationPipeline.cs`; `src/AcDream.App/Streaming/StreamingController.cs`; `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs`; `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail leaving `SmartBox::position_update_complete` false while `CellManager::blocking_for_cells` is set: neither initial login nor portal arrival may reveal or continue simulating an old/partial collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. | Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, missing scenery, or a still-active old generation; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit remains in the authored tunnel and presents the centered wait cue after five seconds. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::UseTime` 0x004D6E30; `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 |
| AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) | | AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) |
| ~~AD-6~~ | **RETIRED 2026-07-31 (placement/streaming Slice 3B).** Cell/cache/topology/building/static-shadow publication plus every retained non-suspended owner touching or withdrawn from the prefix is one Runtime-owned collision generation. Retained includes dynamics and adjacent-root statics; only target-root statics are superseded by the authored replacement. App and Headless build one shared off-side `CollisionWorldState` through one-work-unit preparation/capture/seal cursors. Admission captures the active root in O(1); a stable landblock/owner slot suffix materializes non-target leaves incrementally, so resident-world size cannot become a synchronous clone spike. Reusable per-prefix owner slots and one Runtime-scoped versioned journal replace event-time exact-copy fanout: repeated live mutations coalesce by owner, every draft reconciles only that owner's latest exact state one owner per seal call, discovered relevant owners receive scoped exact updates, and visited unrelated owners receive only a cheap coalesced dirty notification before metered replay. Once topology sealing finishes, observed owners temporarily write through exactly until same-call activation; the finite pre-seal queue therefore drains even under continuous multi-owner movement. New drafts start at their captured journal suffix; old slots are superseded rather than reused behind live cursors and compact through the same meter. Unrelated churn therefore never restarts or starves target capture/sealing. Deterministically ordered concurrent preparations receive committed—not merely sealed—peer deltas and rebase one cache, graph, landblock, or owner leaf per seal step; cancellation therefore cannot leak unpublished topology. Demotion/withdrawal cancels a matching queued or active rebase, suppresses the prefix in unfinished source scans, and retires one owner/cache/graph/outdoor leaf per seal call. The complete previous generation remains queryable until one zero-managed-byte volatile root transfer in the same update-thread call as final reconciliation; that preserves PhysicsDataCache, CellGraph, PhysicsEngine, and ShadowObjectRegistry facade identity, revokes staging, and requires no quiet frame. A stale admission or staging failure disposes only that private generation and cannot withdraw the active world or invalidate a newer admission. Authored same-ID target statics, live-current-cell changes, owner departure/reuse, newly relevant seam-crossing statics, and teardown remain coherent across drafts; empty per-prefix owner containers are reclaimed without invalidating captured seal cursors. The commit clears repaired withdrawal markers before its single notification/readiness acknowledgement, so no optional hydration callback can omit reflood and no observer sees mixed old/new cells. | `src/AcDream.Runtime/Physics/RuntimePhysicsState.cs` (`PrepareCollisionGeneration`, `AdvanceCollisionGenerationPreparation`, `AdvanceCollisionGenerationSeal`, `CommitCollisionGeneration`); `src/AcDream.Core/Physics/CollisionWorldState.cs`; `PhysicsDataCache.cs`; `PhysicsEngine.cs`; `ShadowObjectRegistry.cs`; `src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs`; `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs`; `tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs`; `tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs`; `tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs` | — | — | `CObjCell::init_objects``CPhysicsObj::recalc_cross_cells`, 0x0052b420 / 0x00515a30; `CPhysicsObj::SetPositionInternal` shadow replacement tail 0x00515330 | | ~~AD-6~~ | **RETIRED 2026-07-31 (placement/streaming Slice 3B).** Cell/cache/topology/building/static-shadow publication plus every retained non-suspended owner touching or withdrawn from the prefix is one Runtime-owned collision generation. Retained includes dynamics and adjacent-root statics; only target-root statics are superseded by the authored replacement. App and Headless build one shared off-side `CollisionWorldState` through one-work-unit preparation/capture/seal cursors. Admission captures the active root in O(1); a stable landblock/owner slot suffix materializes non-target leaves incrementally, so resident-world size cannot become a synchronous clone spike. Reusable per-prefix owner slots and one Runtime-scoped versioned journal replace event-time exact-copy fanout: repeated live mutations coalesce by owner, every draft reconciles only that owner's latest exact state one owner per seal call, discovered relevant owners receive scoped exact updates, and visited unrelated owners receive only a cheap coalesced dirty notification before metered replay. Once topology sealing finishes, observed owners temporarily write through exactly until same-call activation; the finite pre-seal queue therefore drains even under continuous multi-owner movement. New drafts start at their captured journal suffix; old slots are superseded rather than reused behind live cursors and compact through the same meter. Unrelated churn therefore never restarts or starves target capture/sealing. Deterministically ordered concurrent preparations receive committed—not merely sealed—peer deltas and rebase one cache, graph, landblock, or owner leaf per seal step; cancellation therefore cannot leak unpublished topology. Demotion/withdrawal cancels a matching queued or active rebase, suppresses the prefix in unfinished source scans, and retires one owner/cache/graph/outdoor leaf per seal call. The complete previous generation remains queryable until one zero-managed-byte volatile root transfer in the same update-thread call as final reconciliation; that preserves PhysicsDataCache, CellGraph, PhysicsEngine, and ShadowObjectRegistry facade identity, revokes staging, and requires no quiet frame. A stale admission or staging failure disposes only that private generation and cannot withdraw the active world or invalidate a newer admission. Authored same-ID target statics, live-current-cell changes, owner departure/reuse, newly relevant seam-crossing statics, and teardown remain coherent across drafts; empty per-prefix owner containers are reclaimed without invalidating captured seal cursors. The commit clears repaired withdrawal markers before its single notification/readiness acknowledgement, so no optional hydration callback can omit reflood and no observer sees mixed old/new cells. | `src/AcDream.Runtime/Physics/RuntimePhysicsState.cs` (`PrepareCollisionGeneration`, `AdvanceCollisionGenerationPreparation`, `AdvanceCollisionGenerationSeal`, `CommitCollisionGeneration`); `src/AcDream.Core/Physics/CollisionWorldState.cs`; `PhysicsDataCache.cs`; `PhysicsEngine.cs`; `ShadowObjectRegistry.cs`; `src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs`; `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs`; `tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs`; `tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs`; `tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs` | — | — | `CObjCell::init_objects``CPhysicsObj::recalc_cross_cells`, 0x0052b420 / 0x00515a30; `CPhysicsObj::SetPositionInternal` shadow replacement tail 0x00515330 |
| AD-10 | Remote slope projection relocated to the queue-empty/head-reached combiner boundary; retail projects inside `CTransition::adjust_offset` during the sweep | `src/AcDream.Core/Physics/PositionManager.cs:47` | Remote bodies don't run a full local transition sweep; boundary projection removes the ~5 Hz Z staircase on slopes, no-op on flat ground | The single-point terrain-normal sample can differ from the sweep's contact plane (cell boundaries, props underfoot) — remote Z drift / stair-stepping | `CTransition::adjust_offset` pc:272296-272346 | | AD-10 | Remote slope projection relocated to the queue-empty/head-reached combiner boundary; retail projects inside `CTransition::adjust_offset` during the sweep | **2026-08-04: file:line corrected** — the mechanism now lives in `src/AcDream.Core/Physics/RemoteMotionCombiner.cs` (`ComposeOffset` ~:65-72 for the interpolation-active boundary projection, the queue-empty fallback ~:163-168); the row's meaning is unchanged, the class was renamed/moved from the stale `PositionManager.cs:47` citation (see the class's own doc comment: "Renamed R5 (was PositionManager)") | Remote bodies don't run a full local transition sweep; boundary projection removes the ~5 Hz Z staircase on slopes, no-op on flat ground | The single-point terrain-normal sample can differ from the sweep's contact plane (cell boundaries, props underfoot) — remote Z drift / stair-stepping; it also cannot see building/EnvCell geometry at all (terrain-only sample), so a remote landing on a house roof gets no slope response from this path regardless of `OnWalkable` — a contributing factor in the 2026-08-04 Bug B roof-plant observation (see `docs/ISSUES.md` #32) | `CTransition::adjust_offset` pc:272296-272346 |
| ~~AD-11~~ | **RETIRED 2026-07-23** — the matching binary disproved the old nonzero interpretation: `ItemUses::IsUseable` executes `not bitfield; and eax,1`, so absent/reset zero is usable and only `USEABLE_NO` disables use. Toolbar, item policy, and world interaction now share that exact Core predicate. | `src/AcDream.Core/Items/ClientObject.cs` (`ItemUseability.IsUseable`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs`; `src/AcDream.App/Interaction/WorldSelectionQuery.cs` | — | — | `ItemUses::IsUseable @ 0x004FCCC0`; matching v11.4186 instructions recorded in `docs/research/2026-07-23-retail-item-use-and-autowear-pseudocode.md` | | ~~AD-11~~ | **RETIRED 2026-07-23** — the matching binary disproved the old nonzero interpretation: `ItemUses::IsUseable` executes `not bitfield; and eax,1`, so absent/reset zero is usable and only `USEABLE_NO` disables use. Toolbar, item policy, and world interaction now share that exact Core predicate. | `src/AcDream.Core/Items/ClientObject.cs` (`ItemUseability.IsUseable`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs`; `src/AcDream.App/Interaction/WorldSelectionQuery.cs` | — | — | `ItemUses::IsUseable @ 0x004FCCC0`; matching v11.4186 instructions recorded in `docs/research/2026-07-23-retail-item-use-and-autowear-pseudocode.md` |
| AD-12 | SecondaryAttributeTable coefficients hardcoded (Health=End×0.5, Stam=End×1.0, Mana=Self×1.0) instead of dat-read; unknown attributes contribute 0 | `src/AcDream.Core/Player/LocalPlayerState.cs:279` | Coefficients never vary across retail dat versions; re-confirmed by ACE AttributeFormula.cs + holtburger; dat port can replace later | A customized portal.dat with modified vital formulas silently yields wrong max-vitals; a missing attribute snapshot underestimates max | SecondaryAttributeTable portal.dat 0x0E0..0x0E2; `CreatureVital::GetMaxValue` 0x0058F2DD | | AD-12 | SecondaryAttributeTable coefficients hardcoded (Health=End×0.5, Stam=End×1.0, Mana=Self×1.0) instead of dat-read; unknown attributes contribute 0 | `src/AcDream.Core/Player/LocalPlayerState.cs:279` | Coefficients never vary across retail dat versions; re-confirmed by ACE AttributeFormula.cs + holtburger; dat port can replace later | A customized portal.dat with modified vital formulas silently yields wrong max-vitals; a missing attribute snapshot underestimates max | SecondaryAttributeTable portal.dat 0x0E0..0x0E2; `CreatureVital::GetMaxValue` 0x0058F2DD |
| AD-13 | 1-second dedup window for identical system chat messages (retail has none) | `src/AcDream.Core/Chat/ChatLog.cs:29` | ACE dual-sends the same system text (0xF7E0 + 0x02EB) for back-compat; without dedup every line doubled (Phase J compromise) | Two genuinely distinct but textually identical system messages within 1 s collapse to one line where retail shows both | ACE dual-send 0xF7E0 + 0x02EB | | AD-13 | 1-second dedup window for identical system chat messages (retail has none) | `src/AcDream.Core/Chat/ChatLog.cs:29` | ACE dual-sends the same system text (0xF7E0 + 0x02EB) for back-compat; without dedup every line doubled (Phase J compromise) | Two genuinely distinct but textually identical system messages within 1 s collapse to one line where retail shows both | ACE dual-send 0xF7E0 + 0x02EB |
@ -239,7 +239,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-83 | **CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary**: the CCylSphere family port (2026-07-05, retires AP-6) reads `collide_with_point`'s PerfectClip time-of-impact math (0x0053adb6+) from ACE `CylSphere.CollideWithPoint` because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (`movement.Z + radius` in the not-definite ascending case; `GlobalCurrCenter[0]` used even for head-sphere hits — the latter matches the raw decomp read). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. The non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified. Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylCollideWithPoint`; pseudocode doc `docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md` §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` | | AP-83 | **CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary**: the CCylSphere family port (2026-07-05, retires AP-6) reads `collide_with_point`'s PerfectClip time-of-impact math (0x0053adb6+) from ACE `CylSphere.CollideWithPoint` because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (`movement.Z + radius` in the not-definite ascending case; `GlobalCurrCenter[0]` used even for head-sphere hits — the latter matches the raw decomp read). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. The non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified. Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylCollideWithPoint`; pseudocode doc `docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md` §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` |
| AP-91 | **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` | | AP-91 | **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` |
| AP-86 | **Remote SHADOW-follows-resolved via a pose/cell-gated per-tick re-flood** (remote-creature de-overlap #184): every remote's collision shadow is rewritten at the resolved body position by the DR tick or authoritative UP tail, so collision remains where the creature renders and de-overlap persists. The effect matches retail, but acdream runs the full multipart cell flood whenever the body moved more than 1 cm, changed complete orientation, or crossed a cell instead of translating the existing shadow in place and relinking only when its crossed-cell set changes. Cross-cell motion now commits body/root/full-cell before the canonical rebucket callback; local and authoritative remote publishers prove exact-record spatial residency after that callback; pending projection suspends the retained shadow and cannot re-add it, including initial-pending and callback GUID-reuse cases. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs`; `src/AcDream.App/Physics/LiveEntityShadowPublisher.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (local projection); `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (authoritative UP tails); `src/AcDream.App/World/LiveEntityPresentationController.cs` (ordinary projection residency); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`UpdatePosition`) | The pose/cell gate is exact at de-overlap equilibrium, preserves offset/multipart shapes during in-place turns, and the resulting registered cell set matches retail; loaded/pending residency is symmetric and incarnation-scoped | A dense moving or turning crowd can still perform a full registration flood per creature per tick and create CPU/Gen0 pressure; a still crowd is gated out. Retire with an in-place move plus cell-relink-on-change implementation | `CPhysicsObj::SetPositionInternal(CTransition const*)` 0x00515330 → `change_cell`, then `remove_shadows_from_cells`/`add_shadows_to_cells` after the resolved frame/contact commit | | AP-86 | **Remote SHADOW-follows-resolved via a pose/cell-gated per-tick re-flood** (remote-creature de-overlap #184): every remote's collision shadow is rewritten at the resolved body position by the DR tick or authoritative UP tail, so collision remains where the creature renders and de-overlap persists. The effect matches retail, but acdream runs the full multipart cell flood whenever the body moved more than 1 cm, changed complete orientation, or crossed a cell instead of translating the existing shadow in place and relinking only when its crossed-cell set changes. Cross-cell motion now commits body/root/full-cell before the canonical rebucket callback; local and authoritative remote publishers prove exact-record spatial residency after that callback; pending projection suspends the retained shadow and cannot re-add it, including initial-pending and callback GUID-reuse cases. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs`; `src/AcDream.App/Physics/LiveEntityShadowPublisher.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (local projection); `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (authoritative UP tails); `src/AcDream.App/World/LiveEntityPresentationController.cs` (ordinary projection residency); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`UpdatePosition`) | The pose/cell gate is exact at de-overlap equilibrium, preserves offset/multipart shapes during in-place turns, and the resulting registered cell set matches retail; loaded/pending residency is symmetric and incarnation-scoped | A dense moving or turning crowd can still perform a full registration flood per creature per tick and create CPU/Gen0 pressure; a still crowd is gated out. Retire with an in-place move plus cell-relink-on-change implementation | `CPhysicsObj::SetPositionInternal(CTransition const*)` 0x00515330 → `change_cell`, then `remove_shadows_from_cells`/`add_shadows_to_cells` after the resolved frame/contact commit |
| AP-87 | **Remote MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap** beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07; unified across player-remote and NPC-remote by C4 route 4a, 2026-08-03 retail's disassembly makes no `this==player` distinction here either, so the two formerly-duplicated per-kind copies are now the SAME decision): retail `CPhysicsObj::MoveOrTeleport` (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the 96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions `|Body.Position worldPos| > 4 m` (a large correction / an unplaced first-UP body) and `!willBeDrTicked` (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body garbage resolved pos the reverted attempt's INVISIBLE monster. A third condition `firstUp` (`LastServerPosTime <= 0`) is RETAINED, not dropped, in the unified seam: it is a belt hint only the 4 m guard is the load-bearing backstop and it is structurally false for player remotes because the player-remote caller stamps `LastServerPosTime` in its diagnostic roll-forward block before it routes, so unifying the two copies on all three conditions leaves the player branch's own behaviour bit-identical | `src/AcDream.Runtime/Physics/RuntimeRemoteSteadyStatePosition.cs` (`ApplyInterpolate`, `BodySnapThreshold`); called from `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` for both the player-remote and NPC-remote near-Interpolate branches. The legacy far/cell-less/rejected fallbacks in that file still carry their own pre-existing copies of these constants until C4 route 4b deletes them | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii inherits **TS-46** | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare | `CPhysicsObj::MoveOrTeleport` 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap 96 m); `InterpolationManager` 100 m `AutonomyBlipDistance` (the retail large-correction path) | | AP-87 | **Remote MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap** beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07; unified across player-remote and NPC-remote by C4 route 4a, 2026-08-03 retail's disassembly makes no `this==player` distinction here either, so the two formerly-duplicated per-kind copies are now the SAME decision): retail `CPhysicsObj::MoveOrTeleport` (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the 96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions `|Body.Position worldPos| > 4 m` (a large correction / an unplaced first-UP body) and `!willBeDrTicked` (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body garbage resolved pos the reverted attempt's INVISIBLE monster. A third condition `firstUp` (`LastServerPosTime <= 0`) is RETAINED, not dropped, in the unified seam: it is a belt hint only the 4 m guard is the load-bearing backstop and it is structurally false for player remotes because the player-remote caller stamps `LastServerPosTime` in its diagnostic roll-forward block before it routes, so unifying the two copies on all three conditions leaves the player branch's own behaviour bit-identical | `src/AcDream.Runtime/Physics/RuntimeRemoteSteadyStatePosition.cs` (`ApplyInterpolate`, `BodySnapThreshold`); called from `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` for both the player-remote and NPC-remote near-Interpolate branches. The legacy far/cell-less/rejected fallbacks in that file still carry their own pre-existing copies of these constants until C4 route 4b deletes them | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii inherits **TS-46** | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare. **2026-08-04 observed live**: the route 4a two-client test caught exactly this risk — a player remote jumping onto a house roof plants there (the landing block's unconditional `OnWalkable` forces the body to treat the steep roof as walkable, so it never slides) and sits until it has drifted >4 m from the server's actual slid-down position, at which point this backstop fires and blips it to that position instead of sliding smoothly (Bug B, `docs/ISSUES.md` #32) | `CPhysicsObj::MoveOrTeleport` 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap 96 m); `InterpolationManager` 100 m `AutonomyBlipDistance` (the retail large-correction path) |
| AP-89 | **TransparentPartHook fade multiplies the SAMPLED TEXTURE alpha, not a separate material alpha channel** (#188, 2026-07-08 — the fading-wall secret-passage doors, e.g. "Pedestal Weak Spot"): retail's `CPhysicsPart::SetTranslucency` (0x0050e670) → `CMaterial::SetTranslucencySimple` (0x005396f0) REPLACES the D3D9 material's 4 alpha channels wholesale (`Ambient.a = Diffuse.a = Specular.a = Emissive.a = 1 translucency`) — a per-material alpha that composes with, but is conceptually separate from, the surface's own sampled texture alpha. acdream's `mesh_modern.frag` has no material-alpha concept at all; the port multiplies the runtime fade's opacity multiplier directly against the already-sampled `color.a` (`FragColor = vec4(rgb, color.a * vOpacityMultiplier)`) | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` (final `FragColor` line); `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`ClassifyBatches` `opacityMultiplier` param, `InstanceGroup.Opacities`); `src/AcDream.Core/Rendering/TranslucencyFadeManager.cs` | Observably identical to retail for any surface whose base texture alpha is 1.0 everywhere — the Pedestal Weak Spot's stone-wall texture, and the overwhelming majority of AC surfaces, since `color.a * 1.0 == color.a` and the fade multiplier alone then drives the ramp exactly as `1 translucency` would | A hypothetical object that is BOTH already alpha-keyed/blended from its own texture (stained glass, a flame surface) AND plays a TransparentPartHook fade simultaneously would compound the two alphas (texture-alpha × fade-multiplier) instead of the fade cleanly replacing/overriding the surface's own alpha as retail's material-replace does — such an object would fade darker / more-transparent than retail, not just at retail's rate | `CPhysicsPart::SetTranslucency` 0x0050e670; `CMaterial::SetTranslucencySimple` 0x005396f0 (`alpha = 1 translucency`, applied to all 4 D3D9 material alpha channels) | | AP-89 | **TransparentPartHook fade multiplies the SAMPLED TEXTURE alpha, not a separate material alpha channel** (#188, 2026-07-08 — the fading-wall secret-passage doors, e.g. "Pedestal Weak Spot"): retail's `CPhysicsPart::SetTranslucency` (0x0050e670) → `CMaterial::SetTranslucencySimple` (0x005396f0) REPLACES the D3D9 material's 4 alpha channels wholesale (`Ambient.a = Diffuse.a = Specular.a = Emissive.a = 1 translucency`) — a per-material alpha that composes with, but is conceptually separate from, the surface's own sampled texture alpha. acdream's `mesh_modern.frag` has no material-alpha concept at all; the port multiplies the runtime fade's opacity multiplier directly against the already-sampled `color.a` (`FragColor = vec4(rgb, color.a * vOpacityMultiplier)`) | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` (final `FragColor` line); `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`ClassifyBatches` `opacityMultiplier` param, `InstanceGroup.Opacities`); `src/AcDream.Core/Rendering/TranslucencyFadeManager.cs` | Observably identical to retail for any surface whose base texture alpha is 1.0 everywhere — the Pedestal Weak Spot's stone-wall texture, and the overwhelming majority of AC surfaces, since `color.a * 1.0 == color.a` and the fade multiplier alone then drives the ramp exactly as `1 translucency` would | A hypothetical object that is BOTH already alpha-keyed/blended from its own texture (stained glass, a flame surface) AND plays a TransparentPartHook fade simultaneously would compound the two alphas (texture-alpha × fade-multiplier) instead of the fade cleanly replacing/overriding the surface's own alpha as retail's material-replace does — such an object would fade darker / more-transparent than retail, not just at retail's rate | `CPhysicsPart::SetTranslucency` 0x0050e670; `CMaterial::SetTranslucencySimple` 0x005396f0 (`alpha = 1 translucency`, applied to all 4 D3D9 material alpha channels) |
| AP-90 | **Radar fellowship/allegiance relationship state is modeled but not yet delivered at runtime.** `RetailRadar.GetBlipShape` and `RadarBlipColors.For` implement retail's leader/member/allegiance precedence, and `RadarSnapshotProvider` exposes a `relationshipFor(guid)` seam, but acdream does not yet maintain live fellowship membership and its `AllegianceTree` is not wired into GameWindow. PK/PKLite relationship shapes do work from PWD flags. | `src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs`; `src/AcDream.Core/Ui/RetailRadar.cs`; `src/AcDream.Core/Ui/RadarBlipColors.cs` | Preserve the exact model/seam now and avoid inventing membership from names or chat; connect it when the social game-event state is ported | Fellowship members render their ordinary player color/shape instead of bright-green leader/member triangles; allegiance members render an ordinary plus instead of a hollow box | `gmRadarUI::GetBlipColor` 0x004D76F0; `gmRadarUI::GetBlipShape` 0x004D7B60 | | AP-90 | **Radar fellowship/allegiance relationship state is modeled but not yet delivered at runtime.** `RetailRadar.GetBlipShape` and `RadarBlipColors.For` implement retail's leader/member/allegiance precedence, and `RadarSnapshotProvider` exposes a `relationshipFor(guid)` seam, but acdream does not yet maintain live fellowship membership and its `AllegianceTree` is not wired into GameWindow. PK/PKLite relationship shapes do work from PWD flags. | `src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs`; `src/AcDream.Core/Ui/RetailRadar.cs`; `src/AcDream.Core/Ui/RadarBlipColors.cs` | Preserve the exact model/seam now and avoid inventing membership from names or chat; connect it when the social game-event state is ported | Fellowship members render their ordinary player color/shape instead of bright-green leader/member triangles; allegiance members render an ordinary plus instead of a hollow box | `gmRadarUI::GetBlipColor` 0x004D76F0; `gmRadarUI::GetBlipShape` 0x004D7B60 |
| AP-92 | Private creature viewports (paperdoll and examination) render through isolated `IGpuRenderTarget`s and blit into `UiViewport`; retail renders each `CreatureMode` directly and advances a cloned `CPhysicsObj`, while examination currently refreshes its clone from the live target's animated mesh pose. **V6l narrowing (2026-07-28), V11 update (2026-07-29):** the target is backend-neutral and the blit's V origin is not assumed — `IUiViewportRenderer.TextureIsBottomUp` derives it from the backend that made the texture. With GL deleted the only answer in the tree is Vulkan's top-left origin, but the seam is kept rather than folded flat, because it costs one property and it is what let the origin question be answered by data instead of by assumption. | `src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs`; `src/AcDream.App/Rendering/PaperdollFramePresenter.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.App/UI/UiViewport.cs` | Vulkan render-to-texture is the modern backend equivalent; shared live pose data provides animation without registering a second gameplay entity or duplicating the world sequencer | Alpha, lighting, state isolation, or an assessment-time cloned motion diverging later from the live target can differ from direct retail CreatureMode presentation. The origin half of this risk is closed; a FUTURE backend would have to answer `TextureIsBottomUp` for itself | `CPhysicsObj::makeObject(CPhysicsObj const*) @ 0x005144B0`; `gmPaperDollUI::PostInit @ 0x004A5360`; `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `UIElement_Viewport::SetCamera`; retail `CreatureMode::Render` | | AP-92 | Private creature viewports (paperdoll and examination) render through isolated `IGpuRenderTarget`s and blit into `UiViewport`; retail renders each `CreatureMode` directly and advances a cloned `CPhysicsObj`, while examination currently refreshes its clone from the live target's animated mesh pose. **V6l narrowing (2026-07-28), V11 update (2026-07-29):** the target is backend-neutral and the blit's V origin is not assumed — `IUiViewportRenderer.TextureIsBottomUp` derives it from the backend that made the texture. With GL deleted the only answer in the tree is Vulkan's top-left origin, but the seam is kept rather than folded flat, because it costs one property and it is what let the origin question be answered by data instead of by assumption. | `src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs`; `src/AcDream.App/Rendering/PaperdollFramePresenter.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.App/UI/UiViewport.cs` | Vulkan render-to-texture is the modern backend equivalent; shared live pose data provides animation without registering a second gameplay entity or duplicating the world sequencer | Alpha, lighting, state isolation, or an assessment-time cloned motion diverging later from the live target can differ from direct retail CreatureMode presentation. The origin half of this risk is closed; a FUTURE backend would have to answer `TextureIsBottomUp` for itself | `CPhysicsObj::makeObject(CPhysicsObj const*) @ 0x005144B0`; `gmPaperDollUI::PostInit @ 0x004A5360`; `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `UIElement_Viewport::SetCamera`; retail `CreatureMode::Render` |

View file

@ -0,0 +1,142 @@
# 2026-08-04 — Remote landing investigation (Bug A / Bug B, route 4a live test)
**Status:** Report-only. No fix applied. Companion to `docs/ISSUES.md` #32
(the two symptoms below were both already named by that row's "lands on roof
in falling animation, can't slide off" line) and to the
`ACDREAM_PROBE_REMOTE_LANDING` probe added in `PhysicsDiagnostics.cs`.
## Background
The user's live two-client route 4a test surfaced two PLAYER-remote defects:
- **Bug A** — a remote stays in the falling animation after landing, then
visibly lands (the pose clears) only after a delay.
- **Bug B** — a remote jumping onto a house plants on the roof where retail
slides off, then blips to the slid-down position.
Neither is a route 4a regression (`44830a0e`, 2026-08-04). The landing block
in `LiveEntityNetworkUpdateController.cs` that both bugs touch is
byte-identical to the pre-4a version — verified via
`git show 19d95094:src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs`,
which shows the same unconditional
`Body.TransientState |= Contact | OnWalkable` at the same landing site
before route 4a existed. **Do not revert `44830a0e`.**
## Bug B — root-caused, not yet fixed
See `docs/ISSUES.md` #32's 2026-08-04 addendum and
`docs/architecture/retail-divergence-register.md` rows AD-10 and AP-87 for
the full citation chain. Summary: the landing block force-sets
`OnWalkable` unconditionally, where retail derives it from the contact
plane (`CPhysicsObj::SetPositionInternal` @0x00515330, pseudo-C
:283501-283509 — `on_walkable = contact_plane.N.z >= floor_z`). A steep
roof is Contact but not on_walkable; forcing both suppresses the slide,
and the body sits until AP-87's 4 m drift-snap backstop blips it to the
server's already-slid position. A correct fix additionally depends on
**#173**'s remote collision-velocity reflect (shipped, its dedicated gate
folded into the unrun Campaign P matrix scenario 8) and on **AD-10**'s
slope projection, which is terrain-only and cannot see building/EnvCell
geometry at all.
## Bug A — three hypotheses, no overlapping fix
The falling-pose-lingers symptom has three candidate root causes. Each
points at a different code path with a non-overlapping fix, so guessing
which one applies risks fixing the wrong thing (or "fixing" all three and
losing track of which one mattered). The
`ACDREAM_PROBE_REMOTE_LANDING=1` probe (added 2026-08-04,
`src/AcDream.Core/Physics/PhysicsDiagnostics.cs`:
`LogRemoteLanding`/`LogRemoteLandingGateNoOp`) was built specifically to
discriminate them from one live capture at both landing-detection sites:
`LiveEntityNetworkUpdateController.cs`'s UpdatePosition-driven landing
block (`site=controller`) and `RuntimeRemotePhysicsUpdater.cs`'s per-tick
VectorUpdate landing branch (`site=per-tick`, ~:493-551).
### H1 — Gravity state bit wiped mid-air, HitGround's gate silently no-ops (CONFIRMED mechanism, occurrence unconfirmed)
`MotionInterpreter.HitGround()` (`MotionInterpreter.cs:2426`, retail
`CMotionInterp::HitGround` 0x00528ac0) starts with:
```csharp
if (!PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity))
return;
```
This mirrors retail's own gate (`state & 0x400`, decomp raw
305996-306014) — retail requires Gravity still set at landing, same as
we do. If something clears the Gravity bit on a remote's body BEFORE its
landing edge fires, `HitGround()` returns immediately: no
`RemoveLinkAnimations`, no `apply_current_movement` re-dispatch, and the
sequencer never receives the command that would swap Falling → the
landing link → the grounded cycle. The falling pose then only clears once
some LATER event forces a cycle (a subsequent UpdateMotion, or the
generic per-tick funnel eventually reasserting a grounded stance by a
different path) — matching the observed "delay."
Both known Gravity-clear sites in the codebase are the POST-HitGround
"DR bookkeeping" clears
(`LiveEntityNetworkUpdateController.cs` and
`RuntimeRemotePhysicsUpdater.cs`, both guarded by
`IsCurrentStateAuthority`/`IsCurrentOwner` version checks) — i.e. the
intended clear happens AFTER HitGround, not before. No third site was
found that clears Gravity early in this pass; if H1 is the live culprit,
the probe should catch a case where an inbound authority-version race (a
second UP superseding the landing packet, or the state-authority version
check failing) caused the clear to land ahead of a re-entrant landing
detection. **Discriminator:** `gravitySet=false` in the `[remote-landing]`
line, paired with a `[remote-landing-gate]` `NOOP` line at the same site.
### H2 — No `DefaultSink` bound at the landing edge (hypothesized, plausible)
`HitGround()`'s `apply_current_movement` dispatches through
`Motion.DefaultSink`. The controller site only calls
`EnsureRemoteMotionBindings` (which creates the sink) when
`_animatedEntities.TryGetValue(entity.Id, out var aeForLand)` finds an
entry AND `aeForLand.Sequencer is not null`
(`LiveEntityNetworkUpdateController.cs`, right before the probe call added
2026-08-04). A remote whose `LiveEntityAnimationState`/`Sequencer` hasn't
been created yet at the exact frame its landing UP arrives (freshly
streamed-in, or a presentation race) would reach `HitGround()` with
Gravity still set but no sink to drive — the re-apply computes correctly
but has nowhere to write, so nothing visible changes until the sink is
bound on a later frame and something else (the stale `VU.land` per-tick
branch, or the next ordinary UpdateMotion) catches the pose up.
**Discriminator:** `gravitySet=true`, `hasDefaultSink=false` in the
`[remote-landing]` line.
### H3 — HitGround dispatches correctly; the delay is downstream in animation-scheduler consumption (hypothesized, weakest evidence)
If both Gravity and the sink are fine at the landing edge, the failure
(if it still reproduces) is not in the physics/motion-interpreter layer
at all — `HitGround()` successfully queues the landing link, but the
render-side animation scheduler (`LiveEntityAnimationScheduler`/
`LiveEntityAnimationPresenter`) doesn't drain and apply that queued
transition for several frames, so the falling pose visibly persists even
though the underlying `MotionInterpreter` state is already correct.
**Discriminator:** `gravitySet=true`, `hasDefaultSink=true`, sequencer
`seqStyle`/`seqMotion` at the landing edge necessarily still reads the
pre-landing (Falling) values (the read happens immediately before
HitGround runs, so this alone doesn't distinguish success from H3) —
confirming/refuting H3 requires cross-referencing the SAME guid's
subsequent `[remote-landing]`/`VU.land`/`ACDREAM_DUMP_MOTION` SetCycle
lines over the next several frames to see whether the cycle swap is
applied promptly or lags.
## Decision table for tomorrow's capture
Run with `ACDREAM_PROBE_REMOTE_LANDING=1` (pair with
`ACDREAM_DUMP_MOTION=1` for the existing `VU.land`/SetCycle lines) across
a route 4a session that reproduces Bug A, then read the first
`[remote-landing]` line for the affected guid at its landing edge:
| `gravitySet` | `hasDefaultSink` | Falling pose clears next frame? | Implicates |
|---|---|---|---|
| `false` (+ `[remote-landing-gate]` NOOP) | — | — | **H1** — fix where Gravity gets cleared/never-set before this landing edge |
| `true` | `false` | — | **H2** — fix the sink-binding race (bind before the landing block runs, or defer the landing block until a sink exists) |
| `true` | `true` | No — lags several frames in the `VU.land`/SetCycle trail | **H3** — fix the animation-scheduler consumption path, not physics |
| `true` | `true` | Yes | Landing worked correctly on this instance — Bug A did not reproduce here; re-run to catch the failing case |
Whichever row fires, the fix belongs in a DIFFERENT file/method than the
other two rows, so this table should be read for exactly one row per
capture before deciding where to touch code — the whole point of the
probe was to avoid fixing all three speculatively.

View file

@ -1860,6 +1860,34 @@ internal sealed class LiveEntityNetworkUpdateController
{ {
_motionRuntime.EnsureRemoteMotionBindings(rmState, aeForLand, update.Guid); _motionRuntime.EnsureRemoteMotionBindings(rmState, aeForLand, update.Guid);
} }
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
// capture the exact state HitGround is about to act on —
// see PhysicsDiagnostics.LogRemoteLanding for the field
// list and PhysicsDiagnostics.ProbeRemoteLandingEnabled
// for the discriminator table. TEMPORARY — strip once
// the live-test run has landed.
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
{
bool gravitySetForProbe = rmState.Body.HasGravity;
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLanding(
site: "controller",
guid: update.Guid,
airborneBefore: true,
gravitySet: gravitySetForProbe,
contact: rmState.Body.InContact,
onWalkable: rmState.Body.OnWalkable,
hasDefaultSink: rmState.Motion.DefaultSink is not null,
resolveIsOnGround: null,
sequencerStyle: aeForLand?.Sequencer?.CurrentStyle ?? 0,
sequencerMotion: aeForLand?.Sequencer?.CurrentMotion ?? 0);
if (!gravitySetForProbe)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
"controller", update.Guid);
}
}
ulong landingStateAuthorityVersion = ulong landingStateAuthorityVersion =
positionRecord.StateAuthorityVersion; positionRecord.StateAuthorityVersion;
rmState.Movement.HitGround(); rmState.Movement.HitGround();

View file

@ -155,6 +155,92 @@ public static class PhysicsDiagnostics
public static bool ProbeStickyEnabled { get; set; } public static bool ProbeStickyEnabled { get; set; }
= Environment.GetEnvironmentVariable("ACDREAM_PROBE_STICKY") == "1"; = Environment.GetEnvironmentVariable("ACDREAM_PROBE_STICKY") == "1";
/// <summary>
/// Bug A investigation (2026-08-04, live route 4a test — see
/// <c>docs/ISSUES.md</c> #32 and
/// <c>docs/research/2026-08-04-remote-landing-investigation.md</c>): a
/// PLAYER remote sometimes stays in the falling animation after landing,
/// then snaps to the grounded pose after a delay. Three hypotheses were
/// identified, with non-overlapping fixes, so this probe captures the
/// state needed to discriminate them at BOTH remote landing-detection
/// sites: the UpdatePosition landing block in
/// <c>LiveEntityNetworkUpdateController</c> (site=<c>controller</c>) and
/// the per-tick VectorUpdate landing branch in
/// <c>RuntimeRemotePhysicsUpdater</c> (site=<c>per-tick</c>).
///
/// <para>
/// When true, emits one <c>[remote-landing]</c> line per landing edge via
/// <see cref="LogRemoteLanding"/>, capturing: the airborne flag on entry,
/// whether the Gravity state bit is still set (the
/// <c>MotionInterpreter.HitGround</c> gate at
/// <c>MotionInterpreter.cs</c>:~2435 no-ops silently when it is NOT —
/// hypothesis 1), the Contact/OnWalkable transient bits, whether a
/// <c>DefaultSink</c> is bound (hypothesis 2 — nothing to dispatch
/// through), the per-tick site's <c>resolveResult.IsOnGround</c> (n/a at
/// the controller site, which has no resolver call), and the
/// sequencer's current style/motion id (hypothesis 3 — the sequencer
/// disagrees with what the re-apply should produce). A companion
/// <c>[remote-landing-gate]</c> line fires via
/// <see cref="LogRemoteLandingGateNoOp"/> whenever a landing site is
/// reached but Gravity is already clear, so the HitGround call about to
/// happen is a silent no-op — the single most valuable signal for
/// hypothesis 1.
/// </para>
///
/// <para>
/// Initial state from <c>ACDREAM_PROBE_REMOTE_LANDING=1</c>. TEMPORARY —
/// strip once the discriminating live-test capture has landed.
/// </para>
/// </summary>
public static bool ProbeRemoteLandingEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_REMOTE_LANDING") == "1";
/// <summary>
/// Emit one <c>[remote-landing]</c> line for a remote landing-detection
/// edge. Caller MUST guard with
/// <c>if (!ProbeRemoteLandingEnabled) return;</c> before calling.
/// <paramref name="resolveIsOnGround"/> is <see langword="null"/> at the
/// controller site (no per-frame resolver call at that edge).
/// </summary>
public static void LogRemoteLanding(
string site,
uint guid,
bool airborneBefore,
bool gravitySet,
bool contact,
bool onWalkable,
bool hasDefaultSink,
bool? resolveIsOnGround,
uint sequencerStyle,
uint sequencerMotion)
{
var ci = System.Globalization.CultureInfo.InvariantCulture;
string onGroundText = resolveIsOnGround.HasValue
? resolveIsOnGround.Value.ToString()
: "n/a";
Console.WriteLine(string.Format(ci,
"[remote-landing] site={0} guid=0x{1:X8} t={2} airborneBefore={3} " +
"gravitySet={4} contact={5} onWalkable={6} hasDefaultSink={7} " +
"resolveIsOnGround={8} seqStyle=0x{9:X8} seqMotion=0x{10:X8}",
site, guid, Environment.TickCount64, airborneBefore,
gravitySet, contact, onWalkable, hasDefaultSink, onGroundText,
sequencerStyle, sequencerMotion));
}
/// <summary>
/// Emit one <c>[remote-landing-gate]</c> line when a landing edge is
/// reached but the Gravity state bit is already clear, so the imminent
/// <c>MotionInterpreter.HitGround</c> call will silently no-op (the
/// gate at <c>MotionInterpreter.cs</c>:~2435) — hypothesis 1 for Bug A.
/// Caller MUST guard with
/// <c>if (!ProbeRemoteLandingEnabled) return;</c> before calling.
/// </summary>
public static void LogRemoteLandingGateNoOp(string site, uint guid)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[remote-landing-gate] site={site} guid=0x{guid:X8} t={Environment.TickCount64} NOOP gravityAlreadyClear=true"));
}
public static void LogCellSetBuild( public static void LogCellSetBuild(
uint seedCellId, uint seedCellId,
System.Numerics.Vector3 sphereCenter, System.Numerics.Vector3 sphereCenter,
@ -651,6 +737,7 @@ public static class PhysicsDiagnostics
ProbeSweptEnabled = false; ProbeSweptEnabled = false;
ProbeStepWalkEnabled = false; ProbeStepWalkEnabled = false;
ProbeTeleportEnabled = false; ProbeTeleportEnabled = false;
ProbeRemoteLandingEnabled = false;
// Side-channel fields // Side-channel fields
LastBspHitPoly = null; LastBspHitPoly = null;

View file

@ -528,6 +528,34 @@ internal sealed class RuntimeRemotePhysicsUpdater
// ~1 Hz re-emit. // ~1 Hz re-emit.
ulong landingStateAuthorityVersion = ulong landingStateAuthorityVersion =
record.StateAuthorityVersion; record.StateAuthorityVersion;
// Bug A investigation (2026-08-04, docs/ISSUES.md #32):
// capture the exact state HitGround is about to act on —
// see PhysicsDiagnostics.LogRemoteLanding for the field
// list and PhysicsDiagnostics.ProbeRemoteLandingEnabled
// for the discriminator table. TEMPORARY — strip once
// the live-test run has landed.
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeRemoteLandingEnabled)
{
bool gravitySetForProbe = rm.Body.HasGravity;
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLanding(
site: "per-tick",
guid: serverGuid,
airborneBefore: true,
gravitySet: gravitySetForProbe,
contact: rm.Body.InContact,
onWalkable: rm.Body.OnWalkable,
hasDefaultSink: rm.Motion.DefaultSink is not null,
resolveIsOnGround: resolveResult.IsOnGround,
sequencerStyle: sequencer?.CurrentStyle ?? 0,
sequencerMotion: sequencer?.CurrentMotion ?? 0);
if (!gravitySetForProbe)
{
AcDream.Core.Physics.PhysicsDiagnostics.LogRemoteLandingGateNoOp(
"per-tick", serverGuid);
}
}
rm.Movement.HitGround(); rm.Movement.HitGround();
if (!IsCurrentOwner( if (!IsCurrentOwner(
record, record,