fix #116 (partial, Ghidra-confirmed): slide_sphere degenerate guard uses F_EPSILON, not EpsilonSq

The user brought up Ghidra; its decompiler (patchmem.gpr, full PDB)
resolved the Binary-Ninja `test ah,5` x87 branch-sign ambiguity that
blocked the desk read. CSphere::slide_sphere (0x00537440) decompiles
cleanly to:

  fVar3 = |cross(collisionNormal, contactPlane.N)|²;
  if (::F_EPSILON <= fVar3) {                       // crease exists
      ... offset = cross * dot(cross,gDelta)/fVar3;
      if (|offset|² < ::F_EPSILON) return COLLIDED_TS;   // degenerate guard
      ... add_offset_to_check_pos -> SLID_TS
  }

Retail compares the SQUARED magnitudes against F_EPSILON
(0.000199999995 ~= 0.0002 = PhysicsGlobals.EPSILON). Our port compared
against EpsilonSq (0.0002^2 = 4e-8) - a ~5000x too-tight threshold (the
BN pseudo-C rendered the comparison as `test ah,5` after an x87 FCMP,
which is sign-ambiguous; agent reads disagreed). Fixed both comparisons
at TransitionTypes.cs:3098,3105 to EPSILON.

Effect: crease-exists now needs >=0.81 deg between the wall and contact
normals (was 0.011 deg - which routed near-parallel pairs through the
numerically unstable projection); the degenerate guard now hard-stops
slides under ~1.41 cm like retail (was 0.2 mm). Branch POLARITY was
already correct - no change there.

No regression: full physics suite (612) + full Core (1443) green. Not a
register deviation (no row existed; this is an undocumented porting
error corrected to match retail).

This does NOT close #116 - it fixes a tangential constant, not either
reported shape. Ghidra also settled the two shapes' diagnosis (recorded
in ISSUES.md #116 + physics digest):
- Shape-1: our cn=UnitZ default IS retail-faithful (validate_transition
  0x0050aa70 has the identical `if (collision_normal_valid==0)
  set_collision_normal(UnitZ)`). The real divergence is upstream -
  tick-22760 our collision_normal_valid was false where retail's was
  true (it recorded the door-face normal). Needs the instrumented
  tick-22760 replay.
- Shape-2 (D4 stays skipped, note sharpened): slide_sphere slides
  in-frame (SLID_TS) so Z=1.92 is faithful and the D4 Z=2.0 hard-stop
  pin is the suspect half; the threshold fix didn't move D4 (real slide,
  not degenerate). Needs a cdb trace of an airborne wall hit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-13 10:21:51 +02:00
parent 35961f2039
commit bf18a54369
3 changed files with 60 additions and 11 deletions

View file

@ -546,14 +546,17 @@ public class BSPStepUpTests
/// every frame replays the same hard stop and the character hangs in falling
/// animation until another correction breaks the loop.
/// </summary>
[Fact(Skip = "Issue #116 — slide-response divergence family (P1-era " +
"slide_sphere work made the first airborne wall frame slide in-frame " +
"to Z=1.92 instead of the L.2c-pinned hard stop at Z=2.0; the cached " +
"sliding-normal mechanism retail seeds via get_object_info " +
"(pc:279992, transient bit 4 → init_sliding_normal) only governs the " +
"NEXT frame, so which first-frame response is retail-faithful needs " +
"its own oracle read. NOT a cell-set problem — BR-7/A6.P4 left this " +
"byte-identical. See docs/ISSUES.md #116.")]
[Fact(Skip = "Issue #116 shape-2 — the engine slides IN-FRAME to Z=1.92 " +
"on the first airborne wall frame; this pin expects an L.2c hard stop " +
"at Z=2.0. Ghidra (2026-06-12) confirms retail CSphere::slide_sphere " +
"(0x00537440) applies the slide IN-FRAME (add_offset_to_check_pos → " +
"SLID_TS), so our 1.92 is faithful TO slide_sphere and the Z=2.0 " +
"expectation is the SUSPECT half — but whether retail's first " +
"airborne frame REACHES slide_sphere (→1.92) or hard-stops upstream " +
"(collide_with_environment dispatch / no last-known plane) needs a " +
"cdb trace of an airborne wall hit before flipping the assertion. The " +
"#116 threshold fix (EpsilonSq→F_EPSILON) did NOT change this — the D4 " +
"offset is a real slide, not degenerate. See docs/ISSUES.md #116.")]
public void D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames()
{
var (root, resolved) = BSPStepUpFixtures.TallWall();