From 325fee7cbb7ff0514f903a96583a5fd36c315174 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 30 Jul 2026 08:13:20 +0200 Subject: [PATCH 1/4] docs+test(physics): retire stale TS-1 row; file AD-53/AD-54 for its two acdream-only branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Campaign P Slice P2 step 1 (docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §2, §6 Step 1/2). The TS-1 register row (retail-divergence-register.md:238) described work that is already done: SpherePath.PrecipiceSlide, Transition.CliffSlide, and Transition.EdgeSlideAfterStepDownFailed are real, tested ports of retail's edge_slide -> precipice_slide/cliff_slide chain (pc:274316, pc:272397, pc:273001-273090). Its cited :1254 line was stale stepping-loop code the file moved past. The one real remaining gap (the back-probe fallback skipping retail's walkable_check_pos/localspace_sphere recache before its second precipice_slide call, pc:274318-274326 / 0050b4e0-0050b507) needed no production code change: a fresh read of SPHEREPATH::get_walkable_pos (0050a8f0), cache_localspace_sphere (0050c9d0), and set_walkable_check_pos (00509ce0) shows that machinery exists to re-project a sphere across retail's PER-CELL local coordinate frames. acdream's SpherePath.WalkableVertices and GlobalSphere are populated in UNIFIED WORLD SPACE at assignment time (SetWalkable/SetWalkableTransformed, SetCheckPos/RestoreCheckPos), so both operands BSPQuery.FindCrossedEdge compares are already commensurable -- retail's recache is a no-op correction under this architecture, and FindCrossedEdge never reads a sphere radius, so retail's walkable_scale radius correction has no acdream counterpart either. Documented in-code at the back-probe site with full citations, and pinned with EdgeSlideBackProbePrecipiceSlideTests: a walkable polygon rediscovered near GlobalCurrCenter, tested against GlobalSphere[0] restored to the original failed target, crosses the edge and slides -- it does not wedge into Collided (and the inverse case, standing inside the polygon with no edge crossed, correctly still returns Collided matching retail's own precipice_slide on a false find_crossed_edge). TS-1's other two flagged gaps are real acdream-only compensating branches, not retail reads, and get their own rows rather than being silently retired alongside it: - AD-53: CliffSlide's three-source reference-normal fallback chain (LastWalkablePlane -> LastKnownContactPlane -> world-up) vs retail's direct last_known_contact_plane.N use. A fresh read of last_known_contact_plane's maintenance (pc:272659-272668) confirms retail overwrites it unconditionally every validate_transition pass, including with a steep plane -- so the fallback chain compensates for AP-4's incomplete OnWalkable bookkeeping, not a retail-matching read. - AD-54: the walkable-steepness reroute to CliffSlide before PrecipiceSlide when the stored walkable polygon itself is steeper than FloorZ. Retail's raw edge_slide has no such branch; the permissive LandingZ acceptance that makes this state reachable IS retail-faithful (TS-4's own BSPTREE::find_collisions citation), but whether retail's outer transitional_insert retry loop absorbs the resulting COLLIDED_TS some other way is not yet independently verified -- flagged open in the row. Physics test suite: 1836 passed, 1 skipped (D4, unrelated to this change). Co-Authored-By: Claude Opus 5 --- .../retail-divergence-register.md | 19 +-- src/AcDream.Core/Physics/TransitionTypes.cs | 62 +++++++ .../EdgeSlideBackProbePrecipiceSlideTests.cs | 151 ++++++++++++++++++ 3 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 3bf18d91..42f7d251 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -62,10 +62,12 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 44 rows (AD-52 filed 2026-07-29 at Campaign N slice N6 — the fragment-assembler 60 s partial TTL + completed-sequence ring; AD-51 filed 2026-07-29 at Campaign N slice N4 — the reclaimed-word pool for ACE's fresh-sequence cleartext RejectRetransmit; AD-50 filed 2026-07-29 at Campaign N slice N2 — the inbound-watermark ACE init; AD-49 stays reserved for Campaign N §5's blob-layer ordering deferral, filed when its slice lands; AD-47 and AD-48 filed 2026-07-29 at Campaign V slice V11 — the MSAA sample-position and present-pacing rows the campaign's risk register scheduled for the GL deletion; AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover) +## 2. Adaptation (AD) — 46 rows (AD-53/AD-54 filed 2026-07-30 at Campaign P Slice P2, split out of the retired TS-1 row — CliffSlide's three-source reference-normal fallback chain and the walkable-steepness reroute to CliffSlide; AD-52 filed 2026-07-29 at Campaign N slice N6 — the fragment-assembler 60 s partial TTL + completed-sequence ring; AD-51 filed 2026-07-29 at Campaign N slice N4 — the reclaimed-word pool for ACE's fresh-sequence cleartext RejectRetransmit; AD-50 filed 2026-07-29 at Campaign N slice N2 — the inbound-watermark ACE init; AD-49 stays reserved for Campaign N §5's blob-layer ordering deferral, filed when its slice lands; AD-47 and AD-48 filed 2026-07-29 at Campaign V slice V11 — the MSAA sample-position and present-pacing rows the campaign's risk register scheduled for the GL deletion; AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| +| AD-53 | `Transition.CliffSlide`'s reference-normal cross-product operand tries THREE sources in priority order (`LastWalkablePlane` if `Normal.Z >= FloorZ`, then `LastKnownContactPlane` at the same threshold, then world-up `UnitZ`); retail's `CTransition::cliff_slide` uses `this->collision_info.last_known_contact_plane.N` directly, with no fallback chain | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CliffSlide`, the `referenceNormal`/`refSource` selection above the cross-product) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). A fresh read of `last_known_contact_plane`'s own maintenance (pc:272659-272668) confirms retail overwrites it unconditionally from `contact_plane` every `validate_transition` pass — including with a steep plane — so retail keeps NO separately-preserved flat-ground history there either; this fallback chain is a genuine acdream invention, not a retail-matching read. Kept because it compensates for AP-4's incomplete `OnWalkable` bookkeeping (L.4-cliffslide-fallback, 2026-04-30): without it, `cross(currentSteep, lastKnownSteep)` degenerates to zero after >1 frame on a continuous steep slope, and CliffSlide returns `OK` (no deflection) instead of downhill drift — the "stay on the roof" wedge the L.4 session fought | If AP-4's `OnWalkable` reordering is ever completed/removed, `last_known_contact_plane` should carry the same information retail's does and this fallback chain becomes unneeded ballast (or, worse, silently picks a stale `LastWalkablePlane` over the now-correct current one) — re-audit together with AP-4 | `CTransition::cliff_slide` pc:272397 (0050a6d0); `last_known_contact_plane` maintenance pc:272659-272668 (~0050ad07); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #2 | +| AD-54 | `Transition.EdgeSlideAfterStepDownFailed` reroutes to `CliffSlide` instead of `PrecipiceSlide` when the stored walkable polygon itself is steeper than `FloorZ` (`sp.WalkablePlane.Normal.Z < PhysicsGlobals.FloorZ`); retail's raw `SPHEREPATH::edge_slide` has no steepness branch here — `if (walkable != null) { ... precipice_slide(...) }` unconditionally | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed`, the `L.4-walkable-steep` block) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). The permissive `LandingZ` walkable-acceptance threshold that lets a steep roof become "walkable" in the first place IS confirmed retail-faithful (`BSPTREE::find_collisions` unconditional `walkable_allowance = LandingZ`, pc:323740-323783, TS-4's own citation) — so a steep-roof walkable polygon is a real state retail also reaches. What is NOT independently verified is whether retail's outer `transitional_insert` caller absorbs a same-polygon-standing `COLLIDED_TS` from `precipice_slide` (its raw `find_crossed_edge` returning false while standing on, not crossing, the polygon) some other way that avoids the acdream "stuck in a Collided revert loop" this reroute prevents | If retail's outer retry loop turns out to already handle the no-crossed-edge-while-standing-on-a-steep-poly case without a reroute, this branch is an unnecessary compensating layer that could route a genuinely PrecipiceSlide-bound case (a shallow polygon edge that happens to sit at exactly `FloorZ`) into CliffSlide instead | `SPHEREPATH::edge_slide` pc:273001-273090 (0050b3d0, direct walkable branch quoted at pc:364-370 in the P2 research doc); `BSPTREE::find_collisions` pc:323740-323783 (0053a730, unconditional `LandingZ`); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #3 | | AD-46 | **LIVE. Reframed at Campaign V slice V11 (2026-07-29), when GL was deleted and the comparison that discovered this row ceased to exist.** Dense alpha-blended distant scenery (the treeline) may read slightly denser than retail's, because the anisotropic TAP PATTERN is implementation-defined and acdream's Vulkan driver does not tap identically to retail's D3D9 one. Both request the same sampler state — trilinear, clamp-and-repeat, the device's maximum anisotropy. **What changed at V11 is only the left-hand side of the comparison**: this was measured GL-vs-Vulkan (~15% of the pixels in the band), and it is now a Vulkan-vs-retail question against the D3D oracle in the last column. The measurement below is retained as the evidence that the residual is a tap pattern and not a bug, even though one of its two arms no longer exists. | `src/AcDream.App/Rendering/Wb/WorldTextureArray.cs` (`RhiWorldTextureArray.WorldArrayAnisotropy`); measured in plan §5.5.19, reframed §5.5.24 | Not assumed — narrowed by measurement while both backends still existed, on an offline capture with no session, no entities and both clocks pinned. Anisotropy 1 → 41,509 differing pixels in the tree band; anisotropy 16 (GL's value, and retail's `m_D3DCaps.MaxAnisotropy`) → 22,266, and the rest of the frame fell to 497 px of 563,200, i.e. 8.8e-04, inside the campaign's 0.001 threshold. The residual was not a sub-pixel shift (an integer shift search found none), not a sharpness change (high-frequency energy matched within 5%), and not depth precision (forcing Vulkan's window-depth range to GL's compressed [0.5, 1] moved it by 3%). Monotone improvement toward GL's own anisotropy with no knob left is what made it a driver property rather than a bug. | Distant foliage shimmers or reads denser than retail's. The class is confined to alpha-blended dense overlap: opaque terrain, roofs, walls, water, statics, the character and the whole retained UI are inside threshold. **Now unfalsifiable by self-differential** — with GL gone, the only way to retire this row is a side-by-side against the retail client, not against another acdream backend. | `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`, whose `SetSamplerState(stage, 0xA /* D3DSAMP_MAXANISOTROPY */, m_D3DCaps.MaxAnisotropy)` at `0x005a4230` is the value acdream requests | | AD-47 | **Filed at Campaign V slice V11 (2026-07-29); the campaign's risk register scheduled this row here.** Multisample resolve sample POSITIONS are unspecified by both the Vulkan and D3D9 specifications, so acdream's MSAA-on silhouette edges do not match retail's pixel-for-pixel even at the same sample count. acdream's strict pixel gates therefore run with MSAA forced OFF on every arm, and MSAA-on gets only a relaxed visual smoke. | `src/AcDream.App/RuntimeOptions.cs` (`ACDREAM_MSAA_SAMPLES`); forced to 0 in `tools/run-offline-pixel-gate.ps1` | Measured, not assumed: plan §5.5.16 compared two backends at 4x and found **8.83% of the frame differing — 81,359 px of 921,600 — essentially all of it hugging foliage and silhouette edges**, which is ninety-fold over the 0.001 gate threshold. That is two implementations' sample patterns, not a renderer divergence, which is why forcing MSAA off is what makes the remaining difference attributable rather than a threshold relaxation. | Edge quality on thin geometry (fence rails, foliage, distant railings) differs from retail at the sub-pixel level whenever MSAA is on, which is the ordinary player configuration. Because the gates run MSAA off, **a real regression confined to the multisample path would not be caught by them** — that is the actual exposure this row records. | D3D9 `D3DRS_MULTISAMPLEANTIALIAS` / `D3DMULTISAMPLE_TYPE` as set by `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`; retail's sample pattern is the driver's, exactly as ours is | | AD-48 | **Filed at Campaign V slice V11 (2026-07-29).** Presentation is paced by the Vulkan swapchain present mode (FIFO, i.e. VSync) or by a refresh-rate software pacer when uncapped, rather than by retail's D3D9 `Present` with its own frame-rate limiter. Frame delivery cadence, and therefore input-to-photon latency, is a property of our present path rather than a port of retail's. | `src/AcDream.App/RuntimeOptions.cs:98-100`; `src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs` | Retail's limiter and ours both bound the frame rate to the display; the simulation is fixed-step and clock-driven, so gameplay timing does not ride on presentation cadence. The uncapped path exists for measurement and is not the shipping default. | A pacing mismatch shows up as judder or input latency that differs from retail's feel without any visual difference in a captured frame — invisible to every pixel gate by construction. Issue **#235** (the capped/RDP jump-presentation cadence alias) is the known live instance of this class. | D3D9 `IDirect3DDevice9::Present`; retail's frame limiter in `RenderDeviceD3D` | @@ -231,11 +233,11 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-125 | Transport control packets (the 2.0 s cumulative AckSequence and the 0.6 s RequestRetransmit) are emitted STANDALONE; retail piggybacks optional headers onto queued outbound packets first-fit (`FlowQueue::CoalesceData @ 0x00547740`, invoked at `TransmitNewPackets @ 0x00547A6E`), and `EnqueueNaks` hands the NAK to `PacketController::EnqueueOptionalHeader @ 0x00543C84` rather than emitting directly. | `src/AcDream.Core.Net/Transport/AckNakScheduler.cs` (`EmitCumulativeAck`, `EmitNakRequest`) | ACE honours a RequestRetransmit ONLY when EncryptedChecksum is absent (NetworkSession.cs:283-284) — a retail-style piggyback onto a sequenced packet encrypts the NAK and ACE silently ignores it, making S2C loss unrecoverable; ACE likewise advances its client-sequence watermark on any packet whose flags are not exactly AckSequence (:474-476), so coalesced control content on a borrowed sequence risks skipping a real packet. Standalone exact-flag emission is the only ACE-safe shape; it also keeps reliable packets free of optional headers, making the resend cache strip provably a no-op. | Slightly higher C2S datagram count than retail (one extra small packet per 2.0 s / per NAK window); marginally more loss exposure for the control packets themselves on a metered path. | `FlowQueue::CoalesceData @ 0x00547740`; `SharedNet::EnqueuePak @ 0x00543B10`; `SharedNet::EnqueueNaks @ 0x00543BD0`; ACE `NetworkSession.cs:283-284,:342-343,:474-476` | | AP-126 | One monotonic Stopwatch-backed clock (`TransportClock`) drives every transport gate (2.0 s ack, 0.6 s NAK, 0.333 s handshake retry, 0.5 s interval, 5 s assembler sweep); retail splits gates between `Timer::cur_time` (server-adjusted) and `Timer::local_time`. | `src/AcDream.Core.Net/Transport/TransportClock.cs` | The cur/local split only matters for gates that must track server clock adjustments; none of the ported gates semantically depend on server time — they are local cadences. A single injectable source also gives the virtual-clock test seam every conformance suite relies on. | A future port of a genuinely server-clock-relative gate could silently use the wrong clock if it reuses TransportClock without checking this row. | `SharedNet::EnqueuePak @ 0x00543B10` (cur_time); `ClientNet::ProcessConnection @ 0x00545450` (local_time for the 140 s check) | -## 4. Temporary stopgap (TS) — 43 active rows (TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) +## 4. Temporary stopgap (TS) — 42 active rows (TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port, see the AD-53/AD-54 rows for the two compensating branches it left registered; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| -| TS-1 | PrecipiceSlide context missing — conservative stop-at-edge instead of retail's EdgeSlide → PrecipiceSlide / CliffSlide | `src/AcDream.Core/Physics/TransitionTypes.cs:1254` | Awaiting the next L.2c slice; a diagnostic records which ingredient (precipice context / steep plane / EdgeSlide flag) is missing | Player stops dead at precipice edges where retail slides along/over — visible mismatch at cliff and roof edges | retail EdgeSlide → PrecipiceSlide chain | +| ~~TS-1~~ | **RETIRED 2026-07-30 (Campaign P Slice P2) — the row was stale, not the code.** The cited `:1254` line is unrelated stepping-loop code; the file moved substantially since the row was written. Retail's `EdgeSlide → PrecipiceSlide / CliffSlide` chain is already a real, tested port: `SpherePath.PrecipiceSlide` (`TransitionTypes.cs:943-970`, retail `SPHEREPATH::precipice_slide` pc:274316), `Transition.CliffSlide` (`:2080-2164`, retail `CTransition::cliff_slide` pc:272397, return-value mapping verified against `acclient.h:6100-6108`), and `Transition.EdgeSlideAfterStepDownFailed` (`:1907-2078`, mirrors `CTransition::edge_slide` pc:273001-273090). The one real gap (back-probe fallback skipping retail's `walkable_check_pos`/`localspace_sphere` recache, pc:274318-274326) needed no code change: acdream's `WalkableVertices`/`GlobalSphere` are populated in unified world space at assignment time (`SetWalkable`/`SetWalkableTransformed`, `SetCheckPos`/`RestoreCheckPos`), so both operands `BSPQuery.FindCrossedEdge` compares are already commensurable — retail's per-cell local-frame reprojection is a no-op correction here. Documented in-code at the back-probe site and pinned by `EdgeSlideBackProbePrecipiceSlideTests`. The chain's two acdream-only compensating branches (CliffSlide's three-source reference-normal fallback; the walkable-steepness reroute to CliffSlide before PrecipiceSlide) are real, non-retail additions — filed as AD-53 / AD-54 rather than folded into this row. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SpherePath.PrecipiceSlide`, `Transition.CliffSlide`, `Transition.EdgeSlideAfterStepDownFailed`); `tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs` | — | — | `SPHEREPATH::precipice_slide` pc:274316 (0050cc80); `CTransition::cliff_slide` pc:272397 (0050a6d0); `CTransition::edge_slide` pc:273001-273090 (0050b3d0); `SPHEREPATH::get_walkable_pos`/`cache_localspace_sphere`/`set_walkable_check_pos` pc:274318-274326 (0050a8f0/0050c9d0/00509ce0); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2, §6 Step 1 | | TS-4 | Path-6 steep-poly slide-tangent shortcut: airborne hits on >FloorZ polys skip retail's SetCollide → Path-4 → ContactPlane landing chain, returning Slid in place. **Includes a `SetSlidingNormal` write at both sites** — retail's BSP layer never writes `collision_info.sliding_normal` (only `validate_transition` 0x0050ac21 does; the #137 mechanism-2 class), so on transition success the steep-face normal persists to the body and seeds the next frame | `src/AcDream.Core/Physics/BSPQuery.cs` (Path-6 steep branches, `worldNormal.Z < FloorZ`) | Deliberate deviation: our faithful port DID wedge (missing step_up_slide / cliff_slide details on grounded-steep); validated against the 2026-04-30 retail cdb trace (retail body didn't wedge). Filed L.5+ for retail-strict | Airborne steep contact never commits Contact / lands as retail — roof-bounce trajectories, landing events, grounded-steep transitions diverge; a persisted steep-face normal can absorb an exactly-anti-parallel next-frame push (#137 wedge class) until an oblique input clears it | `BSPTREE::find_collisions` SetCollide pc:323783-323821 | | TS-5 | `CanJump` always true — burden/stamina gating deferred (stat plumbing incomplete pre-M2). R3-W3 extends this row: `IWeenieObject.JumpStaminaCost`/`PlayerWeenie.JumpStaminaCost` are new (feeding `jump_is_allowed`'s verbatim stamina-refusal branch) and are ALSO always-affordable/cost-0 stubs for the same reason | `src/AcDream.Core/Physics/PlayerWeenie.cs:44` (`CanJump`), `:52` (`JumpStaminaCost`, R3-W3) | Marked deferred; harmless until stats matter | Client launches jumps retail refuses (exhausted/overburdened) — server rejection / rubber-band; divergent jump availability vs retail muscle memory | CMotionInterp jump path stamina/burden inquiry; `jump_is_allowed` 0x005282b0 `JumpStaminaCost` vtable +0x44 | | TS-6 | Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp | `src/AcDream.Core/World/WeatherState.cs:200` | Decomp research verified the sky loop never reads `DefaultPesObjectId`; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found | Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) | FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings) | @@ -310,12 +312,11 @@ WITH that phase, not before. 1. **TS-27 — INBOUND retransmit handling** — the outbound sent-packet cache + resend landed with Campaign N Slice N1 (2026-07-29, class-doc gap list fixed same commit); the inbound sequence-aligned ISAAC + client NAK emission (N2/N4) remain the hard blocker for non-loopback play — one lost S2C packet still deafens the session permanently. 2. **TS-4 — Path-6 steep slide-tangent shortcut** — landing/contact state diverges on every airborne-steep hit; the L.5+ retail-strict followup is already filed with the missing-ingredient analysis. 3. **UN-1 — CheckOtherCells iteration order** — behavior-bearing halt order with a log-cosmetics justification; trivial to fix (iterate CELLARRAY build order, sort only in probe output). -4. **TS-1 — PrecipiceSlide stop-at-edge** — visible movement mismatch at every cliff/roof edge; diagnostic already records which ingredient is missing. -5. **UN-6 — 200 ms ConnectResponse sleep** — unexplained constant on every login with an intermittent-failure shape; either find the ACE race and cite it, or replace with an acknowledged-ready check. -6. **UN-4 — GfxObj sides/negative-surface logic** — diagnose against the retail-cited CellStruct interpretation on a known double-sided GfxObj; promote to AP with a citation or align it. -7. **TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12)** — vitals wrong for the whole session after any buff; parser shape is known from holtburger. -8. **TS-55 — AdminEnvirons fog/radar presentation** — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and `0x270F` together. -9. **TS-19 — Legacy ChaseCamera deletion** — already marked "pending the follow-up deletion commit"; its continued existence can mask or manufacture flap symptoms during debugging. +4. **UN-6 — 200 ms ConnectResponse sleep** — unexplained constant on every login with an intermittent-failure shape; either find the ACE race and cite it, or replace with an acknowledged-ready check. +5. **UN-4 — GfxObj sides/negative-surface logic** — diagnose against the retail-cited CellStruct interpretation on a known double-sided GfxObj; promote to AP with a citation or align it. +6. **TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12)** — vitals wrong for the whole session after any buff; parser shape is known from holtburger. +7. **TS-55 — AdminEnvirons fog/radar presentation** — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and `0x270F` together. +8. **TS-19 — Legacy ChaseCamera deletion** — already marked "pending the follow-up deletion commit"; its continued existence can mask or manufacture flap symptoms during debugging. **Phase-gated (do WITH the phase, flagged here so they aren't forgotten):** M2 combat must land TS-5 (CanJump gating), TS-23 (PK bits), TS-25 diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 94771c05..0d3624ea 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -1985,6 +1985,23 @@ public sealed class Transition // PrecipiceSlide. CliffSlide deflects motion along the ridge // between current-steep and last-known-walkable; gravity then // produces visible downhill drift. + // + // TS-1 gap #3 (register AD-54, Campaign P Slice P2 2026-07-30): + // retail's raw SPHEREPATH::edge_slide has NO steepness branch here + // — `if (walkable != null) { ... precipice_slide(...) }` unconditionally + // (acclient_2013_pseudo_c.txt:364-370 per the P2 research quote). The + // LandingZ permissive acceptance itself IS retail-faithful — confirmed + // by TransitionalInsert's own Path-4 Collide branch + // (TransitionTypes.cs, `DoCheckWalkable(PhysicsGlobals.LandingZ, engine)` + // above) and TS-4's BSPTREE::find_collisions read + // (pc:323740-323783: `sphere_path.walkable_allowance = LandingZ` + // unconditionally, no slope test) — so a steep roof really is + // "walkable" in retail too. What is NOT independently verified from + // the raw decomp is whether retail's OUTER caller (transitional_insert) + // absorbs a same-polygon-standing Collided from precipice_slide via + // its own retry loop rather than needing this reroute; see + // docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §2 + // gap #3. if (sp.WalkablePlane.Normal.Z < PhysicsGlobals.FloorZ) { var cliffPlane = sp.WalkablePlane; @@ -2015,6 +2032,32 @@ public sealed class Transition // Retail back-probes from the current sphere center to rediscover the // walkable polygon we just left, then restores the failed candidate and // runs precipice_slide against that polygon. + // + // TS-1 gap #1 research (Campaign P Slice P2, 2026-07-30, + // docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §2): + // retail's SPHEREPATH::edge_slide back-probe branch re-caches + // walkable_check_pos/localspace_sphere here — SPHEREPATH::get_walkable_pos + // (0050a8f0) -> SPHEREPATH::cache_localspace_sphere (0050c9d0) -> + // SPHEREPATH::set_walkable_check_pos (00509ce0), acclient_2013_pseudo_c.txt + // :274318-274326 (0050b4e0-0050b507) — before its second precipice_slide + // call. That machinery re-projects the restored check_pos sphere into the + // walkable polygon's OWN per-cell local frame via Position::localtolocal, + // and precipice_slide itself then applies a LandDefs::get_block_offset + // landblock correction (pc:274341) before testing find_crossed_edge. + // Deliberately NOT ported here: acdream's SpherePath.WalkableVertices/ + // WalkablePlane are populated in UNIFIED WORLD SPACE at assignment time + // (SetWalkable/SetWalkableTransformed above bake worldOrigin+scale in + // immediately), and GlobalSphere is likewise always world-space + // (SetCheckPos/RestoreCheckPos). Both operands BSPQuery.FindCrossedEdge + // compares are therefore already commensurable with no landblock/cell + // reprojection needed — retail's local-frame recache is a no-op + // correction under this architecture, and FindCrossedEdge never reads a + // sphere radius, so retail's walkable_scale radius correction has no + // acdream counterpart either. Pinned by + // EdgeSlideBackProbePrecipiceSlideTests (direct SpherePath.PrecipiceSlide + // state test): a walkable polygon rediscovered near GlobalCurrCenter, + // tested against GlobalSphere[0] restored to the original failed target, + // crosses the edge and slides — it does not wedge into Collided. Vector3 backToCurrent = sp.GlobalCurrCenter[0].Origin - sp.GlobalSphere[0].Origin; sp.AddOffsetToCheckPos(backToCurrent); @@ -2048,6 +2091,25 @@ public sealed class Transition // deflection. Using LastWalkable preserves the prior flat-ground // plane across continuous-slope frames; world-up gives a guaranteed // non-zero deflection when no walkable history exists at all. + // + // TS-1 gap #2 (register AD-53, Campaign P Slice P2 2026-07-30): retail's + // raw CTransition::cliff_slide (pc:272397, 0050a6d0) uses + // this->collision_info.last_known_contact_plane.N DIRECTLY as the second + // cross-product operand — no fallback chain. Confirmed by a fresh read of + // last_known_contact_plane's own maintenance + // (acclient_2013_pseudo_c.txt:272659-272668, pc ~0050ad07): retail + // overwrites last_known_contact_plane from contact_plane UNCONDITIONALLY + // on every validate_transition pass, the same "gets overwritten by + // whatever's current, including a steep plane" behavior this file's + // ContactPlane/LastKnownContactPlane tracking already has — retail does + // NOT maintain a separately-preserved flat-ground history there either. + // This three-source chain (LastWalkablePlane -> LastKnownContactPlane -> + // UnitZ) is therefore a genuine acdream invention, not a retail-matching + // read — kept because it compensates for AP-4's incomplete OnWalkable + // bookkeeping (see DO-NOT-RETRY item 9 in + // docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §0) + // and removing it reintroduces the degenerate-cross "stay on the roof" + // wedge the L.4 session (2026-04-30) fought. See that doc's §2 gap #2. Vector3 referenceNormal; string refSource; if (sp.HasLastWalkablePolygon && sp.LastWalkablePlane.Normal.Z >= PhysicsGlobals.FloorZ) diff --git a/tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs b/tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs new file mode 100644 index 00000000..726649b8 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs @@ -0,0 +1,151 @@ +using System.Numerics; +using AcDream.Core.Physics; +using Xunit; +using Plane = System.Numerics.Plane; + +namespace AcDream.Core.Tests.Physics; + +/// +/// Campaign P Slice P2, TS-1 gap #1 (2026-07-30 research pass, +/// docs/research/2026-07-30-response-layer-edge-family-pseudocode.md +/// §2). Pins the specific state Transition.EdgeSlideAfterStepDownFailed's +/// back-probe fallback (TransitionTypes.cs:2015-2031) hands to +/// : a walkable polygon rediscovered +/// near GlobalCurrCenter (the last-known-good grounded position), +/// tested against GlobalSphere[0] restored to the ORIGINAL failed +/// move target (off the polygon's edge) after RestoreCheckPos(). +/// +/// +/// Retail's SPHEREPATH::edge_slide back-probe branch +/// (acclient_2013_pseudo_c.txt:274316-274326, 0050b4e0-0050b507) +/// re-caches walkable_check_pos/localspace_sphere from +/// SPHEREPATH::get_walkable_pos (0050a8f0) via +/// SPHEREPATH::cache_localspace_sphere (0050c9d0) and +/// SPHEREPATH::set_walkable_check_pos (00509ce0) before calling +/// precipice_slide a second time. That machinery exists to solve a +/// coordinate-FRAME problem: retail's walkable polygon and +/// check_pos are each expressed relative to a PER-CELL local frame +/// (cache_localspace_sphere re-projects one into the other's frame via +/// Position::localtolocal, and precipice_slide itself applies a +/// LandDefs::get_block_offset landblock correction, pc:274341). +/// +/// +/// +/// acdream's / +/// are populated in UNIFIED WORLD SPACE at assignment time (see +/// SpherePath.SetWalkable/SetWalkableTransformed, +/// TransitionTypes.cs:667-739, which bake worldOrigin and +/// scale in immediately), and is +/// likewise always world-space (SpherePath.SetCheckPos/ +/// RestoreCheckPos, TransitionTypes.cs:621-650). Both operands +/// compares are therefore ALREADY +/// commensurable without any recache step — retail's local-frame +/// re-projection is a no-op correction in acdream's flat-world-space design. +/// also never reads a sphere radius +/// (only sphereCenter), so retail's radius/walkable_scale +/// correction has no acdream counterpart to begin with. +/// +/// +/// +/// This test does not add a recache step (there is nothing for it to +/// correct in this architecture); it instead PINS the claim: given exactly +/// the field values the back-probe fallback produces (a world-space walkable +/// polygon near GlobalCurrCenter, a restored GlobalSphere[0] at +/// the original off-edge target), PrecipiceSlide must find the +/// crossed edge and slide — not wedge into Collided. +/// +/// +public class EdgeSlideBackProbePrecipiceSlideTests +{ + /// + /// Back-probe re-discovers a flat platform's edge polygon near the last + /// known good center; the restored (failed) target sphere sits just past + /// the +X edge. PrecipiceSlide must cross that edge and slide, matching + /// retail's post-recache precipice_slide result (ADJUSTED_TS/SLID_TS, + /// never a stuck COLLIDED_TS). + /// + [Fact] + public void PrecipiceSlide_BackProbeState_CrossesEdge_DoesNotWedge() + { + var transition = new Transition(); + var sp = transition.SpherePath; + + // Last known good grounded center — where the back-probe offset + // points back toward (retail: global_curr_center). + sp.GlobalCurrCenter[0].Origin = new Vector3(0f, 0f, 1f); + sp.GlobalCurrCenter[0].Radius = 0.5f; + + // The walkable polygon the back-probe's DoStepDown rediscovered near + // that center: a flat 2x2 platform, top face at Z=1. + var plane = new Plane(Vector3.UnitZ, -1f); + sp.SetWalkable( + plane, + new[] + { + new Vector3(-1f, -1f, 1f), + new Vector3(1f, -1f, 1f), + new Vector3(1f, 1f, 1f), + new Vector3(-1f, 1f, 1f), + }, + Vector3.UnitZ); + + // GlobalSphere[0] after RestoreCheckPos(): the ORIGINAL failed move + // target, just past the platform's +X edge (off the polygon, over + // open air) — exactly what the back-probe fallback hands to + // PrecipiceSlide once retail's edge_slide restores check_pos. + sp.CheckPos = new Vector3(1.2f, 0f, 1f); + sp.GlobalSphere[0].Origin = sp.CheckPos; + sp.GlobalSphere[0].Radius = 0.5f; + + var result = sp.PrecipiceSlide(transition); + + Assert.NotEqual(TransitionState.Collided, result); + Assert.True( + result is TransitionState.Slid or TransitionState.Adjusted or TransitionState.OK, + $"Back-probe PrecipiceSlide must slide/adjust across the found edge, not wedge; got {result}."); + + // The walkable context is consumed (retail: this->walkable = nullptr + // inside precipice_slide before slide_sphere runs). + Assert.False(sp.WalkableValid); + } + + /// + /// Sanity inverse: when the restored target sphere is still WELL INSIDE + /// the rediscovered polygon (no edge crossed — e.g. the back-probe found + /// the same ground the mover is standing on), retail's raw + /// precipice_slide returns COLLIDED_TS + /// (acclient_2013_pseudo_c.txt:274322-274326: eax==0 → walkable + /// = nullptr; return 2). Confirms the "no wedge" claim above is about + /// the edge-crossing case specifically, not a blanket "never Collided." + /// + [Fact] + public void PrecipiceSlide_NoEdgeCrossed_ReturnsCollided_MatchingRetail() + { + var transition = new Transition(); + var sp = transition.SpherePath; + + sp.GlobalCurrCenter[0].Origin = new Vector3(0f, 0f, 1f); + sp.GlobalCurrCenter[0].Radius = 0.5f; + + var plane = new Plane(Vector3.UnitZ, -1f); + sp.SetWalkable( + plane, + new[] + { + new Vector3(-1f, -1f, 1f), + new Vector3(1f, -1f, 1f), + new Vector3(1f, 1f, 1f), + new Vector3(-1f, 1f, 1f), + }, + Vector3.UnitZ); + + // Restored target well inside the polygon — no edge crossed. + sp.CheckPos = new Vector3(0.1f, 0f, 1f); + sp.GlobalSphere[0].Origin = sp.CheckPos; + sp.GlobalSphere[0].Radius = 0.5f; + + var result = sp.PrecipiceSlide(transition); + + Assert.Equal(TransitionState.Collided, result); + } +} From 4f7e29f7cf29fff1aa09d05cca8f35d6ccbc5a28 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 30 Jul 2026 08:17:32 +0200 Subject: [PATCH 2/4] fix(physics): AP-7 - port calc_friction's retail 0.25f threshold; retire AP-7, file AD-55 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Campaign P Slice P2 step 3 (docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1, §6 Step 5). The named retail decomp (CPhysicsObj::calc_friction, pseudo-C:276694-276822, 0050ee70) independently re-confirms the 0.25f threshold (derived twice, once per BN-rendered branch); the in-code claim that "the decompile uses 0.0" traced to the older, unnamed FUN_0050f940 Ghidra chunk at a different address -- per CLAUDE.md the named decomp wins. calc_friction now reads angle = dot(Velocity, GroundNormal); if (angle >= 0.25f) return; then unconditionally removes the normal-aligned velocity component, then applies the existing (already-present but previously unreachable) PhysicsState.Sledding-gated friction overrides. The BN-rendered "two duplicated branches" around the state check is adopted as a single linear function matching ACE's PhysicsObj.calc_friction shape -- the branch split is most likely a BN decompiler artifact around one `if (state & SLEDDING_PS)` block (ACE-derived, Ghidra-verify; low implementation risk either way since ACE's reading is adopted regardless). Why this doesn't repeat the reverted 2026-04-30 L.3c regression (naive 0.0 -> 0.25f bump dropped forward locomotion 3 -> 0.16 m/s): that test predates the 2026-07-17 R6 "local player animation-owned grounded movement" landing. PlayerMovementController (Runtime/Gameplay, out of this slice's scope) zeroes Velocity.X/Y to exactly zero every tick before calc_friction runs whenever animation root motion drives the walk, so friction has nothing horizontal left to hammer on the production graphical local-player path. Pinned at the PhysicsBody level (the only file this slice may touch) by GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests. The headless/get_state_velocity path and remote/NPC movers still feed real velocity into this function and remain the ones to watch if a similar regression resurfaces there -- flagged in the retired AP-7 row for future sessions working in Runtime/Gameplay. Left an open, explicitly-flagged discrepancy: the raw decomp's Sledding slope-flatness test computes cos(10 deg) (~0.984808) while ACE's port (and acdream's prior dead code) compares GroundNormal.Z > 0.99999536f (~0.175 deg from flat) -- physically different tests, neither confirmed this pass (Ghidra MCP down). Kept 0.99999536f provisionally (least churn) and filed AD-55 for just that constant rather than silently picking one. Register: AP-7 retired with a corrected citation; AD-55 filed for the cos(10 deg) question. Core.Tests: 3916 passed, 2 skipped (both pre-existing and unrelated), 0 failed. Co-Authored-By: Claude Opus 5 --- .../retail-divergence-register.md | 7 +- src/AcDream.Core/Physics/PhysicsBody.cs | 94 +++++++++++----- .../Physics/PhysicsBodyTests.cs | 103 +++++++++++++++++- 3 files changed, 174 insertions(+), 30 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 42f7d251..1dcd4ba7 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -62,12 +62,13 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 46 rows (AD-53/AD-54 filed 2026-07-30 at Campaign P Slice P2, split out of the retired TS-1 row — CliffSlide's three-source reference-normal fallback chain and the walkable-steepness reroute to CliffSlide; AD-52 filed 2026-07-29 at Campaign N slice N6 — the fragment-assembler 60 s partial TTL + completed-sequence ring; AD-51 filed 2026-07-29 at Campaign N slice N4 — the reclaimed-word pool for ACE's fresh-sequence cleartext RejectRetransmit; AD-50 filed 2026-07-29 at Campaign N slice N2 — the inbound-watermark ACE init; AD-49 stays reserved for Campaign N §5's blob-layer ordering deferral, filed when its slice lands; AD-47 and AD-48 filed 2026-07-29 at Campaign V slice V11 — the MSAA sample-position and present-pacing rows the campaign's risk register scheduled for the GL deletion; AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover) +## 2. Adaptation (AD) — 47 rows (AD-55 filed 2026-07-30 at Campaign P Slice P2, split out of the retired AP-7 row — the open cos(10°)-vs-0.99999536f Sledding slope-flatness constant; AD-53/AD-54 filed the same slice, split out of the retired TS-1 row — CliffSlide's three-source reference-normal fallback chain and the walkable-steepness reroute to CliffSlide; AD-52 filed 2026-07-29 at Campaign N slice N6 — the fragment-assembler 60 s partial TTL + completed-sequence ring; AD-51 filed 2026-07-29 at Campaign N slice N4 — the reclaimed-word pool for ACE's fresh-sequence cleartext RejectRetransmit; AD-50 filed 2026-07-29 at Campaign N slice N2 — the inbound-watermark ACE init; AD-49 stays reserved for Campaign N §5's blob-layer ordering deferral, filed when its slice lands; AD-47 and AD-48 filed 2026-07-29 at Campaign V slice V11 — the MSAA sample-position and present-pacing rows the campaign's risk register scheduled for the GL deletion; AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| | AD-53 | `Transition.CliffSlide`'s reference-normal cross-product operand tries THREE sources in priority order (`LastWalkablePlane` if `Normal.Z >= FloorZ`, then `LastKnownContactPlane` at the same threshold, then world-up `UnitZ`); retail's `CTransition::cliff_slide` uses `this->collision_info.last_known_contact_plane.N` directly, with no fallback chain | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CliffSlide`, the `referenceNormal`/`refSource` selection above the cross-product) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). A fresh read of `last_known_contact_plane`'s own maintenance (pc:272659-272668) confirms retail overwrites it unconditionally from `contact_plane` every `validate_transition` pass — including with a steep plane — so retail keeps NO separately-preserved flat-ground history there either; this fallback chain is a genuine acdream invention, not a retail-matching read. Kept because it compensates for AP-4's incomplete `OnWalkable` bookkeeping (L.4-cliffslide-fallback, 2026-04-30): without it, `cross(currentSteep, lastKnownSteep)` degenerates to zero after >1 frame on a continuous steep slope, and CliffSlide returns `OK` (no deflection) instead of downhill drift — the "stay on the roof" wedge the L.4 session fought | If AP-4's `OnWalkable` reordering is ever completed/removed, `last_known_contact_plane` should carry the same information retail's does and this fallback chain becomes unneeded ballast (or, worse, silently picks a stale `LastWalkablePlane` over the now-correct current one) — re-audit together with AP-4 | `CTransition::cliff_slide` pc:272397 (0050a6d0); `last_known_contact_plane` maintenance pc:272659-272668 (~0050ad07); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #2 | | AD-54 | `Transition.EdgeSlideAfterStepDownFailed` reroutes to `CliffSlide` instead of `PrecipiceSlide` when the stored walkable polygon itself is steeper than `FloorZ` (`sp.WalkablePlane.Normal.Z < PhysicsGlobals.FloorZ`); retail's raw `SPHEREPATH::edge_slide` has no steepness branch here — `if (walkable != null) { ... precipice_slide(...) }` unconditionally | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed`, the `L.4-walkable-steep` block) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). The permissive `LandingZ` walkable-acceptance threshold that lets a steep roof become "walkable" in the first place IS confirmed retail-faithful (`BSPTREE::find_collisions` unconditional `walkable_allowance = LandingZ`, pc:323740-323783, TS-4's own citation) — so a steep-roof walkable polygon is a real state retail also reaches. What is NOT independently verified is whether retail's outer `transitional_insert` caller absorbs a same-polygon-standing `COLLIDED_TS` from `precipice_slide` (its raw `find_crossed_edge` returning false while standing on, not crossing, the polygon) some other way that avoids the acdream "stuck in a Collided revert loop" this reroute prevents | If retail's outer retry loop turns out to already handle the no-crossed-edge-while-standing-on-a-steep-poly case without a reroute, this branch is an unnecessary compensating layer that could route a genuinely PrecipiceSlide-bound case (a shallow polygon edge that happens to sit at exactly `FloorZ`) into CliffSlide instead | `SPHEREPATH::edge_slide` pc:273001-273090 (0050b3d0, direct walkable branch quoted at pc:364-370 in the P2 research doc); `BSPTREE::find_collisions` pc:323740-323783 (0053a730, unconditional `LandingZ`); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #3 | +| AD-55 | `calc_friction`'s Sledding slope-flatness test compares `GroundNormal.Z > 0.99999536f` (≈0.175° from flat); the raw retail decomp literally computes `__fcos(0.17453292519943295)` (= cos(10°) ≈ 0.984808) and compares that against `contact_plane.N.z` — physically very different tests (0.175° accepts only essentially-perfectly-flat ground; 10° accepts any modest slope) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`calc_friction`, the Sledding near-flat branch) | Filed 2026-07-30 splitting AP-7's retirement (Campaign P Slice P2). Two hypotheses, neither confirmed this pass: (a) BN misdecompiled a raw float-constant load as an `__fcos()` call (a known BN artifact class), or (b) ACE's own port made an independent error and cos(10°) is correct. `0.99999536f` is kept provisionally — least churn, since it is what acdream's own prior (structurally unreachable) dead code already had — pending a live Ghidra decompile of `0050ee70` checking whether the FCOS opcode is real or a raw `FLD` of one of these two constants | Currently harmless in production: nothing sets `PhysicsState.Sledding` client-side (see #166 research), so this branch is unreachable either way. The moment a data-authored Sledding toggle exists, the wrong constant changes which slopes get the light 0.2f sled-friction override vs. the heavier default | `CPhysicsObj::calc_friction` pc:276694-276822 (0050ee70), the `__fcos(0.17453292519943295)` slope-flatness comparison; ACE `PhysicsObj.calc_friction` PhysicsObj.cs:2120-2141 (`0.99999536f`); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §1, §7 item 3 | | AD-46 | **LIVE. Reframed at Campaign V slice V11 (2026-07-29), when GL was deleted and the comparison that discovered this row ceased to exist.** Dense alpha-blended distant scenery (the treeline) may read slightly denser than retail's, because the anisotropic TAP PATTERN is implementation-defined and acdream's Vulkan driver does not tap identically to retail's D3D9 one. Both request the same sampler state — trilinear, clamp-and-repeat, the device's maximum anisotropy. **What changed at V11 is only the left-hand side of the comparison**: this was measured GL-vs-Vulkan (~15% of the pixels in the band), and it is now a Vulkan-vs-retail question against the D3D oracle in the last column. The measurement below is retained as the evidence that the residual is a tap pattern and not a bug, even though one of its two arms no longer exists. | `src/AcDream.App/Rendering/Wb/WorldTextureArray.cs` (`RhiWorldTextureArray.WorldArrayAnisotropy`); measured in plan §5.5.19, reframed §5.5.24 | Not assumed — narrowed by measurement while both backends still existed, on an offline capture with no session, no entities and both clocks pinned. Anisotropy 1 → 41,509 differing pixels in the tree band; anisotropy 16 (GL's value, and retail's `m_D3DCaps.MaxAnisotropy`) → 22,266, and the rest of the frame fell to 497 px of 563,200, i.e. 8.8e-04, inside the campaign's 0.001 threshold. The residual was not a sub-pixel shift (an integer shift search found none), not a sharpness change (high-frequency energy matched within 5%), and not depth precision (forcing Vulkan's window-depth range to GL's compressed [0.5, 1] moved it by 3%). Monotone improvement toward GL's own anisotropy with no knob left is what made it a driver property rather than a bug. | Distant foliage shimmers or reads denser than retail's. The class is confined to alpha-blended dense overlap: opaque terrain, roofs, walls, water, statics, the character and the whole retained UI are inside threshold. **Now unfalsifiable by self-differential** — with GL gone, the only way to retire this row is a side-by-side against the retail client, not against another acdream backend. | `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`, whose `SetSamplerState(stage, 0xA /* D3DSAMP_MAXANISOTROPY */, m_D3DCaps.MaxAnisotropy)` at `0x005a4230` is the value acdream requests | | AD-47 | **Filed at Campaign V slice V11 (2026-07-29); the campaign's risk register scheduled this row here.** Multisample resolve sample POSITIONS are unspecified by both the Vulkan and D3D9 specifications, so acdream's MSAA-on silhouette edges do not match retail's pixel-for-pixel even at the same sample count. acdream's strict pixel gates therefore run with MSAA forced OFF on every arm, and MSAA-on gets only a relaxed visual smoke. | `src/AcDream.App/RuntimeOptions.cs` (`ACDREAM_MSAA_SAMPLES`); forced to 0 in `tools/run-offline-pixel-gate.ps1` | Measured, not assumed: plan §5.5.16 compared two backends at 4x and found **8.83% of the frame differing — 81,359 px of 921,600 — essentially all of it hugging foliage and silhouette edges**, which is ninety-fold over the 0.001 gate threshold. That is two implementations' sample patterns, not a renderer divergence, which is why forcing MSAA off is what makes the remaining difference attributable rather than a threshold relaxation. | Edge quality on thin geometry (fence rails, foliage, distant railings) differs from retail at the sub-pixel level whenever MSAA is on, which is the ordinary player configuration. Because the gates run MSAA off, **a real regression confined to the multisample path would not be caught by them** — that is the actual exposure this row records. | D3D9 `D3DRS_MULTISAMPLEANTIALIAS` / `D3DMULTISAMPLE_TYPE` as set by `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`; retail's sample pattern is the driver's, exactly as ours is | | AD-48 | **Filed at Campaign V slice V11 (2026-07-29).** Presentation is paced by the Vulkan swapchain present mode (FIFO, i.e. VSync) or by a refresh-rate software pacer when uncapped, rather than by retail's D3D9 `Present` with its own frame-rate limiter. Frame delivery cadence, and therefore input-to-photon latency, is a property of our present path rather than a port of retail's. | `src/AcDream.App/RuntimeOptions.cs:98-100`; `src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs` | Retail's limiter and ours both bound the frame rate to the display; the simulation is fixed-step and clock-driven, so gameplay timing does not ride on presentation cadence. The uncapped path exists for measurement and is not the shipping default. | A pacing mismatch shows up as judder or input latency that differs from retail's feel without any visual difference in a captured frame — invisible to every pixel gate by construction. Issue **#235** (the capped/RDP jump-presentation cadence alias) is the known live instance of this class. | D3D9 `IDirect3DDevice9::Present`; retail's frame limiter in `RenderDeviceD3D` | @@ -116,7 +117,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 3. Documented approximation (AP) — 93 active rows +## 3. Documented approximation (AP) — 92 active rows (AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84 collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered @@ -129,7 +130,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-3 | Step-down chain triggered only when contact is invalid OR steeper than walkable; retail's `transitional_insert` OK-path ALWAYS runs it | `src/AcDream.Core/Physics/TransitionTypes.cs:1197` | Conditional preserves the observed-to-matter cases (edge departure, steep cliff-slide) without running the chain every step (per pc:273191 agent reports) | Steps where retail runs step-down despite a valid walkable contact (bump maintenance, edge-slide arming) are skipped — float-off or missed edge slides in untested geometry | `transitional_insert` OK-path pc:273191 | | AP-4 | CliffSlide check moved BEFORE retail's Branch-1 (`!OnWalkable` → restore+OK) gate, compensating our L.2.3i FloorZ OnWalkable bookkeeping | `src/AcDream.Core/Physics/TransitionTypes.cs:1316` | Retail's order with our incomplete OnWalkable stops the player dead every frame on steep slopes ("stay on the roof"); reorder restores downhill drift | CliffSlide fires in states where retail's Branch 1 would restore-and-OK — body slides where retail holds, e.g. contact-plane-bearing steep geometry near edges | retail EdgeSlide dispatch order (transitional_insert step-down failure) | | AP-5 | Step-down skips Placement validation for the contact-maintenance call (`runPlacement=false`); ACE/retail run it unconditionally (kept for DoStepUp) | `src/AcDream.Core/Physics/TransitionTypes.cs:3393` | Residual wall-slide artifacts made Placement misfire, leaving players stuck near walls; the skip was the targeted L.2.3h fix | Step-down can settle into positions Placement would reject — slight wall embedding, or accepting a step-down through overlap geometry retail catches | `CTransition::step_down` pc:272952; ACE Transition.cs:731-741 | -| AP-7 | `calc_friction` threshold 0.0 with retail's state gate missing; retail uses 0.25 gated by an undecoded state check | `src/AcDream.Core/Physics/PhysicsBody.cs:307` | Bumping the threshold without the gate hammered normal walking (3 → 0.16 m/s); as-read 0.0 kept; locomotion probably state-exempted in retail. Filed L.3c-followup | Friction engages under different conditions — post-landing slides, knockback decay, sledding speeds mismatch retail's deceleration | pc:276702-276705 (state gate + 0.25) | +| ~~AP-7~~ | **RETIRED 2026-07-30 (Campaign P Slice P2) — the "state gate" was a BN decompiler artifact, not a locomotion exemption.** `calc_friction` now ports retail's confirmed 0.25f threshold (`if (angle >= 0.25f) return;`) unconditionally, no special-cased gate. The "state check at pc:276702" the old row cited is `PhysicsState.Sledding` (confirmed via ACE's `PhysicsObj.calc_friction`, references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2120-2141, and `SLEDDING_PS=0x800000` in acclient.h:2838) — it gates the 1.5625/6.25/near-flat friction-value OVERRIDE, not the threshold return itself; acdream had no live Sledding setter then or now (see #166 research, docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §3), so the branch was simply unreachable dead code, not an exemption for ordinary walking. The reverted 2026-04-30 L.3c attempt (naive 0.0→0.25 bump, forward locomotion 3→0.16 m/s in `PlayerMovementControllerTests`) does not reproduce on the production graphical local-player path post-R6: `PlayerMovementController` zeroes `Velocity.X/Y` to exactly zero every tick before `calc_friction` runs whenever animation root motion drives the walk, so friction has no horizontal velocity left to hammer (pinned at the PhysicsBody level by `GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests`). The headless/`get_state_velocity` movement-controller path and remote/NPC movers still feed real velocity into this function and remain the ones to watch if a similar regression resurfaces there. | `src/AcDream.Core/Physics/PhysicsBody.cs` (`calc_friction`); `tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs` (AP-7 test block) | — | — | `CPhysicsObj::calc_friction` pc:276694-276822 (0050ee70); ACE `PhysicsObj.calc_friction` PhysicsObj.cs:2120-2141; `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §1 | | AP-10 | Dry-corner water depth: retail's 0.1 m allowed sink-in collapsed to 0 | `src/AcDream.Core/Physics/TerrainSurface.cs:481` | The 0.1 offset destabilizes the feet-exactly-on-plane contact-touch check (dist > EPSILON → SetContactPlane never fires → float/fall); retail's ~10 cm sink-in is visually indistinguishable | Masks a contact-touch epsilon fragility — other water-depth values exercising the same instability could oscillate shoreline walkable validation; retail's wet/dry corner sink-in visual absent | `ObjCell.get_water_depth` / `calc_water_depth` (via ACE port) | | AP-11 | Hand-authored 4-keyframe fallback sky set (sunrise/noon/sunset, fog ~80–350 m) when the Region dat isn't loaded yet | `src/AcDream.Core/World/SkyState.cs:167` | A renderable sky is needed during boot before the Region dat parses; safety net on region-load failure | Any window where the fallback is active shows sky/fog lighting only roughly resembling retail's dat-driven values | SkyTimeOfDay keyframes, Region dat 0x13000000 | | AP-12 | Enchantment family-stacking tiebreak by largest SpellId; retail picks highest Generation, tie-broken by latest cast | `src/AcDream.Core/Spells/EnchantmentMath.cs:89` | `ActiveEnchantmentRecord` doesn't carry Generation; SpellId correlates with generation level in practice | Where spell ids don't track power within a family (or same-generation re-cast), the wrong buff wins — vital-max / stat values diverge from retail | `CEnchantmentRegistry::EnchantAttribute` 0x00594570 (pc:416110) | diff --git a/src/AcDream.Core/Physics/PhysicsBody.cs b/src/AcDream.Core/Physics/PhysicsBody.cs index acb47ea3..da03c10b 100644 --- a/src/AcDream.Core/Physics/PhysicsBody.cs +++ b/src/AcDream.Core/Physics/PhysicsBody.cs @@ -12,9 +12,12 @@ namespace AcDream.Core.Physics; // FUN_00511ec0 set_velocity — store + clamp to MaxVelocity // FUN_00511fa0 set_local_velocity — body→world transform then set_velocity // FUN_00511de0 set_on_walkable — set/clear OnWalkable transient flag -// FUN_0050f940 calc_friction — ground-contact friction // FUN_00515020 update_object — per-frame top-level driver // +// calc_friction is now cited against the NAMED retail decomp instead of the +// older unnamed FUN_0050f940 chunk — see its own doc comment below +// (CPhysicsObj::calc_friction, acclient_2013_pseudo_c.txt:276694, 0050ee70). +// // Cross-checked against ACE PhysicsObj.cs and PhysicsGlobals.cs. // ──────────────────────────────────────────────────────────────────────────── @@ -542,48 +545,87 @@ public sealed class PhysicsBody calc_acceleration(); } - // ── FUN_0050f940 ─────────────────────────────────────────────────────── + // ── CPhysicsObj::calc_friction (0050ee70) ─────────────────────────────── /// /// Apply friction deceleration to the velocity when the body is standing /// on a walkable surface. /// - /// Decompiled logic (FUN_0050f940): + /// AP-7 resolved (Campaign P Slice P2, 2026-07-30, + /// docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1). + /// The named retail decomp (CPhysicsObj::calc_friction, + /// pseudo-C:276694-276822, 0050ee70) independently re-confirms the + /// 0.25f threshold (derived twice, once per BN-rendered branch) — + /// the OLD in-code claim that "the decompile uses 0.0" traced to the + /// unnamed, superseded FUN_0050f940 Ghidra chunk at a DIFFERENT + /// address; per CLAUDE.md the named decomp wins. Cross-checked against + /// ACE PhysicsObj.calc_friction + /// (references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2120-2141), + /// which reads as ONE linear function rather than the BN-rendered "two + /// duplicated branches" — the branch split is most likely a BN decompiler + /// artifact around a single if (state & SLEDDING_PS) block + /// (ACE-derived, Ghidra-verify: see the research doc §7 items 1-2 for the + /// still-open BN-artifact-vs-genuine-duplication question; low + /// implementation risk either way since ACE's single-linear-function + /// reading is adopted regardless). + /// + /// Decompiled logic (retail, ACE-derived shape): /// if NOT OnWalkable → return - /// fVar1 = dot(groundNormal, velocity) - /// if fVar1 < 0: - /// velocity -= fVar1 * groundNormal (remove inward normal component) - /// scalar = pow(1 - friction, dt) - /// velocity *= scalar + /// angle = dot(velocity, contactPlane.N) + /// if angle >= 0.25f → return (moving away fast enough — no friction) + /// velocity -= angle * contactPlane.N (remove inward normal component, unconditional) + /// friction = this->friction (same baseline in every case) + /// if Sledding: velocityMag2-banded override (see below) + /// velocity *= pow(1 - friction, dt) /// - /// The threshold (0.0 from _DAT_007c78a0) means any velocity with a - /// downward component relative to the normal gets friction applied. - /// Positive dot means moving away from the surface — no friction. + /// L.3c attempt (2026-04-30, REVERTED): a bare 0.25f bump with no other + /// change dropped measured forward locomotion from ~3 m/s to ~0.16 m/s + /// in PlayerMovementControllerTests — friction engaged EVERY tick because + /// flat-ground walking has dot(velocity, groundNormal) ≈ 0, which is + /// < 0.25f. The research pass's math check: DefaultFriction=0.95f (both + /// PhysicsBody.cs:120 and ACE PhysicsGlobals.cs:15 agree — not a divergent + /// constant), so pow(0.05, dt) at 60 Hz over 1s ≈ 0.951^60 ≈ 4.9% velocity + /// remaining — matches the observed hammering almost exactly. /// - /// Cross-checked with ACE PhysicsObj.calc_friction which uses 0.25f as - /// the threshold instead; the decompile uses 0.0. We match the decompile. + /// Why this is safe to land now: the L.3c test predates the 2026-07-17 + /// "local player animation-owned grounded movement" landing (R6). + /// PlayerMovementController.cs (~line 1742) zeroes Velocity.X/Y to 0 + /// immediately before UpdatePhysicsInternal runs whenever animation root + /// motion drives the walk (the production graphical local-player path + /// since R6) — walking displacement comes from the animation Frame delta + /// applied directly to Position, not from integrating Velocity. Friction + /// decaying an already-zero horizontal Velocity is a no-op, so the L.3c + /// mechanism does not reproduce on that path. The `else` branch (no + /// animation root motion — headless/test-controller movers using + /// `get_state_velocity`, and remote/NPC movers) DOES still feed real XY + /// speed into Velocity and remains exposed; see + /// GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests for + /// the regression pin on the root-motion path specifically. /// - /// L.3c attempt (2026-04-30, REVERTED): tried bumping to 0.25f per - /// retail acclient_2013_pseudo_c.txt:276705. Build green but - /// PlayerMovementControllerTests showed forward locomotion dropping - /// from ~3m/s to ~0.16m/s — friction now hammers normal walking. - /// Retail's friction block is gated by an additional state check at - /// line 276702 (`(this->state & ...) == 0`) that we didn't decode - /// fully; locomotion is probably skipped from the friction path - /// while actively walking. Filed as L.3c-followup; keeping the - /// matching-the-decompile-as-read 0.0 threshold for now. + /// ⚠️ Constant discrepancy (Ghidra-verify, NOT resolved this pass): the + /// raw decomp's Sledding slope-flatness test literally computes + /// __fcos(0.17453292519943295) (= cos(10°) ≈ 0.984808) and compares + /// against contact_plane.N.z; ACE's port instead compares + /// ContactPlane.Normal.Z > 0.99999536f directly (≈0.175° from flat, NOT + /// 10°) — physically very different tests. Neither hypothesis (a BN + /// misdecompile of a raw float load as __fcos, vs. an independent ACE + /// port error) is confirmed; a live Ghidra decompile of 0050ee70 settles + /// it. 0.99999536f is kept provisionally (least churn — it's what + /// acdream's own prior dead code already had); do not silently resolve + /// this without the Ghidra check. See register row AD-55. /// public void calc_friction(float dt, float velocityMag2) { if ((TransientState & TransientStateFlags.OnWalkable) == 0) return; - float dot = Vector3.Dot(GroundNormal, Velocity); - if (dot >= 0f) + float angle = Vector3.Dot(Velocity, GroundNormal); + if (angle >= 0.25f) return; // Remove the component of velocity that presses into the ground normal. - Velocity -= dot * GroundNormal; + // Unconditional past the threshold check — no separate inner guard. + Velocity -= angle * GroundNormal; float friction = Friction; @@ -592,7 +634,7 @@ public sealed class PhysicsBody { if (velocityMag2 < 1.5625f) // 1.25² — slow sled friction = 1.0f; - else if (velocityMag2 >= 6.25f && GroundNormal.Z > 0.99999536f) // near-flat + else if (velocityMag2 >= 6.25f && GroundNormal.Z > 0.99999536f) // near-flat, Ghidra-verify (see doc comment) friction = 0.2f; } diff --git a/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs b/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs index b4e55c73..897a10f2 100644 --- a/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs +++ b/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs @@ -8,7 +8,8 @@ namespace AcDream.Core.Tests.Physics; /// /// Unit tests for PhysicsBody — the C# port of CPhysicsObj's core simulation /// from acclient.exe (FUN_005111d0, FUN_00511420, FUN_00511ec0, FUN_00511fa0, -/// FUN_00511de0, FUN_0050f940, FUN_00515020). +/// FUN_00511de0, FUN_00515020, and the named +/// CPhysicsObj::calc_friction at 0050ee70). /// public sealed class PhysicsBodyTests { @@ -454,6 +455,106 @@ public sealed class PhysicsBodyTests Assert.Equal(1f, body.Velocity.X, precision: 4); } + // ════════════════════════════════════════════════════════════════════ + // AP-7 (Campaign P Slice P2, 2026-07-30): calc_friction's 0.25f threshold + // docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1 + // ════════════════════════════════════════════════════════════════════ + + [Fact] + public void calc_friction_dot_between_zero_and_quarter_now_engages_friction() + { + // dot(velocity, groundNormal) = 0.1 — ABOVE the old 0.0 threshold + // (no friction pre-fix) but BELOW the new retail 0.25f threshold + // (friction now engages). This is exactly the window the 0.0 -> 0.25f + // port changes; pinning it here documents the intentional behavior + // change the AP-7 register row used to warn about. + var body = MakeGrounded(); + body.GroundNormal = Vector3.UnitZ; + body.Friction = 0.95f; + body.Velocity = new Vector3(5f, 0f, 0.1f); + float mag2 = body.Velocity.LengthSquared(); + + body.calc_friction(1f / 60f, mag2); + + Assert.True(body.Velocity.Length() < 5f, + "Retail's 0.25f threshold means dot=0.1 (below 0.25) engages friction, " + + "unlike the old 0.0 threshold which would have returned early here."); + } + + [Fact] + public void calc_friction_dot_at_quarter_threshold_returns_early_no_change() + { + // dot(velocity, groundNormal) = 0.25 exactly -> angle >= 0.25f is true + // -> early return, matching ACE's `if (angle >= 0.25f) return;`. + var body = MakeGrounded(); + body.GroundNormal = Vector3.UnitZ; + body.Velocity = new Vector3(5f, 0f, 0.25f); + var before = body.Velocity; + float mag2 = body.Velocity.LengthSquared(); + + body.calc_friction(1f / 60f, mag2); + + Assert.Equal(before, body.Velocity); + } + + [Fact] + public void GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests() + { + // Campaign P Slice P2 research finding: the reverted 2026-04-30 L.3c + // regression (forward locomotion 3 -> 0.16 m/s) cannot reproduce on + // the production graphical local-player path post-R6, because + // PlayerMovementController zeroes Velocity.X/Y to exactly zero every + // tick BEFORE UpdatePhysicsInternal/calc_friction runs whenever + // animation root motion drives the walk (walking displacement comes + // from the animation Frame delta applied directly to Position, not + // from integrating Velocity). This test pins that specific state at + // the PhysicsBody level (the only file this slice may change): + // Velocity.XY == 0 on flat ground is IDENTICAL after calc_friction + // whether the threshold is the old 0.0 or the new retail 0.25 -- + // friction has nothing to hammer because there is no horizontal + // velocity for it to act on. Only the residual vertical (gravity) + // component may be affected by the normal-removal step, exactly as + // retail's own contact handling expects. + var body = MakeGrounded(); + body.GroundNormal = Vector3.UnitZ; + body.Friction = 0.95f; + // Root-motion path's exact per-tick shape: horizontal zeroed, only + // the world Z survives (a small residual downward settle velocity). + body.Velocity = new Vector3(0f, 0f, -0.05f); + + body.calc_friction(1f / 60f, body.Velocity.LengthSquared()); + + Assert.Equal(0f, body.Velocity.X, precision: 5); + Assert.Equal(0f, body.Velocity.Y, precision: 5); + Assert.True(MathF.Abs(body.Velocity.Z) < 0.05f, + $"Root-motion horizontal speed must stay exactly at full (zero) " + + $"speed under the new threshold; got Velocity={body.Velocity}"); + } + + [Fact] + public void calc_friction_sledding_state_gate_reachable_with_new_threshold() + { + // The Sledding-gated overrides (1.5625/6.25/near-flat) were already + // present but structurally unreachable in production (nothing sets + // PhysicsStateFlags.Sledding, see #166 research §3) -- this test only + // confirms the branch still behaves once the outer 0.25f gate is + // passed, so a future data-authored Sledding toggle lands on tested + // code. + var body = MakeGrounded(); + body.GroundNormal = Vector3.UnitZ; + body.State |= PhysicsStateFlags.Sledding; + body.Velocity = new Vector3(3f, 0f, -0.5f); // velocityMag2 = 9.25, >= 6.25 + float mag2 = body.Velocity.LengthSquared(); + + body.calc_friction(1f / 60f, mag2); + + // friction should be 0.2f (near-flat, fast sled) rather than the + // default 0.95f -- less decay, so speed should stay closer to 3. + Assert.True(body.Velocity.Length() > 2.9f, + $"Fast near-flat sledding should use the light 0.2f friction override; " + + $"got speed {body.Velocity.Length()}"); + } + // ════════════════════════════════════════════════════════════════════ // update_object — per-frame driver // ════════════════════════════════════════════════════════════════════ From 65de6921ced251183d1a20a3d7479fbcc71d64b3 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 30 Jul 2026 08:28:18 +0200 Subject: [PATCH 3/4] test(physics): TS-4 fixture-first attempt reproduces the 2026-04-30 wedge; shortcut stays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Campaign P Slice P2 step 2-3 (docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §4, §6 Step 3). Per the research doc's own port order, TS-4's Path-6 steep-poly shortcut may only be removed after a fixture reproduces the original "stuck in falling animation on a steep roof" symptom cleanly with the shortcut disabled. No surviving live-session fixture exists from the 2026-04-30 L.4 commit (b1af56e); this adds a dat-free multi-frame capture (Ts4SteepRoofWedgeCaptureTests) using BSPStepUpFixtures.SlopedUnwalkable's 63.4 degree slope, replayed at 30 Hz with gravity integrated between PhysicsEngine.ResolveWithTransition calls -- the same idiom as Issue185OutdoorStairsSeamReplayTests. Against today's baseline (shortcut active) the capture is green, as expected (the shortcut's explicit AddOffsetToCheckPos keeps the body moving every tick by construction). Scratch-removed the shortcut (both BSPQuery.cs sphere0/sphere1 branches, not committed -- reverted after capture) and re-ran the same test: the body falls and lands cleanly on the steep polygon at tick 17 (InContact, OnWalkable=false, via retail's own permissive CTransition::check_walkable LandingZ gate, pc:273202), then freezes at that exact position for the rest of the run -- the exact historical wedge shape, tripping the test's own >0.5s-frozen threshold at tick 33. Root-cause diagnosis via ACDREAM_DUMP_EDGE_SLIDE=1: the freeze is upstream of EdgeSlideAfterStepDownFailed/CliffSlide entirely (none of that dispatch's diagnostics fire). TransitionalInsert's Phase 2 object-collision check returns Adjusted on every retry attempt because Path 6's retail- faithful SetCollide returns ADJUSTED_TS without repositioning the sphere (unlike the interim shortcut, which explicitly pushes the sphere off the face) -- the same steep polygon re-triggers Path 6 on the immediate retry, forever, and Phase 3 (the sp.Collide handling that contains DoCheckWalkable, the Placement re-test, and the TS-1 CliffSlide chain) is gated on Phase 1 AND Phase 2 both returning OK, so it is structurally unreachable from this state. TS-1's completeness is moot here -- the code path that would call into it never runs. Per the mission's explicit escape valve: STOP here, keep the shortcut, and report -- do not improvise a third variant. Full diagnosis, the exact capture, and the concrete next research question (does retail's own transitional_insert loop check sphere_path.collide on every iteration regardless of Phase 2's own return value, or only when Phase 2 returns OK?) are recorded in the research doc's §7 item 6 and the doc's headline; the campaign plan's P2 section gets a matching status note. Physics test suite: 1841 passed, 1 skipped (D4, pre-existing/unrelated), 0 failed. Co-Authored-By: Claude Opus 5 --- .../2026-07-29-physics-parity-campaign.md | 9 + ...0-response-layer-edge-family-pseudocode.md | 122 +++++++++-- .../Physics/Ts4SteepRoofWedgeCaptureTests.cs | 202 ++++++++++++++++++ 3 files changed, 311 insertions(+), 22 deletions(-) create mode 100644 tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs diff --git a/docs/plans/2026-07-29-physics-parity-campaign.md b/docs/plans/2026-07-29-physics-parity-campaign.md index bb173915..a73f2dcc 100644 --- a/docs/plans/2026-07-29-physics-parity-campaign.md +++ b/docs/plans/2026-07-29-physics-parity-campaign.md @@ -84,6 +84,15 @@ character state continuously. ### P2 — Response-layer edge family — retires TS-1, TS-4, AP-7; closes #166, #116 +**Status (2026-07-30):** TS-1 and AP-7 retired; #166 reattributed in +ISSUES.md (no new code — see the research doc §3). **TS-4 is deferred, not +retired** — the fixture-first removal attempt this doc's §6 Step 3 requires +reproduced the historical 2026-04-30 L.4 wedge (a dat-free capture, +`Ts4SteepRoofWedgeCaptureTests`); the shortcut stays in `BSPQuery.cs`. Root +cause and the concrete next research step are recorded in +`docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §7 item +6. #116 is untouched (oracle-first, own session). + The collision *response* layer (what happens after a hit): ground friction, cliff edges, downhill landings, near-perpendicular wall slides. One oracle-driven pass; the physics digest's DO-NOT-RETRY table diff --git a/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md b/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md index 2dc9ba92..ff83cf7c 100644 --- a/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md +++ b/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md @@ -1,20 +1,30 @@ # P2 — Collision response-layer edge family: port-ready pseudocode -**Status: RESEARCH PASS COMPLETE (2026-07-30).** Research-only doc for -Campaign P Slice P2 (`docs/plans/2026-07-29-physics-parity-campaign.md` -§P2). Retires TS-1, TS-4, AP-7; closes #166, #116. No source changes made -by this doc; it is the pre-port research artifact for a future -implementation session. **Headline findings that change the plan's -assumptions:** TS-1 is already substantially ported (the register row -and plan phrasing are stale — see §2); #166 is very likely NOT about a -literal `PhysicsState.Sledding` auto-toggle at all (see §3); AP-7's L.3c -regression may no longer reproduce under the post-R6 animation-root-motion -architecture for the graphical path, but likely still reproduces for the -headless/test path (see §1); TS-4's shortcut removal is coupled to TS-1's -completion and must not be done independently (see §6 Step 3-4); #116 -remains a genuine oracle-first research item needing live cdb/Ghidra, not -an implementation item (see §5). Read §6 (port order) before starting -implementation — the safe sequence is not the plan's listed item order. +**Status: RESEARCH PASS COMPLETE (2026-07-30); IMPLEMENTATION PASS PARTIAL +(2026-07-30).** Originally a research-only doc for Campaign P Slice P2 +(`docs/plans/2026-07-29-physics-parity-campaign.md` §P2); a same-day +implementation session landed TS-1's retirement and AP-7's fix, attempted +TS-4 per this doc's own §6 Step 3 fixture-first order, reproduced the +historical wedge, and stopped — see §7 item 6 for the full capture and +root-cause diagnosis. TS-4 is NOT retired; its shortcut stays in place. +#166 got a reattribution note in ISSUES.md rather than new code (per §3). +#116 remains untouched (oracle-first, out of implementation scope). +**Headline findings that change the plan's assumptions:** TS-1 was already +substantially ported (the register row and plan phrasing were stale — see +§2); the one real gap needed no code change (acdream's unified world-space +`SpherePath` design makes retail's per-cell recache a no-op correction here +— see §2 and the TS-1 register row's retirement text); #166 is very likely +NOT about a literal `PhysicsState.Sledding` auto-toggle at all (see §3); +AP-7's L.3c regression does not reproduce on the production graphical +root-motion path post-R6, and now ports retail's confirmed 0.25f threshold +(see §1); TS-4's shortcut removal is coupled to TS-1's completion and +reproduces a wedge even after TS-1 lands — see the §7 item 6 update for the +precise mechanism (Phase 3 of `TransitionalInsert` is structurally +unreachable from Path 6's unconditional `SetCollide`, which returns +`Adjusted` without repositioning the sphere) and what a future attempt +needs to check first; #116 remains a genuine oracle-first research item +needing live cdb/Ghidra, not an implementation item (see §5). Read §6 (port +order) before starting further implementation on this family. Every claim below is tagged **FACT** (grep/read-verified against the named-retail decomp, the register, ISSUES.md, or current acdream source @@ -959,13 +969,81 @@ it blocks. walkable-steepness reroute as real compensating adaptations) or whether they're unnecessary inventions. Needs a fresh, focused named-decomp read (not Ghidra/cdb-gated — just not done this pass). -6. **[TS-4 / Step 3]** Whether TS-1's Step 1 fix alone is sufficient to - let TS-4's shortcut be safely removed, or whether the 2026-04-30 L.4 - session's wedge had additional causes not yet identified. Answerable - only by the capture in §6 Step 3 — not a Ghidra/cdb question, but - listed here because it's the single highest-risk unresolved item in - this document (deleting a load-bearing shortcut based on an unproven - assumption). +6. **[TS-4 / Step 3] ANSWERED 2026-07-30 (implementation session) — NOT + sufficient; the wedge reproduces, and its mechanism is now precisely + characterized.** TS-1's gap #1 fix (this document's §2, landed the same + session) does NOT unblock TS-4. A dat-free multi-frame capture + (`tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs`, + using `BSPStepUpFixtures.SlopedUnwalkable`'s 63.4° slope, + `PhysicsEngine.ResolveWithTransition` replayed at 30 Hz with gravity + integrated between resolves — the same replay idiom as + `Issue185OutdoorStairsSeamReplayTests`) reproduces the EXACT historical + shape with the Path-6 steep shortcut temporarily removed (both + `BSPQuery.cs` sphere0/sphere1 branches): the body falls cleanly (30 + ticks, position advancing every tick), lands at + `(0.500, 0.000, 1.247)` at tick 17 with `InContact=true, OnWalkable=false` + (confirming the steep polygon WAS accepted via the permissive + `CTransition::check_walkable(0.0871556997f)` / `LandingZ` gate exactly + as predicted from the retail source read below), then **freezes at that + exact position for the remaining 16+ ticks with zero movement** — the + test's own wedge-detection threshold (>15 consecutive frozen ticks = + >0.5s) trips at tick 33. With the shortcut restored, the same test is + green (the shortcut's explicit `AddOffsetToCheckPos` keeps the body + moving every tick by construction). **The shortcut stays; TS-4 is NOT + retired this session.** + + **Root-cause diagnosis (`ACDREAM_DUMP_EDGE_SLIDE=1` capture against the + scratch shortcut-removed build):** the freeze is NOT inside + `EdgeSlideAfterStepDownFailed`/`CliffSlide` at all — none of that + dispatch's diagnostic lines (`DumpEdgeSlideBranch`, + `DumpStepDownBranchGate`, the `[steep-roof] PHASE3-RESET*` lines) fire + even once during the frozen ticks. Every frozen tick instead logs only + `edge-slide: phase2 attempt=0 env=OK obj=Adjusted` followed by + `attempt=1 env=OK obj=Adjusted` — i.e. `TransitionalInsert`'s Phase 2 + object-collision check (`FindObjCollisionsInCell`, `TransitionTypes.cs:1572`) + returns `Adjusted` on BOTH retry attempts, and per + `TransitionTypes.cs:1591-1596` an `Adjusted` `objState` unconditionally + `continue`s (retries Phase 1/2 from the top) rather than falling through + toward Phase 2.5/Phase 3. **Phase 3 — the `if (sp.Collide) { ... }` + block at `TransitionTypes.cs:1625` that contains the `DoCheckWalkable` + Placement re-test AND (on walkable failure) the reset-with-conditional- + `kill_velocity` path — is gated on Phase 1 AND Phase 2 BOTH returning + `OK` simultaneously (`TransitionTypes.cs:1568-1621`). Path 6's own + unconditional `SetCollide` (the retail-faithful code path TS-4 would + restore) returns `Adjusted`, not `OK` (matching retail's own + `return 3; // ADJUSTED_TS` at pc:323783, quoted in §4 above) AND does + NOT itself reposition the sphere** — unlike the interim shortcut, which + explicitly calls `AddOffsetToCheckPos` to push the sphere off the face + every time it fires. With no repositioning, the SAME steep polygon at + the SAME distance re-triggers Path 6 on the immediate retry, which + again returns `Adjusted`, forever — an Adjusted↔retry oscillation at a + fixed point that the loop's 2-attempt-per-resolve budget silently + absorbs (returning the frozen position as though the resolve + succeeded), repeating identically on every subsequent tick's fresh + resolve call. **Phase 3 (and therefore `DoCheckWalkable`, + `CliffSlide`, and the TS-1 chain entirely) is structurally unreachable + from this state** — TS-1's completeness is moot here because the code + path that would call into it never runs. + + **What this means for a future attempt:** the missing piece is NOT + (only) in `EdgeSlideAfterStepDownFailed`/`CliffSlide` — it is in how + `TransitionalInsert`'s Phase 1/2/2.5/3 dispatch (`TransitionTypes.cs + :1568-1710`) distinguishes "Phase 2 found a NEW collision, retry from + the top" from "Phase 2 registered a touch via `sp.Collide` and should + fall through toward Phase 3 regardless of its own `Adjusted` return." + Retail's own `transitional_insert` (pc:273137, `0050b6f0`) has NOT been + read closely enough this pass to say definitively whether it treats a + Path-6-sourced `ADJUSTED_TS` differently from an ordinary Adjusted + result before this session's `continue`-on-Adjusted structure was + written — that fresh, close read (specifically: does retail's loop + check `sphere_path.collide` on EVERY iteration regardless of the + latest Phase-2 return value, or only when Phase 2 returns OK?) is the + concrete next step, not a second speculative code change. Do not retry + the plain shortcut-deletion variant without that read; do not invent a + third variant (e.g. teaching Path 6 to reposition the sphere itself) + without confirming that's what retail actually does — that would be + exactly the kind of guess CLAUDE.md's workflow forbids twice in a row + on the same item. 7. **[#116 shape-1]** Where exactly, in the BSP/environment hit-test dispatch (candidate: `BSPTREE::find_collisions`'s `PathClipped`/ `collide_with_pt` arm, pseudo-C ~323700-323830, sibling to the diff --git a/tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs b/tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs new file mode 100644 index 00000000..29f55a22 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using System.Numerics; +using AcDream.Core.Physics; +using Xunit; +using Xunit.Abstractions; + +namespace AcDream.Core.Tests.Physics; + +/// +/// Campaign P Slice P2, TS-4 (Section 6 Step 3): the 2026-04-30 "L.4" fixture +/// capture required before the Path-6 steep-poly slide-tangent shortcut may be +/// removed (docs/research/2026-07-30-response-layer-edge-family-pseudocode.md +/// §4, §6 Step 3). The original repro was a live-client jump onto a steep +/// roof that got the body "stuck in falling animation" for many frames; no +/// captured fixture from that live session survives in the repo (checked +/// docs/research/2026-04-30-* and the L.4 commit `b1af56e`), so this +/// test builds a dat-free multi-frame replay from the existing +/// geometry (a 63.4° +/// slope, normal.Z ≈ 0.447 — below PhysicsGlobals.FloorZ ≈ 0.6642 but +/// above PhysicsGlobals.LandingZ ≈ 0.0871, i.e. exactly the band the +/// L.4 commit's own steep-poly shortcut targets) using the same +/// PhysicsEngine.ResolveWithTransition multi-frame replay idiom as +/// Issue185OutdoorStairsSeamReplayTests. +/// +/// +/// A body falls from directly above the slope's mid-face, integrating +/// gravity between resolves exactly as PhysicsBody.UpdatePhysicsInternal +/// would, for up to 3 simulated seconds (90 ticks at 30 Hz — retail's physics +/// tick rate, #32 L.5). "Wedged" is defined precisely, matching the original +/// bug report ("stuck in falling animation on the roof" for many consecutive +/// frames): the body's position stops changing (within 1 mm) for more than +/// 15 consecutive ticks (0.5 s) while never reaching the flat reference +/// floor at x<0, z=0. A healthy resolution reaches the flat floor (Z ≈ +/// ) well before the 90-tick +/// budget expires, whether it does so by retail's own COLLIDED-then-fall +/// bounce (this file's own git history documents that as retail's actual +/// behavior for a clean Path-6 steep hit with no pre-existing contact plane) +/// or by committing to the steep "walkable" surface via the permissive +/// LandingZ threshold (matching CTransition::check_walkable, +/// pc:273202, 0.0871556997f) and then downhill-drifting off it via +/// the already-ported TS-1 CliffSlide chain. +/// +/// +/// +/// Run TWICE across this slice's git history: once with the Path-6 steep +/// shortcut ACTIVE (pins today's baseline — always green, since the +/// shortcut's own in-frame slide-tangent cannot wedge by construction), and +/// once with it REMOVED (the retail-strict candidate). If both pass, TS-4's +/// removal is evidenced safe and lands in the same commit that deletes the +/// shortcut and its SetSlidingNormal writes. If the removed-shortcut +/// run wedges, the shortcut stays and this file's result against ToT is the +/// recorded evidence — see the commit message / research doc open questions +/// for the outcome actually reached. +/// +/// +public class Ts4SteepRoofWedgeCaptureTests +{ + private readonly ITestOutputHelper _out; + public Ts4SteepRoofWedgeCaptureTests(ITestOutputHelper output) => _out = output; + + private const uint CellId = 0xA9B40001u; + private const int TicksPerSecond = 30; // #32 L.5 retail physics tick rate + private const int MaxTicks = 3 * TicksPerSecond; + private const int WedgeTickThreshold = 15; // 0.5 s of zero motion == wedged + private const float WedgeEpsilon = 0.001f; // 1 mm + + private static PhysicsEngine MakeSlopeEngine() + { + var (root, resolved) = BSPStepUpFixtures.SlopedUnwalkable(); + + const uint LandblockId = 0xA9B4FFFFu; + const uint SyntheticGfxId = 0xDEADBEEFu; + + var heights = new byte[81]; + var heightTab = new float[256]; + for (int i = 0; i < 256; i++) heightTab[i] = -1000f; // terrain never interferes + + var engine = new PhysicsEngine(); + engine.AddLandblock( + LandblockId, + new TerrainSurface(heights, heightTab), + System.Array.Empty(), + System.Array.Empty(), + worldOffsetX: 0f, worldOffsetY: 0f); + + var cache = new PhysicsDataCache(); + var bspTree = new DatReaderWriter.Types.PhysicsBSPTree { Root = root }; + var physics = new GfxObjPhysics + { + BSP = bspTree, + PhysicsPolygons = new System.Collections.Generic.Dictionary(), + Vertices = new DatReaderWriter.Types.VertexArray(), + Resolved = resolved, + BoundingSphere = new DatReaderWriter.Types.Sphere { Origin = Vector3.Zero, Radius = 15f }, + }; + cache.RegisterGfxObjForTest(SyntheticGfxId, physics); + engine.DataCache = cache; + + engine.ShadowObjects.Register( + entityId: SyntheticGfxId, + gfxObjId: SyntheticGfxId, + worldPos: Vector3.Zero, + rotation: Quaternion.Identity, + radius: 15f, + worldOffsetX: 0f, + worldOffsetY: 0f, + landblockId: LandblockId, + collisionType: ShadowCollisionType.BSP, + scale: 1.0f); + + return engine; + } + + /// + /// Falls a player-flagged mover from directly above the 63.4° slope's + /// mid-face and asserts it reaches the flat floor (or at minimum keeps + /// making downward/downhill progress) without a >0.5s frozen stretch. + /// + [Fact] + public void FallOntoSteepSlope_NeverFreezesForOverHalfASecond_AndReachesFloor() + { + var engine = MakeSlopeEngine(); + float r = BSPStepUpFixtures.SphereRadius; + const float dt = 1f / TicksPerSecond; + const float gravity = -9.8f; + + var body = new PhysicsBody + { + TransientState = TransientStateFlags.Active, + }; + + // Start well above the slope's mid-face (slope spans x in [0,1], z in + // [0,2] at that x-range), falling straight down. + Vector3 pos = new(0.5f, 0f, 3.0f); + float fallVelocityZ = 0f; + uint cell = CellId; + + var positions = new List(MaxTicks) { pos }; + int frozenStreak = 0; + bool reachedFloor = false; + + for (int tick = 0; tick < MaxTicks; tick++) + { + fallVelocityZ += gravity * dt; + Vector3 target = pos + new Vector3(0f, 0f, fallVelocityZ * dt); + + var result = engine.ResolveWithTransition( + currentPos: pos, + targetPos: target, + cellId: cell, + sphereRadius: r, + sphereHeight: r * 2f, + stepUpHeight: 0.30f, + stepDownHeight: 0.04f, + isOnGround: false, + body: body, + moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, + movingEntityId: 0x01000000u); + + var newPos = result.Position; + float moved = Vector3.Distance(newPos, pos); + + if (moved < WedgeEpsilon) + frozenStreak++; + else + frozenStreak = 0; + + _out.WriteLine( + $"t{tick,3}: pos=({newPos.X:F3},{newPos.Y:F3},{newPos.Z:F3}) " + + $"moved={moved:F4} onGround={result.IsOnGround} onWalkable={result.OnWalkable} " + + $"contact={result.InContact} vz={fallVelocityZ:F2} frozen={frozenStreak}"); + + Assert.True(frozenStreak <= WedgeTickThreshold, + $"Body frozen for {frozenStreak} consecutive ticks (>{WedgeTickThreshold} == " + + $">0.5s) at tick {tick}, position ({newPos.X:F3},{newPos.Y:F3},{newPos.Z:F3}) — " + + "this is the 'stuck in falling animation on the roof' wedge shape."); + + pos = newPos; + cell = result.CellId; + body.Position = pos; + + if (result.IsOnGround) + fallVelocityZ = 0f; + + positions.Add(pos); + + // Reached the flat reference floor (x<0, z ~ r) — resolved cleanly. + if (pos.X < 0f && pos.Z <= r + 0.05f) + { + reachedFloor = true; + break; + } + } + + Assert.True(reachedFloor, + $"Body never reached the flat reference floor within {MaxTicks} ticks " + + $"({MaxTicks / (float)TicksPerSecond:F1}s); final position " + + $"({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) — this is the wedge the L.4 shortcut guards " + + "against (never resolving off the steep surface at all), distinct from a bounded " + + "per-tick freeze."); + } +} From 2ecd29e2806112d5cc779782d6b4b625ecc56c2c Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 30 Jul 2026 08:28:58 +0200 Subject: [PATCH 4/4] docs: reattribute #166 per Campaign P Slice P2 research; no Sledding auto-toggle needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §3, §6 Step 6. Corrects two things in the original AD-25+AP-7+TS-4 framing: AD-25's local-player half was already ported by the #182 rebuild (2026-07-07) and the remaining gap is remote/NPC-only (Campaign P P3 scope); and no client-side PhysicsState.Sledding auto-toggle exists anywhere in the named-retail decomp or ACE's PhysicsObj.cs -- the only Sledding write site in any reference repo is a per-weenie game-data property, not a physics landing response, so this issue must not wait on inventing one. AP-7 landed this session. TS-4's removal was attempted per its own fixture-first requirement and reproduced the historical 2026-04-30 wedge, so it stays deferred (see its register row and the research doc's §7 item 6). Closure pends TS-4 actually landing and a fresh capture against the campaign's final visual-matrix item 5. Co-Authored-By: Claude Opus 5 --- docs/ISSUES.md | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index e2dd2271..b7343984 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -4261,12 +4261,48 @@ contact chain diverges). Retiring those three rows IS this issue; do them together against a retail cdb capture of a downhill jump (velocity + contact-plane trace at landing). -**Where:** `PlayerMovementController.cs:874` (AD-25 suppression), -`PhysicsBody.cs:307` (AP-7), `BSPQuery.cs:2001` (TS-4). +**Reattribution (2026-07-30, Campaign P Slice P2 research pass — +`docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §3, +§6 Step 6):** confirmed as the AD-25 + AP-7 + TS-4 composite above, with +two corrections to the original framing. First, **AD-25's LOCAL-PLAYER +half was already ported** in the #182 verbatim `UpdateObjectInternal` +rebuild (2026-07-07) — what remains open for AD-25 is remote/NPC-only and +is explicitly Campaign P P3 scope, not this issue. Second, and more +important: **no client-side `PhysicsState.Sledding` auto-toggle exists in +retail, and this issue should not wait on inventing one.** A cross- +reference of the named-retail decomp (zero hits for "sled" anywhere in the +1.4M-line pseudo-C, string or hex-constant search) against ACE's complete +`PhysicsObj.cs` (the same shared `CPhysicsObj` class that produced +`calc_friction`) found the ONLY write site for `PhysicsState.Sledding` +anywhere in any reference repo is a per-weenie game-data property +(`WorldObject_Properties.cs:1105-1109`, server/database-set, same pattern +as `Ethereal`/`Static`) — not a physics-engine landing response. Ordinary +downhill-jump glide-and-bounce in retail is therefore NOT the literal +Sledding state for an ordinary player; Sledding is most likely reserved +for specific data-authored content (e.g. an actual sled-ride mechanic), +outside this issue's scope. AP-7 landed this same session (`calc_friction` +now ports retail's confirmed 0.25f threshold); TS-4's removal was attempted +per its own fixture-first requirement and reproduced the historical +2026-04-30 wedge, so **TS-4 stays deferred** (see its register row and the +research doc §7 item 6 for the precise mechanism and the concrete next +step). Closure of #166 therefore pends: (a) TS-4 actually landing, and (b) +re-checking Campaign P's final visual matrix item 5 ("Downhill jump +landing: sled glide + bounce") against a fresh capture — if the glide/ +bounce still visibly mismatches retail after AP-7 alone (with TS-4 still +deferred), that capture, not a guess, is what should drive any further +work here, and it should go through cdb against live retail before any +client-side Sledding-state mechanism is written. + +**Where:** `PlayerMovementController.cs:874` (AD-25 suppression, remote +half only — local half already ported via #182), +`src/AcDream.Core/Physics/PhysicsBody.cs` (`calc_friction`, AP-7 — DONE +2026-07-30), `src/AcDream.Core/Physics/BSPQuery.cs` (Path 6 steep branches, +TS-4 — deferred, shortcut still in place). **Acceptance:** side-by-side downhill jump: acdream glides/bounces like retail; flat-ground landings unchanged; no micro-bounce death spiral -(the reason AD-25 exists) reintroduced. +(the reason AD-25 exists) reintroduced. Blocked on TS-4's eventual landing +per the note above. ## #164 — UM action-replay dispatches drop the per-action Autonomous bit