fix #172: port the retail CCylSphere collision family (platform step-up)
The Holtburg town-network portal platform (stab 0xC0A9B465, Setup 0x020019E3, CylSphere r=2.597m h=0.256m) blocked the player with an endless rim slide instead of retail's step-up-onto-top — gating the whole #137 dungeon repro. Surfaced when #149 started registering BSP-less stab CylSpheres: the collision SHAPE became right while the RESPONSE was still the hand-rolled AP-6 approximation (step-up gate + radial wall-slide only). Root cause: no cylinder-TOP support anywhere. DoStepUp's internal step-down probe needs retail's step_sphere_down (0x0053a9b0) to land on the flat top — a cylinder has no polygons for the walkable search — so every step-up onto a wide cylinder failed into StepUpSlide and the player orbited the rim (probe-confirmed: [cyl-test] result=Slid with horizontal rim normals, launch-137-repro.log). Port the full family verbatim: dispatcher intersects_sphere 0x0053b440 (placement/ethereal detection, step-down cap landing, walkable probe, grounded step_sphere_up 0x0053b310, PathClipped collide_with_point 0x0053acb0, airborne land_on_cylinder 0x0053b3d0, Collide-flag exact-TOI cap rest) + collides_with_sphere 0x0053a880 + normal_of_collision 0x0053ab50 + slide_sphere 0x0053b2a0. Pseudocode + settled BN x87 ambiguities (via ACE cross-ref) + two ACE bugs found and NOT copied (head-slide foot-disp; see doc §8): docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md Ethereal cylinders now flow through retail's Layer-2 override (pc:276961) instead of the early-OK consume — same net #150 behavior, plus retail placement-blocked-by-cylinder semantics. SlideSphere gains a sphereNum param (retail slides the head sphere by its own displacement, 0x0053b843). Register: AP-6 retired; AP-83 added (PerfectClip TOI tail decoded per ACE, dead code until missiles). Tests: CylSphereFamilyTests (grounded step-up onto the exact platform shape, tall-cylinder block, airborne top landing, ethereal guard); the #42 self-shadow control assertion updated to the retail observable (denied movement — the old ~1m radial self-push was the approximation's artifact, not retail). Suites: Core 2533 / App 713 / UI 425 / Net 385 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
67c3357246
commit
6ab269894a
6 changed files with 1027 additions and 151 deletions
|
|
@ -487,9 +487,20 @@ public class PhysicsEngineTests
|
|||
collisionType: ShadowCollisionType.Cylinder,
|
||||
cylHeight: 1.835f);
|
||||
|
||||
// Without the gate (movingEntityId == 0): the sweep must self-push.
|
||||
// This proves the registry actually causes a collision, so the
|
||||
// following filtered case is not a vacuous pass.
|
||||
// Without the gate (movingEntityId == 0): the sweep must be
|
||||
// INTERFERED WITH by the self-entry. This proves the registry
|
||||
// actually causes a collision, so the following filtered case is not
|
||||
// a vacuous pass.
|
||||
//
|
||||
// Observable updated for the 2026-07-05 CCylSphere family port: the
|
||||
// old hand-rolled response radial-pushed the sphere ~1 m sideways
|
||||
// (the original #42 symptom this test asserted). Retail's dispatcher
|
||||
// (0x0053b440) resolves this geometry — airborne, dead-center on the
|
||||
// cylinder axis, moving up — through land_on_cylinder → the Collide
|
||||
// re-test, whose interp gate hard-stops (COLLIDED); ValidateTransition
|
||||
// then reverts to a stay-put (no sideways teleport, Ok=true). The
|
||||
// response-model-independent interference signal is the DENIED +Z
|
||||
// movement: the sweep must NOT reach the +0.022 target.
|
||||
var unfiltered = engine.ResolveWithTransition(
|
||||
currentPos: bodyPos, targetPos: targetPos,
|
||||
cellId: 0xA9B40039u,
|
||||
|
|
@ -498,11 +509,11 @@ public class PhysicsEngineTests
|
|||
isOnGround: false,
|
||||
movingEntityId: 0u);
|
||||
|
||||
float unfilteredXY = MathF.Sqrt(
|
||||
(unfiltered.Position.X - targetPos.X) * (unfiltered.Position.X - targetPos.X) +
|
||||
(unfiltered.Position.Y - targetPos.Y) * (unfiltered.Position.Y - targetPos.Y));
|
||||
Assert.True(unfilteredXY > 0.5f,
|
||||
$"Without movingEntityId, sweep should self-push (got XY drift {unfilteredXY:F3}m)");
|
||||
Assert.True(unfiltered.Position.Z < targetPos.Z - 0.01f,
|
||||
$"Without movingEntityId, the sweep must collide with the mover's own " +
|
||||
$"ShadowEntry and deny the +Z movement (retail: land_on_cylinder → " +
|
||||
$"Collide re-test → COLLIDED → stay-put). Got Z={unfiltered.Position.Z:F4}, " +
|
||||
$"target Z={targetPos.Z:F4}");
|
||||
|
||||
// With the gate: the sweep must leave XY unchanged.
|
||||
var filtered = engine.ResolveWithTransition(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue