acdream/docs/research/2026-08-07-s4b-validate-walkable-bytepin.md
Erik 712244b8aa docs: S4b's STOP fired — retail is PLANT-THEN-LIFT; the reland maps to AD-66's bare radius alone
The contract's D0 byte-pin refuted the tangent-placement premise:
validate_walkable @0x0050d010 is planted for every normal mover
(Ghidra + BN + ACE agree; ours is already byte-faithful; only the
camera branch is tangent). With the byte-proven bare-radius push-out
the coherent retail mechanism is plant-then-lift — the push fires once
per settle, raises the body to tangent equilibrium, and both checks go
quiet there. The slope float comes from the push, not the placement,
exactly as the original substitution's own comment argued. The 84%
live fire rate was measured against our push-disabled steady state;
the #341 assert-shape flip now reads as order-dependent settle state.
The user's 'port the retail pair' therefore maps to relanding AD-66's
bare radius alone, with the ten-run stability protocol.

The STOP rule paid for itself: an implementer without it would have
made ValidateWalkable DIVERGE from retail in the name of faithfulness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:46:16 +02:00

14 KiB
Raw Permalink Blame History

S4b D0 — byte-pin of OBJECTINFO::validate_walkable's distance basis

Date: 2026-08-07. Author: S4b implementer (single Sonnet agent, no subagents). Status: PREMISE REFUTED — SLICE STOPPED AT D0. No code was changed. D1/D2/D3/D4 were not performed. Nothing is committed.

The question D0 was scoped to answer

Per docs/research/2026-08-07-s4b-tangent-rest-contract.md: does retail's OBJECTINFO::validate_walkable measure the perpendicular sphere clearance (dot(center,N)+D radius, tangent semantics — the same shape as CPolygon::adjust_sphere_to_plane), or a vertical foot point (lowPoint = center (0,0,radius), planted semantics — the shape acdream's current Transition.ValidateWalkable already implements)?

The contract's explicit, pre-authorized stop condition: "If retail's validate_walkable turns out to ALSO use a vertical foot point, STOP — the whole slice premise changes and the session lead re-decides."

Answer: vertical foot point (planted), for the branch that governs every normal mover (players, creatures — anything not the camera/viewer). The stop condition is triggered by direct byte evidence from two independent decompilers plus ACE's independent C# port. All three agree with each other and with acdream's current code.

Byte evidence

1. Ghidra decompile (primary; PDB-paired, patchmem.gpr, port 8081)

OBJECTINFO::validate_walkable @ 0x0050d010:

TransitionState __thiscall
OBJECTINFO::validate_walkable(OBJECTINFO *this, CSphere *param_1, Plane *param_2,
                               int param_3, float param_4, SPHEREPATH *param_5,
                               COLLISIONINFO *param_6, ulong param_7)
{
  ...
  if ((this->state & 4) != 0) {                       // IsViewer branch
    fVar2 = (N.x*center.x + N.y*center.y + N.z*center.z + d) - radius;   // BARE radius — tangent
    ...
    return OK_TS / ADJUSTED_TS;
  }
  // else branch — every normal mover (player, creature, missile, etc.)
  fVar2 = center.x*N.x + (center.z - radius)*N.z + center.y*N.y + d + param_4;
  //       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  //       = dot(center, N) + d - radius*N.z + waterDepth   -- PLANTED, not tangent
  if (-EPSILON <= fVar2) { ... return OK_TS; }
  else {
    if (param_5->check_walkable != 0) return COLLIDED_TS;
    fVar2 = fVar2 / N.z;   // zDist
    ... push sphere up by -fVar2 along Z, step-down interp, etc.
    return ADJUSTED_TS;
  }
}

Full raw decompile captured via GET /decompile_function?address=0x0050d010 against the currently-open patchmem.gpr project (Sept 2013 EoR build, same source as docs/research/named-retail/).

this->state & 4 is ObjectInfoState.IsViewer (= 0x4) — confirmed by references/ACE/Source/ACE.Server/Physics/ObjectInfo.cs:13 and by LandCell.cs:56's !objInfo.State.HasFlag(ObjectInfoState.IsViewer) && !objInfo.Object.State.HasFlag(PhysicsState.Missile) guard on env-collision processing. IsViewer marks the camera/eye collision object, not the player's own movement sphere. Every walking mover (player body, creature, NPC) takes the else branch — the planted one.

param_4 is waterDepth — confirmed by the caller (below): it is used exactly once, added directly after the d term: ... + param_2->d + param_4. It is NOT folded into the lowPoint subtraction — it modifies the threshold, not the sphere's virtual bottom. This is identical in shape and placement to ACE's ... contactPlane.D + waterDepth and to acdream's current contactPlane.D + waterDepth (TransitionTypes.cs:3673). AP-10's sink-in byte-pin: dist = dot(center,N) + D radius·N.z + waterDepth, unchanged from acdream's current formula.

2. Binary Ninja pseudo-C (secondary; corroborates, was initially ambiguous)

acclient_2013_pseudo_c.txt:274479-274617. The state & 4 branch (lines 274487-274488) computes ((N.x*cx + N.y*cy + cz*N.z) + d) - radius — bare radius, matching Ghidra's IsViewer arm exactly.

The else branch (lines 274531-274532) computes N.y*cy + (cz - radius)*N.z + N.x*cx + d — matching Ghidra's planted arm exactly, modulo one artifact: BN reuses the SSA name x87_r7_16 for both the bare radius value (line 274531) and the freshly-computed dot-product expression one line later (274532), because both values transiently occupy the same logical x87 stack slot. This is the "flag-idiom artifact class" the project CLAUDE.md warns about — a naming collision, not an algebraic one; the raw expression at 274532 is unambiguous regardless of what BN chose to call its result. arg5 (waterDepth) never appears by name inside BN's rendering of this function — another BN-only artifact (the float stack parameter gets folded directly into the compound expression without a preserved argN token). Ghidra's param_4 resolves this cleanly. This is exactly the "disassemble if BN is ambiguous" case D0 anticipated, and Ghidra is the byte-pin of record here.

3. ACE's independently-authored C# port (tertiary cross-check)

references/ACE/Source/ACE.Server/Physics/ObjectInfo.cs:101-172, ValidateWalkable:

if (State.HasFlag(ObjectInfoState.IsViewer))
{
    var dist = Vector3.Dot(checkPos.Center, contactPlane.Normal) + contactPlane.D - checkPos.Radius;
    ...  // bare radius — tangent
}
else
{
    var dist = Vector3.Dot(checkPos.Center - new Vector3(0, 0, checkPos.Radius), contactPlane.Normal)
                + contactPlane.D + waterDepth;
    ...  // vertical foot point — planted, waterDepth added after
}

Independently produced (ACE was reverse-engineered years before this project's PDB-named decomp existed), and it agrees with Ghidra byte-for-byte on both branches, including waterDepth's placement. Three independent sources, zero disagreement.

4. Caller context — confirms AD-69 is a separate, correctly-scoped finding

CLandCell::find_env_collisions @ 0x00532f20 (pseudo-C lines 317070-317113, Ghidra-cross-readable) is the sole caller of validate_walkable for outdoor terrain. It calls LandDefs::get_block_offset twice: once to convert global_low_point into the terrain-poly lookup's block frame (for find_terrain_poly), and again (0x00533007) to subtract the block offset from global_sphere->center before the sphere passed into validate_walkable is built. This is the cell-relative correction AD-69 already names, scoped to CTransition::adjust_offset @ 0x0050a370 (a different function, pc:272271-272393 per the register row) — not to validate_walkable. AD-69 is unaffected by this finding; it was already correctly scoped before this slice started.

5. CPolygon::adjust_sphere_to_plane @ 0x00538210 — re-confirmed independently, unaffected

Ghidra decompile (GET /decompile_function?address=0x00538210):

fVar3 = dot(N, center) + d;
fVar4 = dot(N, movement_dir);
if (fVar4 <= EPS) { if (-EPS <= fVar4) return 0; fVar3 -= radius; }   // bare radius
else               { fVar3 = -radius - fVar3; }                       // bare radius
fVar3 /= fVar4;
...

Bare radius, both branches — matches AD-66's byte evidence exactly and matches acdream's existing BSPQuery.cs:364 AdjustSphereToPlane (already a faithful port, confirmed by direct read: dist = dpPos - validPos.Radius / dist = -validPos.Radius - dpPos, bare Radius, no N.z factor). This function's tangent semantics are correct and were never in question.

What this means for the contract's premise

The contract's framing was: "acdream today mixes tangent (BSP walk solve) with planted (ValidateWalkable) and the planted one wins the resting height on terrain [instead of retail's own consistent tangent pairing]." That framing assumed retail pairs tangent-with-tangent (adjust_sphere_to_plane

  • validate_walkable both bare-radius) and that acdream's planted ValidateWalkable is therefore a deviation to fix.

The byte evidence shows retail itself pairs tangent-with-planted, not tangent-with-tangent:

