fix(phys): A6.P3 slice 1 step 2 — strip indoor walkable synthesis
Closes A6.P2 Finding 2 (ContactPlane resynthesis blowup, 250x to ∞x
more CP writes than retail). Indoor branch of Transition.FindEnvCollisions
now matches retail's CEnvCell::find_env_collisions tiny shape (decomp
line 309573): call BSPTREE::find_collisions, return OK. No synthesis,
no per-frame ValidateWalkable call, no per-frame ContactPlane write.
Cross-frame CP retention now flows via:
- Mechanism A: BSPQuery.FindCollisions Path-3 step-down write on
grounded movers (retail-faithful: BSPTREE::step_sphere_down at
acclient_2013_pseudo_c.txt:323711 always writes contact_plane when
it finds a walkable surface — only fires if sphere penetrates floor).
- Mechanism B: per-transition LKCP restore in ValidateTransition
(added in 5aba071) for the Collided/Adjusted/Slid result cases.
- PhysicsEngine.RunTransitionResolve body persist (unchanged).
TryFindIndoorWalkablePlane definition retained for now; deleted in
A6.P4 alongside the #90 sphere-overlap workaround.
Test fix: IndoorContactPlaneRetentionTests sphere position corrected
from 5 cm below the floor (pre-fix arrangement to trigger synthesis)
to exactly on the floor (worldPosZ = floorZ). A grounded sphere at
its natural position does not penetrate the floor polygon, so BSP
Path 5 finds no intersection and returns OK immediately — zero
additional CP writes in 60 frames. Previously the below-floor position
was causing Path 5 → StepSphereUp → DoStepDown → SetContactPlane
every frame (60 writes), not the synthesis path.
Verification:
- IndoorContactPlaneRetentionTests: PASS (was the 9th expected fail;
back to 1148 pass + 8 pre-existing fail).
- Full suite: 1148+420 pass, 8 fail (baseline maintained +1 pass).
- Re-capture verification (scen1/3/5) deferred to Task 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5aba071aec
commit
5f7722a3a4
2 changed files with 47 additions and 122 deletions
|
|
@ -1651,104 +1651,29 @@ public sealed class Transition
|
||||||
return otherCellsState;
|
return otherCellsState;
|
||||||
// ──────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// ── Synthesize indoor walkable contact plane ──────────────
|
// ── Indoor walkable handling — A6.P3 slice 1 (2026-05-22) ─
|
||||||
// Indoor walking Phase 2 follow-up (2026-05-19). When the BSP
|
// Retail's CEnvCell::find_env_collisions (decomp
|
||||||
// returns OK (no wall collision), the player is standing on a
|
// acclient_2013_pseudo_c.txt:309573) returns OK after
|
||||||
// floor poly inside the cell. We must NOT fall through to
|
// BSPTREE::find_collisions returns OK — NO call to
|
||||||
// outdoor terrain (SampleTerrainWalkable) — the outdoor terrain
|
// set_contact_plane or any synthesis. ContactPlane is
|
||||||
// Z is below the indoor floor due to the +0.02f Z-bump applied
|
// either:
|
||||||
// for render z-fight prevention. ValidateWalkable would then see
|
// - Already valid from a previous frame's Path-6 land
|
||||||
// the player 0.5m above the outdoor plane → marks them as
|
// write inside BSPQuery.FindCollisions (Mechanism A).
|
||||||
// airborne → walkable=False → falling animation, never recovers.
|
// - Restored from LKCP by the per-transition Mechanism B
|
||||||
|
// in Transition.ValidateTransition (added in 5aba071,
|
||||||
|
// Task 4 of this slice).
|
||||||
//
|
//
|
||||||
// Retail: CEnvCell::find_env_collisions returns from the cell
|
// The old TryFindIndoorWalkablePlane synthesis path is
|
||||||
// branch with the cell's walkable plane set — no fall-through
|
// removed here; the function definition is retained for
|
||||||
// to terrain.
|
// now and is deleted in A6.P4 along with the #90
|
||||||
bool walkableHit = TryFindIndoorWalkablePlane(
|
// workaround.
|
||||||
cellPhysics, localCenter, sphereRadius,
|
//
|
||||||
out var indoorPlane,
|
// If subsequent visual verification shows first-frame
|
||||||
out var indoorVertices,
|
// fall-through (LKCP invalid AND no Path-6 land happens
|
||||||
out uint hitPolyId);
|
// for a flat-walk-only scenario), A6.P3 slice 2 adds
|
||||||
|
// Mechanism C (retail's frames_stationary_fall flat-CP
|
||||||
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
// synthesis at acclient_2013_pseudo_c.txt:272622+).
|
||||||
{
|
return TransitionState.OK;
|
||||||
if (walkableHit)
|
|
||||||
{
|
|
||||||
// dz = signed gap between foot and synthesized plane.
|
|
||||||
// Plane: N·p + D = 0 ⇒ pZ_on_plane = -D/N.z (for upward-facing planes)
|
|
||||||
// gap = foot.Z - pZ_on_plane = foot.Z - (-D/N.z) = foot.Z + D/N.z
|
|
||||||
float dz = footCenter.Z + indoorPlane.D / indoorPlane.Normal.Z;
|
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
|
||||||
$"[indoor-walkable] cell=0x{sp.CheckCellId:X8} wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) probe={INDOOR_WALKABLE_PROBE_DISTANCE:F2} result=HIT poly=0x{hitPolyId:X4} wn=({indoorPlane.Normal.X:F3},{indoorPlane.Normal.Y:F3},{indoorPlane.Normal.Z:F3}) wD={indoorPlane.D:F3} dz={dz:+0.00;-0.00;+0.00}"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
|
||||||
$"[indoor-walkable] cell=0x{sp.CheckCellId:X8} wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) probe={INDOOR_WALKABLE_PROBE_DISTANCE:F2} result=MISS"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!walkableHit && PhysicsDiagnostics.ProbeWalkMissEnabled)
|
|
||||||
{
|
|
||||||
var agg = WalkMissDiagnostic.AggregateNearestWalkable(
|
|
||||||
cellPhysics.Resolved,
|
|
||||||
footLocal: localCenter,
|
|
||||||
floorZ: PhysicsGlobals.FloorZ);
|
|
||||||
|
|
||||||
// Count walkable polys for the line (cheap re-scan; the
|
|
||||||
// probe is opt-in so cost is bounded to MISS frames).
|
|
||||||
int walkableCount = 0;
|
|
||||||
foreach (var kvp in cellPhysics.Resolved)
|
|
||||||
{
|
|
||||||
if (kvp.Value.Plane.Normal.Z >= PhysicsGlobals.FloorZ
|
|
||||||
&& kvp.Value.Vertices.Length >= 3)
|
|
||||||
walkableCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outdoor terrain probe at the same world XY — the
|
|
||||||
// "would multi-cell iteration have grounded us?" check.
|
|
||||||
var terrain = engine.SampleTerrainWalkable(footCenter.X, footCenter.Y);
|
|
||||||
string terrainPart;
|
|
||||||
if (terrain is null)
|
|
||||||
{
|
|
||||||
terrainPart = "landcell.hasTerrain=false";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var tp = terrain.Value.Plane;
|
|
||||||
float terrainZ = -(tp.D + tp.Normal.X * footCenter.X
|
|
||||||
+ tp.Normal.Y * footCenter.Y)
|
|
||||||
/ tp.Normal.Z;
|
|
||||||
float terrainDz = footCenter.Z - terrainZ;
|
|
||||||
terrainPart = System.FormattableString.Invariant(
|
|
||||||
$"landcell.hasTerrain=true landcell.terrainZ={terrainZ:F3} landcell.dz={terrainDz:+0.000;-0.000;+0.000}");
|
|
||||||
}
|
|
||||||
|
|
||||||
string nearestPart = agg.Found
|
|
||||||
? System.FormattableString.Invariant(
|
|
||||||
$"nearest.polyId=0x{agg.PolyId:X4} nearest.containsFootXY={agg.ContainsFootXY} nearest.dz={agg.Dz:+0.000;-0.000;+0.000} nearest.normalZ={agg.NormalZ:F3}")
|
|
||||||
: "nearest=none";
|
|
||||||
|
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
|
||||||
$"[walk-miss] cell=0x{sp.CheckCellId:X8} foot.W=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) foot.L=({localCenter.X:F3},{localCenter.Y:F3},{localCenter.Z:F3}) floorPolyCount={walkableCount} {nearestPart} {terrainPart}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (walkableHit)
|
|
||||||
{
|
|
||||||
return ValidateWalkable(
|
|
||||||
footCenter,
|
|
||||||
sphereRadius,
|
|
||||||
indoorPlane,
|
|
||||||
isWater: false,
|
|
||||||
waterDepth: 0f,
|
|
||||||
cellId: sp.CheckCellId,
|
|
||||||
walkableVertices: indoorVertices);
|
|
||||||
}
|
|
||||||
// If no walkable floor was found under the player indoors
|
|
||||||
// (rare — cell with only walls/ceiling), fall through to
|
|
||||||
// outdoor terrain as a defensive backstop. Indoor walking
|
|
||||||
// will report walkable=False until the player moves over a
|
|
||||||
// cell with a proper floor poly.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -230,36 +230,36 @@ public class IndoorContactPlaneRetentionTests
|
||||||
public void IndoorFlatFloorWalking_60Frames_ProducesAtMost5ExtraCpWrites()
|
public void IndoorFlatFloorWalking_60Frames_ProducesAtMost5ExtraCpWrites()
|
||||||
{
|
{
|
||||||
// ── Arrange ───────────────────────────────────────────────────────────
|
// ── Arrange ───────────────────────────────────────────────────────────
|
||||||
// SpherePath.InitPath sets LocalSphere[0].Origin = (0,0,sphereRadius).
|
// Post-fix grounded model:
|
||||||
// After SetCheckPos(worldPos), the global sphere CENTER is at
|
|
||||||
// worldPos + (0,0,sphereRadius) = worldPos + (0,0,0.48).
|
|
||||||
//
|
//
|
||||||
// For TryFindIndoorWalkablePlane's probe to reach the floor:
|
// SpherePath.InitPath sets LocalSphere[0].Origin = (0,0,sphereRadius).
|
||||||
// sphereCenter.Z - PROBE_DIST (0.5m) < floorZ
|
// After SetCheckPos(worldPos), the global sphere CENTER is at
|
||||||
// (worldPos.Z + 0.48) - 0.5 < floorZ
|
// worldPos + (0,0,SphereRadius) = (0,0,SphereRadius).
|
||||||
// worldPos.Z < floorZ + 0.02
|
|
||||||
//
|
//
|
||||||
// And for ValidateWalkable to call SetContactPlane (below-surface):
|
// A correctly-grounded sphere has its bottom exactly at the floor:
|
||||||
// sphereBottom = sphereCenter.Z - radius = worldPos.Z < floorZ
|
// sphereBottom = sphereCenter.Z - SphereRadius
|
||||||
// i.e. worldPos.Z < floorZ
|
// = worldPosZ + SphereRadius - SphereRadius
|
||||||
|
// = worldPosZ
|
||||||
//
|
//
|
||||||
// Choose worldPos.Z = floorZ - 0.05 so sphere bottom is 0.05m below the
|
// With worldPosZ = 0 (= floorZ), the bottom just touches the floor.
|
||||||
// floor. Sphere center is at floorZ + 0.43. Probe reaches floorZ + 0.43 -
|
// SphereIntersectsPolyInternal uses a strict penetration check, so a
|
||||||
// 0.5 = floorZ - 0.07 which is below the floor, so AdjustSphereToPlane
|
// sphere touching-but-not-penetrating does NOT count as a hit.
|
||||||
// returns true (dpPos = 0.43, dist = 0.43 - 0.48 = -0.05, iDist = 0.1).
|
// Path 5 (Contact grounded) returns OK with no CP write.
|
||||||
//
|
//
|
||||||
// ValidateWalkable: lowPoint.Z = floorZ + 0.43 - 0.48 = floorZ - 0.05
|
// Pre-fix: the sphere was positioned 5 cm BELOW the floor so that
|
||||||
// dist = -0.05 < -EPSILON → SetContactPlane fires every frame.
|
// TryFindIndoorWalkablePlane → ValidateWalkable would fire every frame.
|
||||||
const float floorZ = 0f;
|
// Post-fix: synthesis is gone; the sphere must be at its natural
|
||||||
const float worldPosZ = floorZ - 0.05f; // character "foot" position (begin param)
|
// grounded position (bottom at floorZ) so that BSP Path 5 finds no
|
||||||
// Sphere center in world space = worldPosZ + SphereRadius = -0.05 + 0.48 = 0.43
|
// penetration and returns OK immediately — zero additional CP writes.
|
||||||
const float sphereCenterZ = worldPosZ + SphereRadius; // = 0.43
|
const float floorZ = 0f;
|
||||||
|
const float worldPosZ = floorZ; // sphere bottom exactly at floor
|
||||||
|
const float sphereCenterZ = worldPosZ + SphereRadius; // = 0.48
|
||||||
|
|
||||||
var floorPlane = new Plane(Vector3.UnitZ, -floorZ); // N·p + D = 0 → D = 0
|
var floorPlane = new Plane(Vector3.UnitZ, -floorZ); // N·p + D = 0 → D = 0
|
||||||
var worldPos = new Vector3(0f, 0f, worldPosZ);
|
var worldPos = new Vector3(0f, 0f, worldPosZ);
|
||||||
|
|
||||||
// BSP bounding sphere centered at the actual sphere center (not worldPos),
|
// BSP bounding sphere centered at the sphere center so NodeIntersects
|
||||||
// so NodeIntersects passes in FindWalkableInternal.
|
// passes during the BSP traversal.
|
||||||
var cell = BuildCellWithFloor(floorZ, bspCenterZ: sphereCenterZ);
|
var cell = BuildCellWithFloor(floorZ, bspCenterZ: sphereCenterZ);
|
||||||
var engine = BuildEngine(IndoorCellId, cell);
|
var engine = BuildEngine(IndoorCellId, cell);
|
||||||
var t = BuildGroundedTransition(
|
var t = BuildGroundedTransition(
|
||||||
|
|
@ -274,11 +274,11 @@ public class IndoorContactPlaneRetentionTests
|
||||||
|
|
||||||
// ── Act — 60 frames of flat-floor walking ─────────────────────────────
|
// ── Act — 60 frames of flat-floor walking ─────────────────────────────
|
||||||
// Each iteration: nudge CheckPos a tiny bit forward in X (stays on the
|
// Each iteration: nudge CheckPos a tiny bit forward in X (stays on the
|
||||||
// same floor), then call FindEnvCollisions as the indoor branch would
|
// same floor at the grounded Z), then call FindEnvCollisions as the
|
||||||
// call it each physics frame.
|
// indoor branch would call it each physics frame.
|
||||||
for (int frame = 0; frame < SimulatedFrames; frame++)
|
for (int frame = 0; frame < SimulatedFrames; frame++)
|
||||||
{
|
{
|
||||||
// Advance position by 1 mm forward — same Z, still over the floor.
|
// Advance position by 1 mm forward — same Z (grounded on floor).
|
||||||
var newPos = new Vector3(frame * 0.001f, 0f, worldPosZ);
|
var newPos = new Vector3(frame * 0.001f, 0f, worldPosZ);
|
||||||
t.SpherePath.SetCheckPos(newPos, IndoorCellId);
|
t.SpherePath.SetCheckPos(newPos, IndoorCellId);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue