docs(research): Campaign P P2 response-layer edge family - AP-7 resolved, TS-1 mostly ported, #166 reattributed
AP-7's gate is the Sledding branch; ACE's linear calc_friction (0.25 dot threshold, unconditional small-angle subtraction, Sledding overrides) is the correct reading and the L.3c walking regression is architecturally moot for the root-motion path. TS-1's register cite is stale dead code - the PrecipiceSlide/CliffSlide/EdgeSlide chain is substantially ported with one precise back-probe re-cache gap. #166 is a composite of AD-25+AP-7+TS-4, not a missing Sledding auto-toggle (no client write site exists). TS-4 removal is sequenced AFTER the TS-1 gap closes with captured fixtures. #116 stays oracle-first. Port order + 8 open Ghidra-verify questions recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
eed29a96f2
commit
3a4782048e
1 changed files with 990 additions and 0 deletions
|
|
@ -0,0 +1,990 @@
|
|||
# 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.
|
||||
|
||||
Every claim below is tagged **FACT** (grep/read-verified against the
|
||||
named-retail decomp, the register, ISSUES.md, or current acdream source
|
||||
in THIS worktree at the time of writing) or **INFERENCE** (reasoned but
|
||||
not yet decomp/cdb/Ghidra-confirmed — do not port on an INFERENCE alone
|
||||
without the flagged follow-up).
|
||||
|
||||
---
|
||||
|
||||
## 0. Binding DO-NOT-RETRY entries (copied verbatim from
|
||||
`memory/project_physics_collision_digest.md`, 3-day-old snapshot —
|
||||
re-verify line numbers before the implementation session)
|
||||
|
||||
These bind the P2 implementer. Do not re-attempt any of these shapes.
|
||||
|
||||
1. **Do NOT add `SetSlidingNormal` calls in the BSP/sphere collision
|
||||
layer.** Retail's only in-transition writer of `collision_info.sliding_normal`
|
||||
is `validate_transition` (0x0050ac21 / 0x0050aa70). A leaked normal +
|
||||
success writeback = an absorbing wedge at empty space. (#137
|
||||
mechanism-2 lesson; directly governs TS-4 item 4 below.)
|
||||
2. **Do NOT re-add a forced constant-shell de-penetration** to the
|
||||
sphere/cyl response. Retail slides tangentially (crease = collisionNormal
|
||||
× contactPlane.Normal) and never force-separates.
|
||||
3. **`SphereCollision` no longer calls `SetSlidingNormal`** (TS-45
|
||||
retired) — the only in-transition sliding-normal writer is
|
||||
`validate_transition`. Keep it that way.
|
||||
4. **Do NOT patch the degenerate-offset guard in `slide_sphere` ad hoc**
|
||||
for #116 — the issue explicitly wants an oracle-driven pass, not a
|
||||
symptom patch. (#116 DO-NOT-RETRY, both the digest and ISSUES #116.)
|
||||
5. **Do NOT re-introduce a topology-based outside-add / radial sweep**
|
||||
to cell membership while touching this family — unrelated layer, but
|
||||
the digest's adjacent #98/#116 sessions warn subagents drift there.
|
||||
6. **`calc_friction` threshold is retail 0.25 vs acdream 0.0** — this is
|
||||
AP-7. The L.3c attempt (naive bump to 0.25, no state gate) regressed
|
||||
normal walking 3 → 0.16 m/s and was reverted. Do NOT repeat a bare
|
||||
threshold bump without decoding the state gate first.
|
||||
7. **Shape-1 of #116 (tick-22760 lateral-slide loss) is NOT the
|
||||
degenerate-offset guard threshold** — that guard kills slides under
|
||||
~1.4 cm; the lost slide was 3.57 cm, well above it. The real
|
||||
divergence is the collision-normal SOURCE (recording layer), not
|
||||
slide/validate. Do not re-chase the guard threshold for shape-1.
|
||||
8. **Do NOT guess the BN `test ah,5` x87 branch polarity/squaring** in
|
||||
`slide_sphere` — this exact construct is called out as undecodable
|
||||
from BN alone (the PosHitsSphere-saga warning). Ghidra MCP settled it
|
||||
once already (2026-06-12) for the EPSILON-vs-EpsilonSq bug; Ghidra MCP
|
||||
is DOWN for this research pass — mark any residual x87-ambiguous claim
|
||||
Ghidra-verify, cite ACE as the fallback tiebreaker, do not silently guess.
|
||||
9. **AP-4 (CliffSlide check moved before retail's Branch-1 gate)** is a
|
||||
live, load-bearing reordering compensating for acdream's incomplete
|
||||
OnWalkable bookkeeping — touches the same code region as TS-1. Do not
|
||||
revert AP-4's reordering without re-verifying OnWalkable is complete;
|
||||
read AP-4's full row before changing `TransitionTypes.cs:1316` control
|
||||
flow.
|
||||
10. **TS-46 (two-scalar sphere reconstruction) is OUT OF SCOPE for P2**
|
||||
(it's P3) but shares files (`TransitionTypes.cs` `InitPath`) — do not
|
||||
fold TS-46 sphere-list work into a P2 commit.
|
||||
|
||||
---
|
||||
|
||||
## 1. AP-7 — friction state gate
|
||||
|
||||
### The function (FACT — named-retail grep-first)
|
||||
|
||||
`CPhysicsObj::calc_friction` is named at pseudo-C:276694
|
||||
(address `0050ee70`), called from `UpdatePhysicsInternal`-equivalent at
|
||||
pseudo-C:278490 (`0050f0a…` region, `CPhysicsObj::calc_friction(this, arg2,
|
||||
var_28)` where `arg2`=dt/quantum, `var_28`=velocity_mag2 — matches acdream's
|
||||
`calc_friction(float dt, float velocityMag2)` signature already).
|
||||
|
||||
Full structure read from pseudo-C:276694-276822 (**FACT**, direct read, not
|
||||
Ghidra):
|
||||
|
||||
```
|
||||
void CPhysicsObj::calc_friction(dt, velocityMag2) {
|
||||
if ((transient_state & 2) != 0) { // OnWalkable (see below)
|
||||
if ((state & MASK) == 0) { // MASK: BN-garbled string constant, see below
|
||||
// ---- Branch A ----
|
||||
dot = dot(contact_plane.N, velocity); // order N·v
|
||||
if (!p_5) // p_5 from (dot < 0.25f) comparison, x87-ambiguous
|
||||
velocity -= dot * contact_plane.N;
|
||||
friction = this->friction; // read field (decompiler shows bare read;
|
||||
// almost certainly `1.0f - this->friction`
|
||||
// collapsed/elided — see ACE cross-check)
|
||||
label_50f00e:
|
||||
velocity *= pow(friction, dt); // shared tail with Branch B
|
||||
} else {
|
||||
// ---- Branch B ----
|
||||
dot = dot(velocity, contact_plane.N); // same value, operand order swapped
|
||||
if (!p_1) { // p_1 from (dot < 0.25f), x87-ambiguous
|
||||
friction = 0.2f; // LOCAL default inside this branch
|
||||
velocity -= dot * contact_plane.N;
|
||||
p_2 = (velocityMag2 ⋛ 1.5625f); // x87-ambiguous direction
|
||||
if (p_2) {
|
||||
p_3 = (velocityMag2 ⋛ 6.25f); // x87-ambiguous direction
|
||||
if (p_3)
|
||||
p_4 = (cos(10°=0.17453292519943295 rad) ⋛ contact_plane.N.z); // x87-ambiguous
|
||||
if (!p_3 || !p_4)
|
||||
friction = this->friction; // fall back to object's own friction
|
||||
}
|
||||
goto label_50f00e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Constants confirmed **FACT** by direct read: `0.25f` (both branches,
|
||||
independently re-derived — not copy-paste, two separate x87 loads),
|
||||
`0.2f`, `1.5625f`, `6.25f`, `0.17453292519943295` (=10° in radians, fed
|
||||
to `__fcos`). The outer gate bit is `transient_state & 2`.
|
||||
|
||||
### Cross-check: ACE `PhysicsObj.calc_friction` (FACT, `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2120-2141`)
|
||||
|
||||
```csharp
|
||||
public void calc_friction(double quantum, float velocity_mag2)
|
||||
{
|
||||
if (!TransientState.HasFlag(TransientStateFlags.OnWalkable)) return;
|
||||
|
||||
var angle = Vector3.Dot(Velocity, ContactPlane.Normal);
|
||||
if (angle >= 0.25f) return;
|
||||
|
||||
Velocity -= ContactPlane.Normal * angle;
|
||||
|
||||
var friction = Friction; // this->friction — SAME baseline in ALL cases
|
||||
if (State.HasFlag(PhysicsState.Sledding))
|
||||
{
|
||||
if (velocity_mag2 < 1.5625f)
|
||||
friction = 1.0f;
|
||||
else if (velocity_mag2 >= 6.25f && ContactPlane.Normal.Z > 0.99999536f)
|
||||
friction = 0.2f;
|
||||
}
|
||||
|
||||
var scalar = (float)Math.Pow(1.0f - friction, quantum);
|
||||
Velocity *= scalar;
|
||||
}
|
||||
```
|
||||
|
||||
`TransientStateFlags.OnWalkable = 0x2` (FACT,
|
||||
`references/ACE/Source/ACE.Server/Physics/PhysicsEngine.cs:11`) — matches
|
||||
the decomp's `transient_state & 2` outer gate exactly, and matches
|
||||
acdream's own `TransientStateFlags.OnWalkable` bit already.
|
||||
|
||||
**This resolves the "state gate" mystery differently than the register's
|
||||
current framing.** ACE shows ONE linear function, not two branches — the
|
||||
`state`/`MASK` test that BN rendered as two duplicated blocks is
|
||||
`PhysicsState.Sledding` (`SLEDDING_PS = 0x800000`, confirmed **FACT** —
|
||||
`acclient.h:2838` places it directly in the `PhysicsState` enum next to
|
||||
`EDGE_SLIDE_PS = 0x400000`, and `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2130`
|
||||
gates on exactly that flag with the exact same 1.5625/6.25/near-1.0
|
||||
constants). The BN decompiler almost certainly duplicated a single
|
||||
`if (state & SLEDDING_PS) { ... } ` block into what read as two
|
||||
near-mirror branches — this is a known BN artifact class
|
||||
(`feedback_bn_decomp_field_names.md`). **ACE-derived reading, mark
|
||||
Ghidra-verify**: I could not confirm from the raw pseudo-C alone why the
|
||||
branches read as fully separate blocks rather than one `if`; a live
|
||||
Ghidra decompile of `0050ee70` would settle whether the source truly had
|
||||
duplicated logic (possible if the original C++ had two near-identical
|
||||
inlined call sites) or whether this is purely a BN rendering artifact. Do
|
||||
not restructure the port around "two branches" — port ACE's single
|
||||
linear shape; it is structurally consistent with every constant the raw
|
||||
decomp independently confirms.
|
||||
|
||||
**The threshold-direction ambiguity (`p_5`/`p_1`/`p_2`/`p_3`/`p_4`, all
|
||||
`test ah,0x5`-class x87 flag tests) is NOT independently Ghidra-verified
|
||||
this pass** (Ghidra MCP is down). ACE's clean `if (angle >= 0.25f) return;`
|
||||
is adopted as the **ACE-derived, Ghidra-verify** reading for `p_5`/`p_1`
|
||||
polarity. The `p_2`/`p_3`/`p_4` velocity-magnitude-band and slope-flatness
|
||||
polarities are likewise **ACE-derived, Ghidra-verify**.
|
||||
|
||||
**⚠️ Constant discrepancy found (FACT, needs Ghidra-verify to resolve):**
|
||||
the raw decomp's slope test literally computes `__fcos(0.17453292519943295)`
|
||||
(= cos(10°) ≈ 0.984808) and compares it against `contact_plane.N.z`.
|
||||
ACE's port instead compares `ContactPlane.Normal.Z > 0.99999536f` directly
|
||||
— no `cos()` call, and 0.99999536 corresponds to an angle of only
|
||||
≈0.175° from flat (`acos(0.99999536) ≈ 0.175°`), not 10°. These are
|
||||
**physically very different tests** (cos(10°) accepts any slope within
|
||||
10° of flat; 0.99999536 accepts only essentially-perfectly-flat ground).
|
||||
Two hypotheses, neither confirmed:
|
||||
(a) BN misdecompiled a raw float-constant load as an `__fcos()` call
|
||||
(a known BN artifact class — spurious x87 opcode reinterpretation);
|
||||
(b) ACE's own decompile/port made an independent error and
|
||||
`cos(10°)=0.984808` is correct.
|
||||
**Do not silently pick one.** File as an open Ghidra question (§7);
|
||||
when Ghidra MCP is back, decompile `0050ee70` directly and check
|
||||
whether the FCOS opcode is actually present at that instruction, or
|
||||
whether it's a raw `FLD` of `0.99999536` (or of `0.984808`).
|
||||
|
||||
### Why the L.3c naive threshold bump (0.0 → 0.25) hammered walking — and why that may no longer be true today (FACT + INFERENCE, high confidence, code-derived)
|
||||
|
||||
`src/AcDream.Core/Physics/PhysicsBody.cs:576-602` is acdream's current
|
||||
`calc_friction`. Its threshold is **0.0** (`if (dot >= 0f) return;`),
|
||||
attributed to the OLDER, unnamed Ghidra decomp (`FUN_0050f940` — a
|
||||
DIFFERENT address than the named function `0050ee70`; the two decomp
|
||||
passes disagree, and per CLAUDE.md the named decomp wins). The named
|
||||
decomp's independently-confirmed **0.25f** (both branches) is FACT-level
|
||||
confirmation that ACE's 0.25f reading — and the register's AP-7 row — are
|
||||
correct, and acdream's in-code comment ("we match the decompile" at 0.0)
|
||||
was matching the wrong (superseded, unnamed) decomp pass.
|
||||
|
||||
The recorded L.3c failure (2026-04-30): bumping the threshold alone to
|
||||
0.25f, with NO other change, dropped measured forward locomotion from
|
||||
~3 m/s to ~0.16 m/s (≈5.3% remaining) in
|
||||
`PlayerMovementControllerTests`. The math checks out exactly:
|
||||
`Friction = DefaultFriction = 0.95f` (confirmed **FACT** — both
|
||||
`src/AcDream.Core/Physics/PhysicsBody.cs:120` and
|
||||
`references/ACE/Source/ACE.Server/Physics/PhysicsGlobals.cs:15` agree on
|
||||
`0.95f`, so this is NOT a divergent constant), and flat-ground walking
|
||||
has `dot(velocity, groundNormal) ≈ 0` (velocity is ~horizontal, normal is
|
||||
~vertical). With threshold 0.0, `dot(≈0) >= 0` triggers the early return
|
||||
— friction NEVER engaged during flat walking. With threshold 0.25,
|
||||
`dot(≈0) < 0.25` — friction ALWAYS engaged, decaying velocity by
|
||||
`pow(1 − 0.95, dt)` = `0.05^dt` EVERY tick. At 60 Hz over 1 second:
|
||||
`0.951^60 ≈ 0.049` — a ~95% velocity loss in one second. That is the
|
||||
observed 3 → 0.16 m/s hammering almost exactly.
|
||||
|
||||
**INFERENCE, code-derived (high confidence, not yet live-verified):** the
|
||||
L.3c test predates the 2026-07-17 "local player animation-owned grounded
|
||||
movement" landing (R6). Reading current
|
||||
`src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:1742-1756`:
|
||||
|
||||
```csharp
|
||||
if (_body.OnWalkable)
|
||||
{
|
||||
float savedWorldVz = _body.Velocity.Z;
|
||||
if (hasAnimationRootMotion)
|
||||
{
|
||||
_body.Velocity = new Vector3(0f, 0f, savedWorldVz); // <-- XY ZEROED
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 stateVelocity = _motion.get_state_velocity();
|
||||
_body.set_local_velocity(
|
||||
new Vector3(stateVelocity.X, stateVelocity.Y, savedWorldVz),
|
||||
autonomous: _body.LastMoveWasAutonomous);
|
||||
}
|
||||
}
|
||||
...
|
||||
_body.UpdatePhysicsInternal(tickDt); // calls calc_friction internally
|
||||
```
|
||||
|
||||
When the graphical client's animation root motion drives the walk
|
||||
(`hasAnimationRootMotion == true`, the production path since R6),
|
||||
`_body.Velocity.X/Y` are forced to **zero** immediately before
|
||||
`calc_friction` runs every tick — because walking displacement now comes
|
||||
from `pmDelta.Origin` (the animation Frame delta, applied directly to
|
||||
`_body.Position` at lines 1764-1766), not from integrating `Velocity`.
|
||||
**Friction decaying an already-zero horizontal Velocity is a no-op.** This
|
||||
means the L.3c hammering mechanism is very likely ARCHITECTURALLY MOOT
|
||||
for the production graphical local-player path today — the regression
|
||||
that blocked AP-7 in April may not reproduce post-R6.
|
||||
|
||||
The `else` branch (no animation root motion — the headless/test-controller
|
||||
path, `_motion.get_state_velocity()`) still feeds real XY speed into
|
||||
`Velocity`, so that path (used by Slice K headless bots and any test
|
||||
without an attached animation source) is still exposed to the same
|
||||
hammering risk a naive threshold-only port would reintroduce.
|
||||
|
||||
**Action for the implementer, not yet executed by this research pass:**
|
||||
before porting, re-run `PlayerMovementControllerTests` (or an equivalent
|
||||
fresh capture) with a 0.25f threshold and the corrected ACE-derived
|
||||
single-linear-function shape, checked separately against (a) the
|
||||
graphical/animated local-player path, (b) the headless/`get_state_velocity`
|
||||
path, and (c) remote/NPC movers (`RuntimeRemotePhysicsUpdater.cs`,
|
||||
`RemoteMotion.cs` — confirm whether their Velocity is root-motion-zeroed
|
||||
the same way, or whether they remain velocity-integrated and therefore
|
||||
friction-sensitive). Do not assume (a) is safe without a fresh capture —
|
||||
this section's confidence is code-derived, not measured.
|
||||
|
||||
### AP-7 verdict
|
||||
|
||||
**Port shape:** replace acdream's two-threshold, dead-Sledding-branch
|
||||
`calc_friction` with ACE's single linear function (0.25f threshold, single
|
||||
`friction = Friction` baseline, `PhysicsState.Sledding`-gated override
|
||||
using the already-present 1.5625/6.25/near-flat constants — acdream
|
||||
already has these at `PhysicsBody.cs:591-597`, just unreachable because
|
||||
nothing ever sets the state bit; see §3). Flag the cos(10°) vs 0.99999536
|
||||
discrepancy (Ghidra-verify) and pick ACE's `0.99999536f` provisionally
|
||||
since acdream's own current dead code already uses it (least churn) —
|
||||
**do not silently resolve the discrepancy by picking one without a
|
||||
citation in the eventual commit; carry the open question into the
|
||||
register row.**
|
||||
|
||||
## 2. TS-1 — PrecipiceSlide / EdgeSlide / CliffSlide chain
|
||||
|
||||
### ⚠️ Major finding: the register/plan framing is STALE — TS-1 is already substantially ported (FACT, code-verified)
|
||||
|
||||
The register row (`docs/architecture/retail-divergence-register.md:238`)
|
||||
says: *"PrecipiceSlide context missing — conservative stop-at-edge
|
||||
instead of retail's EdgeSlide → PrecipiceSlide / CliffSlide"*, citing
|
||||
`TransitionTypes.cs:1254`. **Line 1254 today is unrelated stepping-loop
|
||||
code (the viewer last-step-remainder computation)** — the file has moved
|
||||
substantially since that row was written. Reading the actual current
|
||||
implementation:
|
||||
|
||||
- `SpherePath.PrecipiceSlide(Transition)` — `TransitionTypes.cs:943-970`
|
||||
— a real port of `SPHEREPATH::precipice_slide` (pseudo-C:274316,
|
||||
`0050cc80`), calling `BSPQuery.FindCrossedEdge` (a real port of
|
||||
`CPolygon::find_crossed_edge`, pseudo-C:322909, `00539300` —
|
||||
`BSPQuery.cs:438-482`), sign-flipping via a dot product exactly like
|
||||
retail's `x87_r7_17` sign test, then delegating to
|
||||
`Transition.SlideSphereInternal` exactly like retail's tail call to
|
||||
`CSphere::slide_sphere`.
|
||||
- `Transition.CliffSlide(Plane)` — `TransitionTypes.cs:2037-2102` — a
|
||||
real port of `CTransition::cliff_slide` (pseudo-C:272397, `0050a6d0`):
|
||||
cross product of the two plane normals, Z-flattened, rotated 90° in
|
||||
the XY plane (`(-Y, X, 0)`), degenerate-check, sign-resolved offset
|
||||
application via `SetCollisionNormal`. **Return-value mapping verified
|
||||
FACT-correct against `acclient.h:6100-6108`
|
||||
(`OK_TS=1, COLLIDED_TS=2, ADJUSTED_TS=3, SLID_TS=4`):** the degenerate
|
||||
case returns `TransitionState.OK` (matches retail's `return 1;` =
|
||||
`OK_TS`); the success case returns `TransitionState.Adjusted` (matches
|
||||
retail's `return 3;` = `ADJUSTED_TS`). This is correctly ported, not
|
||||
guessed.
|
||||
- `Transition.EdgeSlideAfterStepDownFailed` — `TransitionTypes.cs:1907-2035`
|
||||
— a dispatcher structurally mirroring `CTransition::edge_slide`
|
||||
(pseudo-C:273001-273090, `0050b3d0`), including the AP-4-registered
|
||||
reordering (steep-contact-plane CliffSlide check moved before the
|
||||
`!OnWalkable || !EdgeSlide` bail, to compensate for acdream's
|
||||
OnWalkable bookkeeping — see DO-NOT-RETRY §0 item 9).
|
||||
|
||||
**This means TS-1's row and the P2 plan's "port the EdgeSlide → PrecipiceSlide
|
||||
/ CliffSlide chain" framing describe work that is largely DONE.** The
|
||||
register row was not retired in the same commit that landed this — a
|
||||
process gap, not a code gap. **Recommend re-verifying with a live capture
|
||||
before writing new code**, not blindly re-porting from scratch.
|
||||
|
||||
### Retail source, quoted in full (FACT, pseudo-C:273001-273090, `0050b3d0`)
|
||||
|
||||
```
|
||||
TransitionState CTransition::edge_slide(arg2/*out*/, arg3=step_down_height, arg4=zVal) {
|
||||
state = object_info.state
|
||||
if ((state & 2) == 0 || (state.byte[1] & 2) == 0) { // NOT walkable-capable / NOT EdgeSlide-capable
|
||||
walkable = null; check_pos = backup_check_pos; check_cell = backup_cell;
|
||||
contact_plane_valid = 0; contact_plane_is_water = 0;
|
||||
*arg2 = OK_TS; return cache_global_sphere(null);
|
||||
}
|
||||
if (contact_plane_valid) {
|
||||
p_1 = (contact_plane.N.z - arg4) ⋛ 0 // x87-ambiguous, ACE-derived: "N.z >= zVal"
|
||||
if (!p_1) { // contact steeper than allowed (zVal = FloorZ or LandingZ)
|
||||
walkable = null; restore_check_pos();
|
||||
*arg2 = cliff_slide(this, &contact_plane);
|
||||
contact_plane_valid = 0; contact_plane_is_water = 0;
|
||||
return 0;
|
||||
}
|
||||
// else: falls through (contact IS walkable-steep-enough)
|
||||
}
|
||||
if (walkable != null) {
|
||||
restore_check_pos();
|
||||
contact_plane_valid = 0; contact_plane_is_water = 0;
|
||||
result = precipice_slide(sphere_path, collision_info);
|
||||
*arg2 = result;
|
||||
return (result == COLLIDED_TS);
|
||||
}
|
||||
if (contact_plane_valid) { // walkable was null, contact fell through as OK
|
||||
walkable = null; restore_check_pos(); cell_array_valid = 1;
|
||||
contact_plane_valid = 0; contact_plane_is_water = 0;
|
||||
*arg2 = OK_TS; return ...;
|
||||
}
|
||||
// ---- back-probe fallback: neither contact nor walkable available ----
|
||||
offset = global_curr_center - global_sphere.center; // back toward where we came from
|
||||
add_offset_to_check_pos(offset);
|
||||
step_down(this, arg3, arg4);
|
||||
contact_plane_valid = 0; contact_plane_is_water = 0;
|
||||
restore_check_pos();
|
||||
if (walkable == 0) {
|
||||
walkable = null; *arg2 = COLLIDED_TS; cell_array_valid = 1; return ...;
|
||||
}
|
||||
contact_plane_valid = 0; contact_plane_is_water = 0;
|
||||
walkable_scale = sphere_path.walkable_scale;
|
||||
cache_localspace_sphere(get_walkable_pos(sphere_path), walkable_scale); // <-- NOT PRESENT IN ACDREAM
|
||||
set_walkable_check_pos(sphere_path, localspace_sphere); // <-- NOT PRESENT IN ACDREAM
|
||||
result = precipice_slide(sphere_path, collision_info);
|
||||
*arg2 = result;
|
||||
return (result == COLLIDED_TS);
|
||||
}
|
||||
```
|
||||
|
||||
### Precise, evidenced gap #1: the back-probe fallback path skips retail's localspace re-cache before its second `precipice_slide` call (FACT)
|
||||
|
||||
Retail's edge_slide has **two distinct `precipice_slide` call sites**:
|
||||
one direct (when `sphere_path.walkable != 0` already), one in the
|
||||
back-probe fallback (when NEITHER a valid contact plane NOR a walkable
|
||||
polygon survived) — and ONLY the second site performs
|
||||
`walkable_scale`/`cache_localspace_sphere`/`get_walkable_pos`/
|
||||
`set_walkable_check_pos` first (pseudo-C:274318-274326, `0050b4e0-0050b507`).
|
||||
|
||||
acdream's `SpherePath` has **no** `WalkableScale`, `LocalspaceSphere`,
|
||||
`GetWalkablePos`, or `SetWalkableCheckPos` member/method anywhere
|
||||
(confirmed by grep across `TransitionTypes.cs` — zero hits), and its
|
||||
single unified `PrecipiceSlide(Transition)` method
|
||||
(`TransitionTypes.cs:943-970`) is called identically from BOTH the
|
||||
direct branch3 case (`TransitionTypes.cs:2002`, matches retail's first
|
||||
call site correctly — no re-cache needed there either) AND the back-probe
|
||||
fallback (`TransitionTypes.cs:2031`, **should** match retail's second
|
||||
call site but is missing the re-cache step retail performs first).
|
||||
|
||||
**Port-ready shape (INFERENCE for the exact field semantics — the
|
||||
walkable_scale/localspace-sphere machinery itself needs a fresh read of
|
||||
`SPHEREPATH::get_walkable_pos`/`cache_localspace_sphere`/
|
||||
`set_walkable_check_pos`, not yet read this pass; FACT that the gap
|
||||
exists, INFERENCE on the fix shape):** add the three missing
|
||||
`SpherePath` members/methods, call them in
|
||||
`EdgeSlideAfterStepDownFailed`'s final fallback block
|
||||
(`TransitionTypes.cs:2015-2031`) immediately before the
|
||||
`sp.PrecipiceSlide(this)` call at line 2031, matching retail's ordering.
|
||||
`get_walkable_pos`/`cache_localspace_sphere`/`set_walkable_check_pos`
|
||||
were not read this pass (budget) — **read them fresh before porting**
|
||||
(pseudo-C, search near `SPHEREPATH::get_walkable_pos` — a hit already
|
||||
appears in the `edge_slide` grep at the top of this section's raw quote,
|
||||
so the symbol exists and is findable).
|
||||
|
||||
### Precise, evidenced gap #2 (likely unregistered adaptation, not a bug per se): `CliffSlide`'s reference-normal fallback chain is an acdream invention (FACT: not in the retail read; adaptation reasoning IS documented in-code)
|
||||
|
||||
Retail's `cliff_slide` (pseudo-C:272397) uses
|
||||
`this->collision_info.last_known_contact_plane.N` **directly, with no
|
||||
fallback chain** as the second cross-product operand. acdream's
|
||||
`CliffSlide` (`TransitionTypes.cs:2037-2070`) instead tries THREE
|
||||
sources in priority order: `LastWalkablePlane` (if `Normal.Z >= FloorZ`),
|
||||
then `LastKnownContactPlane` (same threshold), then `Vector3.UnitZ`
|
||||
world-up. The in-code comment ("L.4-cliffslide-fallback", dated
|
||||
2026-04-30) explains the reasoning (degenerate cross-product when the
|
||||
player has been on a continuous steep slope for >1 frame and
|
||||
`LastKnownContactPlane` itself became steep) but **this reasoning does
|
||||
not appear to have a corresponding register row** (checked AP-4, TS-1 —
|
||||
neither mentions the fallback chain specifically; AP-4 is about
|
||||
re-ordering the CliffSlide-vs-Branch1 check, a different concern).
|
||||
**Flag for the implementer:** either (a) find this exact
|
||||
prioritization in a further retail read (unlikely given the raw decomp's
|
||||
directness, but not yet exhaustively ruled out — `last_known_contact_plane`
|
||||
itself might be retail-maintained differently than acdream's equivalent
|
||||
field, which could make the fallback chain compensate for an upstream
|
||||
divergence rather than being a pure invention), or (b) register it
|
||||
explicitly as an AD/AP row with this citation before or alongside the P2
|
||||
commit. Do not silently leave an unregistered behavioral invention in
|
||||
place while "retiring" the TS-1 row — that violates the register's
|
||||
same-commit rule.
|
||||
|
||||
### Precise, evidenced gap #3 (likely unregistered adaptation): the walkable-polygon steepness reroute in `EdgeSlideAfterStepDownFailed`
|
||||
|
||||
`TransitionTypes.cs:1972-1997` — when `sp.HasWalkablePolygon` is true,
|
||||
acdream additionally checks `sp.WalkablePlane.Normal.Z < FloorZ` and, if
|
||||
so, reroutes to `CliffSlide(sp.WalkablePlane)` INSTEAD of calling
|
||||
`PrecipiceSlide`. Retail's raw `edge_slide` (`if (walkable != null) { ...
|
||||
precipice_slide(...) }`) has **no steepness branch on the walkable
|
||||
polygon itself** — it always calls `precipice_slide` once `walkable !=
|
||||
null`. The in-code comment ("L.4-walkable-steep") argues this compensates
|
||||
for acdream's Path-4 airborne-landing branch accepting steep roofs as
|
||||
"walkable" under the permissive `LandingZ` threshold (a claim this
|
||||
research pass did not independently verify — Path-4 was not read this
|
||||
session; TS-4 §4 below covers the adjacent but distinct Path-6 concern).
|
||||
**Flag for the implementer:** same as gap #2 — verify whether this
|
||||
reroute is compensating for a real upstream divergence (in which case it
|
||||
should be an AD/AP row) or is masking a bug that should be fixed at its
|
||||
source (Path-4's LandingZ acceptance) instead of patched here. Do not
|
||||
retire TS-1 while leaving this unregistered.
|
||||
|
||||
### TS-1 verdict
|
||||
|
||||
**Port shape:** narrow, not a rewrite. (1) Add the retail
|
||||
`walkable_scale`/`cache_localspace_sphere`/`get_walkable_pos`/
|
||||
`set_walkable_check_pos` step to the back-probe fallback path only (gap
|
||||
#1 — a real, missing piece). (2) Audit gaps #2 and #3 against a fresh,
|
||||
focused retail re-read of `last_known_contact_plane` maintenance and the
|
||||
Path-4 landing-acceptance threshold; register whichever holds up as
|
||||
AD/AP rows, or align to retail exactly if the compensation turns out
|
||||
unnecessary. (3) Only THEN retire the TS-1 register row, in the same
|
||||
commit, updating its citation (the current `:1254` citation is already
|
||||
stale and should point at `EdgeSlideAfterStepDownFailed`/`CliffSlide`/
|
||||
`PrecipiceSlide` instead). **Do not** re-derive `edge_slide`/`cliff_slide`/
|
||||
`precipice_slide` from scratch — the existing port is real and largely
|
||||
correct; re-porting risks discarding correct, already-tested work (the
|
||||
CLAUDE.md worldbuilder-inventory lesson applies here by analogy: don't
|
||||
re-port what's already ported and tested).
|
||||
|
||||
## 3. #166 — landing sled (Sledding state set/clear)
|
||||
|
||||
### Finding: `PhysicsState.Sledding` appears to be DATA-AUTHORED, not an automatic landing response (FACT, cross-referenced across 3 independent repos)
|
||||
|
||||
Searched for a SET (`|= SLEDDING_PS`) or automatic-toggle call site for
|
||||
`PhysicsState.Sledding` in: the named-retail pseudo-C (no string "sled"
|
||||
anywhere in the 1.4M-line file — `grep -in sled` returns zero hits; the
|
||||
raw `0x800000` hex literal also returns zero physics-related hits, only
|
||||
unrelated Watson/crash-dump flags), `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs`
|
||||
(full file, only 3 hits: the two `calc_friction`/`UpdateObjectInternal`
|
||||
READ sites already covered, no WRITE site), and
|
||||
`references/ACViewer/ACE/...` (same ACE-derived code, same result).
|
||||
|
||||
The only WRITE sites for `PhysicsState.Sledding` anywhere in any
|
||||
reference repo are:
|
||||
|
||||
```csharp
|
||||
// references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Properties.cs:1105-1109
|
||||
public bool? Sledding
|
||||
{
|
||||
get => GetPhysicsState(PhysicsState.Sledding);
|
||||
set => SetPhysicsState(PhysicsState.Sledding, value);
|
||||
}
|
||||
```
|
||||
|
||||
— a per-weenie boolean **game-data property** (the same pattern as
|
||||
`Ethereal`, `Static`, etc.), packed onto the broadcast `PhysicsState` in
|
||||
`WorldObject_Networking.cs:586-588,700-704`. This is server/database-set,
|
||||
not a client-side automatic landing-response toggle. Since `CPhysicsObj`
|
||||
is shared code between the retail client and server binaries (the same
|
||||
class that produced `calc_friction`, `UpdateObjectInternal`, etc. — all
|
||||
independently confirmed to structurally match ACE's port), the total
|
||||
ABSENCE of a write site anywhere in ACE's ~10,000-line `PhysicsObj.cs`
|
||||
is strong evidence that retail's own `CPhysicsObj` **does not
|
||||
automatically enter Sledding state on a downhill landing either.**
|
||||
|
||||
**INFERENCE (well-supported, not yet cdb/Ghidra-confirmed):** ordinary
|
||||
downhill-jump glide-and-bounce in retail is NOT the literal `Sledding`
|
||||
physics state at all for an ordinary player. `Sledding` is most likely
|
||||
reserved for specific data-authored world content (dungeon/event objects
|
||||
with the property baked into their weenie default `PhysicsState`, e.g.
|
||||
an actual in-world "sled ride" mechanic) — a narrow, data-driven case
|
||||
outside a generic movement port's scope. Grepping WCID/weenie class name
|
||||
lists in `references/ACViewer/ACE/.../WeenieClassName.cs` for "sled"
|
||||
found no obviously-named sled-ride weenies, but that catalog is not
|
||||
exhaustive for retail's original 2013 content and this was not chased
|
||||
further (out of scope — data content, not an algorithm).
|
||||
|
||||
### Cross-check against ISSUES.md #166's OWN root-cause text (FACT)
|
||||
|
||||
`docs/ISSUES.md:4254-4262` (filed 2026-07-03, i.e. BEFORE the P2 campaign
|
||||
plan's phrasing) already attributes #166 to a **named composite of three
|
||||
rows**, and explicitly does **not** mention a Sledding set/clear
|
||||
mechanism:
|
||||
|
||||
> "This is the REGISTER-PREDICTED composite of three known deferred
|
||||
> deviations: **AD-25** (landing wall-bounce velocity reflection
|
||||
> suppressed...), **AP-7** (`calc_friction` threshold 0.0 without
|
||||
> retail's 0.25-with-state-gate...), and **TS-4** (Path-6 steep-poly
|
||||
> slide-tangent shortcut...). Retiring those three rows IS this issue."
|
||||
|
||||
Cross-referencing the digest's #182 rebuild notes
|
||||
(`memory/project_physics_collision_digest.md:837-841`, 2026-07-07):
|
||||
**AD-25's LOCAL-PLAYER landing-bounce reflection was already ported** in
|
||||
the #182 verbatim `UpdateObjectInternal`/`handle_all_collisions` rebuild
|
||||
("Contact committed BEFORE the reflect... retire AD-25's micro-bounce
|
||||
(AD-25 narrowed to the remote-DR sweep)"). What remains open for AD-25 is
|
||||
**remote/NPC-only** and is explicitly P3 scope
|
||||
(`docs/plans/2026-07-29-physics-parity-campaign.md` §P3 item 2), not P2.
|
||||
|
||||
**#166 verdict:** for the LOCAL PLAYER (the case the user actually
|
||||
reported — "jumping down a hill" on their own character), the bounce
|
||||
half (AD-25) is already shipped; what's missing is the glide-deceleration
|
||||
curve (AP-7, §1 above) and the airborne-steep landing chain (TS-4, §4
|
||||
below). Porting AP-7 + TS-4 correctly should retire #166 for the local
|
||||
player WITHOUT inventing any client-side Sledding auto-toggle — inventing
|
||||
one would be exactly the kind of unargued behavior addition CLAUDE.md's
|
||||
no-guessing rule forbids (no decomp evidence supports it). The campaign
|
||||
plan's P2 item 3 phrasing ("port the landing sled (Sledding state
|
||||
set/clear sites)") appears to rest on an assumption not borne out by this
|
||||
research pass.
|
||||
|
||||
**Recommendation for the implementer:** do NOT build a Sledding
|
||||
auto-set/clear mechanism speculatively. Land AP-7 + TS-4 first, capture a
|
||||
fresh downhill-jump-landing trajectory (extend
|
||||
`ACDREAM_CAPTURE_RESOLVE`), and check the #166 visual-matrix item (row 5,
|
||||
"Downhill jump landing: sled glide + bounce") against that alone. If the
|
||||
glide/bounce still visibly mismatches retail after AP-7+TS-4 land, THAT
|
||||
capture — not a guess — is what should drive any further Sledding-state
|
||||
work, and it should go through cdb against live retail (a downhill jump
|
||||
landing, watching `this->state` for the `0x800000` bit) before any client
|
||||
auto-toggle is written. Keep the already-present dead
|
||||
`PhysicsStateFlags.Sledding` branch in `calc_friction` (§1) since it's
|
||||
cheap, decomp-consistent, and harmless if never entered — but do not
|
||||
manufacture a caller that sets it.
|
||||
|
||||
## 4. TS-4 — Path-6 steep-poly shortcut removal
|
||||
|
||||
### The current shortcut (FACT, `src/AcDream.Core/Physics/BSPQuery.cs:2149-2266`)
|
||||
|
||||
Path-6 (the default `sphere_intersects_poly → collide_with_pt / SetCollide`
|
||||
dispatch) tests each hit polygon's world-space normal. For BOTH sphere0
|
||||
(feet) and sphere1 (head), if `worldNormal.Z < PhysicsGlobals.FloorZ`
|
||||
(steeper than ~49° from horizontal), acdream takes a SPECIAL BRANCH:
|
||||
projects the move along the steep face, writes
|
||||
`collisions.SetCollisionNormal(worldNormal)` **and**
|
||||
`collisions.SetSlidingNormal(worldNormal)`, and returns
|
||||
`TransitionState.Slid` immediately — bypassing `SetCollide` entirely for
|
||||
steep hits. Only the shallow case (`worldNormal.Z >= FloorZ`) reaches
|
||||
`path.SetCollide(worldNormal); path.WalkableAllowance = LandingZ; return
|
||||
Adjusted;`.
|
||||
|
||||
The in-code comment is unusually candid about why: **"This is a
|
||||
deliberate deviation from retail... Validated against retail debugger
|
||||
trace 2026-04-30: retail body did not wedge; our retail-faithful port DID
|
||||
wedge because we're missing implementation details of the step_up_slide /
|
||||
cliff_slide chain on grounded-steep movement."** — i.e., this shortcut
|
||||
was shipped SAME-DAY as (and BECAUSE) the retail-faithful
|
||||
`EdgeSlideAfterStepDownFailed`/`CliffSlide`/`PrecipiceSlide` chain (TS-1,
|
||||
§2 above — also dated 2026-04-30, tagged "L.4") still wedged in testing
|
||||
when tried without this shortcut.
|
||||
|
||||
### Retail: NO steepness branch at the BSP layer (FACT, pseudo-C:323740-323783, `0053a730` region)
|
||||
|
||||
Read directly from the named decomp (the `sphere_intersects_poly` /
|
||||
`set_collide` dispatch inside `BSPTREE::find_collisions`'s default path):
|
||||
|
||||
```
|
||||
if (sphere_intersects_poly(...) || eax_26 != 0) {
|
||||
localtoglobalvec(sphere_path.localspace_pos, &saved_ebx, &poly->plane.N);
|
||||
SPHEREPATH::set_collide(&sphere_path, &saved_ebx);
|
||||
sphere_path.walkable_allowance = 0.0871556997f; // = PhysicsGlobals.LandingZ, exact bit match
|
||||
return 3; // ADJUSTED_TS
|
||||
}
|
||||
```
|
||||
|
||||
**There is no steepness test here at all.** Retail's BSP layer calls
|
||||
`set_collide` and returns `ADJUSTED_TS` **unconditionally**, for a steep
|
||||
roof exactly the same as a shallow ramp. `walkable_allowance` is always
|
||||
set to `LandingZ` (the permissive landing threshold) at this layer,
|
||||
regardless of the actual polygon slope. This directly confirms the P2
|
||||
plan's description and the digest's #137-mechanism-2 finding
|
||||
(`memory/project_physics_collision_digest.md:1008-1014`): **retail's
|
||||
BSP/sphere collision layer never writes `collision_info.sliding_normal`
|
||||
at all** — only `validate_transition` (0x0050ac21/0x0050aa70) does, and
|
||||
only success-gated. The steepness differentiation (walkable vs.
|
||||
merely-in-contact, and whether a slide response is needed) happens
|
||||
STRICTLY DOWNSTREAM, in `ValidateTransition`'s `FloorZ` OnWalkable test
|
||||
and — when that surface turns out too steep to be walkable —
|
||||
`EdgeSlideAfterStepDownFailed` → `CliffSlide`/`PrecipiceSlide` (TS-1's
|
||||
domain, §2 above).
|
||||
|
||||
### TS-4 port shape (FACT-grounded, mechanically simple)
|
||||
|
||||
Delete both `if (worldNormal{0,1}.Z < PhysicsGlobals.FloorZ) { ... return
|
||||
TransitionState.Slid; }` blocks (`BSPQuery.cs:2200-2215` and
|
||||
`:2240-2255`) entirely. Both sphere0 and sphere1 hits should fall straight
|
||||
through to the existing `path.SetCollide(worldNormal); path.WalkableAllowance
|
||||
= PhysicsGlobals.LandingZ; return TransitionState.Adjusted;` — i.e., make
|
||||
Path-6 do EXACTLY what its own shallow branch already does, for every
|
||||
hit, matching retail's unconditional `set_collide`. This mechanically
|
||||
retires both `SetSlidingNormal` write sites (satisfying DO-NOT-RETRY §0
|
||||
item 1 permanently — deleted, not just avoided) with no replacement logic
|
||||
needed at this layer.
|
||||
|
||||
### ⚠️ Port-order coupling with TS-1 (INFERENCE, but directly evidenced by the shortcut's own commit history)
|
||||
|
||||
**This is the single most important sequencing fact in this whole
|
||||
document.** The shortcut's comment proves TS-1's retail-faithful chain
|
||||
was ALREADY BUILT once (same day, same "L.4" slice) and STILL wedged —
|
||||
that is why the shortcut exists instead of the faithful chain. Simply
|
||||
deleting the shortcut today, without first confirming TS-1's gaps (§2:
|
||||
missing localspace re-cache in the back-probe fallback; the two
|
||||
unregistered adaptations) are closed, risks reintroducing the EXACT
|
||||
"stuck in falling animation on the roof" / "walks up steep roofs" wedge
|
||||
that motivated the shortcut in the first place.
|
||||
|
||||
**Recommended order:** (1) close TS-1 gap #1 (the missing
|
||||
`walkable_scale`/`cache_localspace_sphere`/`set_walkable_check_pos` step)
|
||||
first, on its own, with the TS-4 shortcut still in place as a safety
|
||||
net. (2) Capture a grounded-steep-slope trajectory (a roof or steep
|
||||
terrain walk, matching whatever repro the 2026-04-30 L.4 session used —
|
||||
check `docs/research/` and git log around that date for the specific
|
||||
repro if it wasn't captured as a fixture) with the shortcut TEMPORARILY
|
||||
disabled behind a flag or in a scratch branch, and confirm no wedge. (3)
|
||||
Only once that capture is clean, delete the TS-4 shortcut for real, in
|
||||
the same commit that closes the TS-4 register row. (4) Re-run the P2
|
||||
final-matrix items 4-6 (cliff/roof edge, downhill landing, shallow wall
|
||||
graze) plus a fresh regression sweep before calling TS-4 done — this is
|
||||
exactly the kind of change the digest's #137 sagas warn compounds subtly
|
||||
(a leaked `SetSlidingNormal` "absorbing wedge at empty space" was the
|
||||
recurring failure mode across three separate historical incidents in the
|
||||
digest, all triggered by a similar not-quite-faithful shortcut).
|
||||
|
||||
## 5. #116 — slide-response family oracle pass
|
||||
|
||||
This item is explicitly an **oracle-first investigation**, not a known
|
||||
fix (`docs/ISSUES.md:8426` status: "OPEN (narrowed)"; digest
|
||||
`memory/project_physics_collision_digest.md:1509`: "OPEN — oracle-first
|
||||
investigation; NOT cell-set"). This section defines the SCOPE of the
|
||||
oracle pass precisely, per the mission's request, rather than proposing
|
||||
a fix — a fix without the live trace below would repeat the exact
|
||||
mistake the digest's DO-NOT-RETRY table already warns against (§0 items
|
||||
4, 7, 8).
|
||||
|
||||
### The two shapes (FACT, restated with citations — both already
|
||||
independently verified against source by the 2026-06-12 Ghidra session)
|
||||
|
||||
**Shape-1 — tick-22760 lateral-slide loss.** Live retail: blocked
|
||||
southward push at a cottage door face, KEPT a tiny lateral slide (X
|
||||
−0.0357 m, `collision_normal=(0,+1,0)`, the door face). acdream's harness
|
||||
hard-stops both components (`collision_normal=(0,0,1)`, i.e. the
|
||||
`UnitZ` ground-fallback default). Ghidra-confirmed
|
||||
(`memory/project_physics_collision_digest.md:1259-1275`):
|
||||
`TransitionTypes.cs:3701-3702`'s `UnitZ` default on invalid
|
||||
collision-normal is **retail-faithful** — retail's `validate_transition`
|
||||
(`0x0050aa70`) has the identical `if (collision_normal_valid==0)
|
||||
set_collision_normal(UnitZ)`. **The divergence is UPSTREAM of both slide
|
||||
and validate**: at tick-22760, acdream's `collision_normal_valid` was
|
||||
FALSE where retail's was TRUE (retail HAD recorded the door-face normal).
|
||||
The slide guard threshold is exonerated — the 3.57 cm lost slide is
|
||||
~18× above the ~1.4 cm degenerate-offset cutoff (`F_EPSILON` = 0.0002,
|
||||
compared against SQUARED magnitude — Ghidra-confirmed, see §0 item 8),
|
||||
so retail's own guard would have kept the slide too.
|
||||
|
||||
**Shape-2 — D4 first-airborne-frame slide vs. hard-stop.** Ghidra
|
||||
confirms `CSphere::slide_sphere` (`0x00537440`) applies its slide
|
||||
IN-FRAME (`add_offset_to_check_pos` → returns `SLID_TS`) — acdream's
|
||||
current in-frame slide to Z=1.92 on frame 1
|
||||
(`BSPStepUpTests.D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames`,
|
||||
`tests/AcDream.Core.Tests/Physics/BSPStepUpTests.cs:560-602`, currently
|
||||
`Skip`-tagged citing #116) is **likely faithful TO `slide_sphere`
|
||||
itself**. What's unconfirmed is whether retail's first airborne wall
|
||||
contact frame REACHES `slide_sphere` at all, or whether an earlier stage
|
||||
(`collide_with_environment`'s dispatch, or the absence of a
|
||||
`last_known_contact_plane` on the very first airborne frame) intercepts
|
||||
it with a hard stop before `slide_sphere` ever runs. The #116 threshold
|
||||
fix (`EpsilonSq` → `F_EPSILON`, shipped `bf18a543`) did **not** move D4 —
|
||||
confirming the D4 offset is a real slide, not a near-degenerate one the
|
||||
threshold fix would have caught.
|
||||
|
||||
### What the P2 oracle pass must determine (mission-specified scope)
|
||||
|
||||
1. **Shape-1's real root cause: where does acdream's collision-normal
|
||||
RECORDING diverge from retail's at tick-22760?** Not slide_sphere, not
|
||||
validate_transition (both exonerated) — the recording path that feeds
|
||||
`collision_info.collision_normal_valid`/`.collision_normal` during the
|
||||
BSP/environment hit-test itself. The digest's own next-step
|
||||
(`memory/project_physics_collision_digest.md:1274-1275`) is unchanged
|
||||
by this research pass: **instrument
|
||||
`DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals`**
|
||||
(`tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs:162`)
|
||||
to trace exactly where, in the BSP hit-test chain feeding
|
||||
`FindObjCollisionsInCell`/`BSPQuery`, the door-face normal
|
||||
`(0,+1,0)` gets computed-and-discarded in acdream but retained in
|
||||
retail. Candidate governing retail functions (need a fresh, focused
|
||||
read — not done this pass, budget): `BSPTREE::find_collisions`
|
||||
(the same dispatch region read for TS-4 in §4, pseudo-C ~323700-323830)
|
||||
and whatever populates `collision_info.collision_normal`/
|
||||
`.contact_plane` on a BLOCKED (not slid, not adjusted) door-face hit
|
||||
specifically — this is a DIFFERENT code path than either Path-6's
|
||||
`set_collide` (which returns ADJUSTED_TS, not COLLIDED_TS) or
|
||||
`slide_sphere` (called only after a walkable/precipice context
|
||||
exists). A blocked door push is most likely dispatched through
|
||||
`CollideWithPt`/`collide_with_pt` (the `PathClipped` branch already
|
||||
visible in Path-6, §4, `BSPQuery.cs:2158-2163`) or a sibling
|
||||
`Path-1`-class function not yet read this pass.
|
||||
2. **Shape-2's real answer: does retail's FIRST airborne wall-contact
|
||||
frame reach `slide_sphere`, or hard-stop upstream?** This needs either
|
||||
a Ghidra decompile of the caller chain immediately above
|
||||
`slide_sphere` (checking whether a `last_known_contact_plane`
|
||||
existence gate exists before the call on frame 1 of an airborne
|
||||
trajectory) or a live cdb trace of an actual airborne wall hit in
|
||||
retail (per the digest's already-written cdb plan,
|
||||
`memory/project_physics_collision_digest.md:8537-8543` /
|
||||
ISSUES.md #116). Ghidra MCP is down this pass — **do not guess this
|
||||
one; it directly controls which of D4's two competing expectations
|
||||
(Z=1.92 in-frame slide vs. Z=2.0 hard-stop-then-slide-frame-2) is
|
||||
correct**, and a wrong guess "regresses ALL wall-slide behavior" per
|
||||
the digest's own warning (§0 item 8).
|
||||
3. **Governing retail functions to anchor the oracle pass (FACT, already
|
||||
cited in ISSUES #116 and this document's own reads):**
|
||||
`CSphere::slide_sphere` (`0x00537440`, pseudo-C:321403-321532 per
|
||||
ISSUES #116's own citation — not re-read this pass, already
|
||||
Ghidra-verified for the epsilon fix), `validate_transition`
|
||||
(`0x0050aa70`, read indirectly via its `UnitZ` default confirmed in
|
||||
§2/§4's cross-references), and the `find_collisions`/environment
|
||||
hit-test recording sites this pass identified as the likely Shape-1
|
||||
location (`BSPTREE::find_collisions` region, pseudo-C ~323700-323830,
|
||||
the same region TS-4 §4 already partially read for the unconditional
|
||||
`set_collide` call — a focused re-read of the SIBLING branches in
|
||||
that same dispatch, specifically the `PathClipped`/`collide_with_pt`
|
||||
arm and whatever arm produces a hard COLLIDED_TS with a recorded
|
||||
normal, is the concrete next research step).
|
||||
|
||||
### #116 verdict — this is NOT a P2 implementation item, it is a P2
|
||||
research item with its own follow-up research session
|
||||
|
||||
Given the depth already logged in the digest and ISSUES.md (an entire
|
||||
Ghidra session, a threshold fix already shipped, two shapes precisely
|
||||
characterized, and an explicit "needs a LIVE cdb session" conclusion
|
||||
reached independently by that prior work), **this research pass concurs
|
||||
with the existing plan of record**: #116 needs (a) an instrumented
|
||||
replay of `DoorBugTrajectoryReplayTests` for shape-1, and (b) either a
|
||||
Ghidra decompile of `slide_sphere`'s caller chain or a live cdb trace of
|
||||
an airborne wall hit for shape-2, BEFORE any code changes. **Do not**
|
||||
patch the degenerate-offset guard, the `UnitZ` default, or `slide_sphere`
|
||||
itself speculatively (DO-NOT-RETRY §0 items 4, 7, 8 all apply directly).
|
||||
This document does not add new pseudocode for #116 beyond what's already
|
||||
recorded, because doing so without the trace would be exactly the kind
|
||||
of guess CLAUDE.md's workflow forbids.
|
||||
|
||||
## 6. Port order + blast radius
|
||||
|
||||
Recommended sequence, reasoning, and required fixtures/captures BEFORE
|
||||
each step's behavior change — not a generic "do them in plan order"
|
||||
list, because this research pass found real coupling the plan's slice
|
||||
ordering doesn't surface.
|
||||
|
||||
### Step 1 — TS-1 gap #1 (localspace re-cache in the back-probe fallback), §2
|
||||
|
||||
**Do this FIRST, alone.** It's the narrowest, most mechanically certain
|
||||
change (a missing setup step before an existing call, not a behavior
|
||||
redesign), and per §4's coupling analysis, TS-4's shortcut removal is
|
||||
UNSAFE until this lands and is proven not to wedge. Blast radius: only
|
||||
the back-probe fallback arm of `EdgeSlideAfterStepDownFailed` — the
|
||||
rarest of the four dispatch arms (reached only when neither a contact
|
||||
plane nor a walkable polygon survived the step-down probe). Low risk of
|
||||
regressing the already-working branch3/branch1/branch2 arms.
|
||||
|
||||
**Required before/after:** a targeted unit test exercising the back-probe
|
||||
arm specifically (check whether one already exists — this pass didn't
|
||||
find one in `BSPStepUpTests.cs`/`DoorBugTrajectoryReplayTests.cs`; if
|
||||
absent, write one first per TDD discipline). No connected capture needed
|
||||
for this step alone — it's a narrow internal-consistency fix.
|
||||
|
||||
### Step 2 — TS-1 gaps #2 and #3 (register audit), §2
|
||||
|
||||
Read the raw decomp fresh for `last_known_contact_plane` maintenance
|
||||
(what writes it, and whether acdream's equivalent field diverges upstream
|
||||
— which would justify gap #2's fallback chain as a real compensating
|
||||
adaptation) and for Path-4's airborne-landing `LandingZ` acceptance logic
|
||||
(cited but not read this pass — needed to judge gap #3). Either register
|
||||
both as AD/AP rows with citations, or remove the acdream-only branches
|
||||
and re-verify against the existing CliffSlide/PrecipiceSlide test
|
||||
coverage. This step can run in parallel with Step 1 (different files,
|
||||
no shared state — a candidate for `superpowers:dispatching-parallel-agents`
|
||||
if the implementer wants to split it out).
|
||||
|
||||
### Step 3 — capture a steep-slope / roof trajectory with TS-4's shortcut disabled
|
||||
|
||||
Before touching `BSPQuery.cs`, build a scratch/flagged path that runs
|
||||
Path-6 WITHOUT the steepness branch (i.e., always `SetCollide` +
|
||||
`WalkableAllowance=LandingZ` + `Adjusted`, matching retail) and replay
|
||||
whatever fixture reproduces the original "stuck in falling animation on
|
||||
the roof" / "walks up steep roofs" symptom the 2026-04-30 L.4 session
|
||||
used to justify the shortcut. **Find that fixture/repro before writing
|
||||
new code** — check `docs/research/2026-04-30-*` and git log around that
|
||||
date; if no fixture survives, this step needs a fresh capture via
|
||||
`ACDREAM_PROBE_RESOLVE=1` against a known steep-roof landblock. Do not
|
||||
skip this step even though Step 1 theoretically closes the gap that
|
||||
caused the original wedge — "theoretically closes" is not "proven not
|
||||
to wedge," and this exact failure mode (a not-quite-faithful shortcut
|
||||
masking a deeper gap) has recurred at least three times in the digest's
|
||||
own history (#137 mechanisms 1-3).
|
||||
|
||||
### Step 4 — land TS-4 (delete the shortcut) only after Step 3 is clean
|
||||
|
||||
Small, mechanical diff once Step 3 validates it's safe (§4's port shape).
|
||||
Retire the TS-4 register row in the same commit.
|
||||
|
||||
### Step 5 — AP-7, independently sequenced (no coupling to Steps 1-4)
|
||||
|
||||
AP-7 touches `PhysicsBody.calc_friction` only — a different file, no
|
||||
shared call path with the TS-1/TS-4 edge-response chain (friction runs
|
||||
on GROUNDED, already-resolved velocity; TS-1/TS-4 run during the
|
||||
collision SWEEP itself, upstream of where friction applies). Can be
|
||||
done in parallel with Steps 1-4. **Required before any code change**:
|
||||
re-run (or write, if it doesn't already isolate the right thing) a
|
||||
regression test capturing (a) graphical/animated local-player grounded
|
||||
walk speed (§1's finding: likely unaffected today, but UNVERIFIED —
|
||||
confirm before claiming victory), (b) headless/`get_state_velocity`-path
|
||||
grounded walk speed (§1's finding: likely STILL exposed to the original
|
||||
L.3c hammering mechanism — this is the one that needs the real fix, not
|
||||
just the threshold), and (c) a remote/NPC mover sample
|
||||
(`RuntimeRemotePhysicsUpdater.cs`/`RemoteMotion.cs` — not audited this
|
||||
pass for Velocity-zeroing behavter; check before assuming safety).
|
||||
|
||||
### Step 6 — #166 visual check, after Steps 1-5
|
||||
|
||||
Per §3's verdict: #166 is very likely closed BY PROXY once AP-7 (Step 5)
|
||||
and TS-4 (Step 4) land — no new code needed beyond those two. Verify
|
||||
against the P2 visual-matrix item 5 (downhill jump landing) LAST, using
|
||||
a fresh `ACDREAM_CAPTURE_RESOLVE` trajectory. Only if it still visibly
|
||||
mismatches retail after Steps 1-5 does #166 need further work, and that
|
||||
further work should be capture-driven, not a speculative Sledding
|
||||
auto-toggle (§3's explicit recommendation).
|
||||
|
||||
### Step 7 — #116, separately scoped, own research session
|
||||
|
||||
Per §5's verdict, this is not an implementation step at all yet — it
|
||||
needs its own instrumented-replay session (shape-1) and a
|
||||
Ghidra-availability-gated or live-cdb session (shape-2) before any
|
||||
pseudocode can be written. Sequence-independent of Steps 1-6 (different
|
||||
root cause layer — collision-normal recording, not response), but
|
||||
shares the same `slide_sphere`/`validate_transition` machinery TS-1
|
||||
depends on, so land it AFTER Steps 1-4 settle to avoid two people
|
||||
touching `TransitionTypes.cs`'s collision-normal plumbing at once.
|
||||
|
||||
### Fixtures/tests that must exist BEFORE any behavior change lands (mission requirement, consolidated)
|
||||
|
||||
- A back-probe-arm-specific unit test for TS-1 Step 1 (write if absent).
|
||||
- The steep-roof/wedge repro fixture for TS-4 Step 3 (locate or
|
||||
recapture).
|
||||
- AP-7's three-way regression test (graphical/animated, headless
|
||||
state-velocity, remote/NPC) for Step 5.
|
||||
- A fresh `ACDREAM_CAPTURE_RESOLVE` downhill-landing trajectory for #166
|
||||
Step 6.
|
||||
- An instrumented `DoorBugTrajectoryReplayTests` capture for #116
|
||||
shape-1 (Step 7) — extend the existing
|
||||
`Diagnostic_Tick22760_DumpEngineInternals` rather than writing a new
|
||||
harness.
|
||||
- Existing coverage that must NOT regress: `SphereCollisionFamilyTests`,
|
||||
`Issue137CorridorSeamReplayTests`, `Issue137SlidingNormalLifecycleTests`,
|
||||
`WindowOpening_HeadCannotFit_EntryBlocked`, and the full
|
||||
`BSPStepUpTests`/`DoorBugTrajectoryReplayTests` suites (D4 stays
|
||||
`Skip`-tagged until #116 shape-2 is actually resolved by evidence, not
|
||||
by this campaign's other changes incidentally shifting its numbers).
|
||||
|
||||
## 7. Open questions needing Ghidra/cdb (not guessed here)
|
||||
|
||||
Consolidated from all five sections above, each tagged with which item
|
||||
it blocks.
|
||||
|
||||
1. **[AP-7]** Is the BN-rendered "two duplicated branches" in
|
||||
`calc_friction` (pseudo-C:276694-276822) a genuine BN decompiler
|
||||
artifact (single retail `if (state & SLEDDING_PS)` block misrendered),
|
||||
or does retail's actual source have two structurally separate paths?
|
||||
A live Ghidra decompile of `0050ee70` settles it. Low implementation
|
||||
risk either way (ACE's single-linear-function reading is adopted
|
||||
regardless), but affects how confidently the port shape can be
|
||||
described as "verified" vs. "ACE-derived."
|
||||
2. **[AP-7]** The `p_5`/`p_1` polarity (does the friction-apply branch
|
||||
trigger on `dot < 0.25` or `dot >= 0.25`?) and the `p_2`/`p_3`/`p_4`
|
||||
velocity-magnitude-band/slope-flatness polarities are all
|
||||
`test ah,0x5`-class x87 flag tests, none independently Ghidra-verified
|
||||
this pass. ACE's clean reading is adopted as ACE-derived; a Ghidra
|
||||
decompile of `0050ee70` (same function as item 1) would settle all of
|
||||
these at once.
|
||||
3. **[AP-7]** The `cos(10°)` (raw decomp) vs. `0.99999536f` (ACE)
|
||||
discrepancy in the Sledding slope-flatness test — genuinely different
|
||||
physical behaviors, not a rounding difference. Needs a Ghidra
|
||||
decompile of `0050ee70` checking whether the FCOS opcode is real or a
|
||||
BN misread of a raw float constant load. Currently unresolved; §1
|
||||
provisionally recommends ACE's `0.99999536f` (least churn from
|
||||
acdream's existing dead code) but flags this explicitly as
|
||||
unconfirmed.
|
||||
4. **[TS-1 gap #1]** `SPHEREPATH::get_walkable_pos`,
|
||||
`cache_localspace_sphere`, and `set_walkable_check_pos` were located
|
||||
(symbol exists, called at pseudo-C:274318-274326) but NOT read this
|
||||
pass (effort budget). A fresh grep-and-read of these three functions
|
||||
is needed before porting Step 1 — this is a same-tool (grep-named)
|
||||
follow-up, not a Ghidra/cdb blocker; flagged here only so it isn't
|
||||
lost.
|
||||
5. **[TS-1 gaps #2, #3]** Whether `last_known_contact_plane` maintenance
|
||||
and Path-4's `LandingZ` acceptance logic genuinely diverge from
|
||||
retail (justifying acdream's CliffSlide fallback chain and the
|
||||
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).
|
||||
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
|
||||
`set_collide` arm read for TS-4), does acdream fail to record the
|
||||
door-face collision normal that retail records at tick-22760? Needs
|
||||
an instrumented replay (§5 Step 7), not Ghidra/cdb per se, but the
|
||||
candidate function itself would benefit from a clean Ghidra
|
||||
decompile alongside the BN pseudo-C already read.
|
||||
8. **[#116 shape-2]** Does retail's first airborne wall-contact frame
|
||||
reach `CSphere::slide_sphere` (→ in-frame slide, Z=1.92-style), or
|
||||
hard-stop upstream via some `last_known_contact_plane`-existence gate
|
||||
(→ Z=2.0-style, sliding deferred to frame 2)? This is the one item in
|
||||
this whole document that the digest, ISSUES.md, AND this research
|
||||
pass all independently converge on: **it needs a live cdb trace of
|
||||
retail landing an airborne wall hit** (toolchain: `docs/architecture`
|
||||
references the CLAUDE.md "Retail debugger toolchain" section; the
|
||||
digest already has a scoped cdb script sketch at
|
||||
`memory/project_physics_collision_digest.md:8537-8543`). A wrong
|
||||
guess here, per the digest's own words, "regresses ALL wall-slide
|
||||
behavior."
|
||||
|
||||
---
|
||||
Loading…
Add table
Add a link
Reference in a new issue