Function Retail semantics acdream today
CPolygon::adjust_sphere_to_plane @0x00538210 (BSP walk / push-out during collision resolution) tangent, bare radius tangent, bare radius (BSPQuery.cs:364) — matches
OBJECTINFO::validate_walkable @0x0050d010, mover branch (per-tick terrain rest/step validation) planted, radius·N.z, waterDepth added after planted, radius·N.z, waterDepth added after (TransitionTypes.cs:3652-3730) — matches
CTransition::adjust_offset @0x0050a370 safety push-out (AD-66) tangent, bare radius (byte-confirmed, withheld pending #341) planted substitution (radius·N.z), the AD-66 deviation

acdream's ValidateWalkable is already a byte-faithful port of retail's mover-path formula — same low-point construction, same epsilon bracket (dist >= -EPSILON / dist <= EPSILON), same zDist = dist/N.z push-up, same step-down interpolation guard, same OnWalkable/Contact flag-setting conditions (state bits 1/2 map onto oi.Contact/oi.OnWalkable exactly). There is no ValidateWalkable distance-basis change to port — porting one would make acdream diverge from retail, not converge to it.

D1 item 1 (ValidateWalkable's distance basis → tangent) is refuted and must not be implemented. Because the contract frames the pair as inseparable ("The pair ports together or not at all" / "STOP — the whole slice premise changes"), the whole D1/D2/D3 program as specified is out, not just item 1 in isolation. Items 2 (AD-66 bare-radius landing in AdjustOffset) and 3 (AD-69's get_block_offset correction) remain independently well-founded by their own byte evidence and their own register rows — but landing them was explicitly conditioned by this contract on the ValidateWalkable pairing argument that just failed, and the #341 anomaly this slice was named for (the flipping absorbed-tick behavior on AD-66 alone) has not been explained by anything found here. Landing AD-66/AD-69 now, under a different justification than the one this contract authorized, is a decision for the session lead, not a call for the implementer to make unilaterally under a refuted contract.

What was NOT done (by design, per the contract's own stop clause)

  • D1 (port the pair) — not implemented. No changes to TransitionTypes.cs ValidateWalkable or AdjustOffset, no changes to BSPQuery.cs / FlatBspQuery.cs.
  • D2 (ten-run RuntimeRemoteUphillProgressTests retest) — not run under this slice's changes, because there are no changes to test. (The existing S4AdjustOffsetConformanceTests.Uphill_NoContactFlapAcrossTicks and the two [Skip]-ed AD-66 tests were read but left untouched and un-unskipped.)
  • D3 (conformance tests + sabotage) — not written.
  • D4 (register/ISSUES bookkeeping) — not performed; AD-66 and AD-69 remain exactly as they are (AD-66 ACTIVE/WITHHELD, AD-69 FILED, #341 open).
  • No files under src/ were edited. No commits were made.

Open question for the session lead

The #341 measurement anomaly (AD-66's bare radius alone producing contradictory absorbed-tick outcomes that flip with test-assert shape) is still unexplained. This slice's finding rules out "ValidateWalkable should also be tangent" as the fix, since that would itself be a retail deviation. Candidate directions the session lead may want to weigh (not investigated here — out of this slice's scope once D0 refuted the premise):

  • The anomaly may be a test-harness artifact (the tests' own docstring already flags "flipping with nothing but the shape of the test's post-tick asserts" as suspicious) rather than a real engine behavior difference.
  • AD-69's get_block_offset gap could be entangled with AD-66's in a way that only shows up when both change together — but that is a testable hypothesis, not yet tested.
  • The #331 absorb-tick semantics (explicitly OUT of scope for both this contract and S4b) may be the actual source of the flip, independent of AD-66/AD-69 entirely.

Citations

  • OBJECTINFO::validate_walkable @ 0x0050d010 (Ghidra patchmem.gpr decompile; BN pseudo-C acclient_2013_pseudo_c.txt:274479-274617)
  • ObjectInfoState.IsViewer = 0x4 (references/ACE/Source/ACE.Server/Physics/ObjectInfo.cs:13)
  • references/ACE/Source/ACE.Server/Physics/ObjectInfo.cs:101-172 (ValidateWalkable, independent port)
  • references/ACE/Source/ACE.Server/Physics/Common/LandCell.cs:56 (IsViewer/Missile env-collision guard)
  • CLandCell::find_env_collisions @ 0x00532f20 (acclient_2013_pseudo_c.txt:317070-317113; the get_block_offset calls and validate_walkable call site)
  • CPolygon::adjust_sphere_to_plane @ 0x00538210 (Ghidra decompile; AD-66's cited address, re-confirmed)
  • src/AcDream.Core/Physics/TransitionTypes.cs:3652-3730 (acdream's current ValidateWalkable — read directly, confirmed byte-faithful to the mover branch above)
  • src/AcDream.Core/Physics/TransitionTypes.cs:5501-5655 (acdream's current AdjustOffset, including the AD-66 WITHHELD comment block and AD-69's target dist computation)
  • src/AcDream.Core/Physics/BSPQuery.cs:364-428 (acdream's current AdjustSphereToPlane — read directly, confirmed byte-faithful bare-radius)
  • docs/architecture/retail-divergence-register.md rows AD-66 (line 65 header context), AD-69 (line 166)
  • tests/AcDream.Core.Tests/Physics/S4AdjustOffsetConformanceTests.cs (the two [Skip]-ed AD-66 tests, read but left untouched)
  • docs/research/2026-08-07-s4b-tangent-rest-contract.md (this slice's pinned contract; the stop clause invoked above is quoted from its D0 section verbatim)