diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 2bc4041..fe3e2ef 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -1651,104 +1651,29 @@ public sealed class Transition return otherCellsState; // ────────────────────────────────────────────────────────── - // ── Synthesize indoor walkable contact plane ────────────── - // Indoor walking Phase 2 follow-up (2026-05-19). When the BSP - // returns OK (no wall collision), the player is standing on a - // floor poly inside the cell. We must NOT fall through to - // outdoor terrain (SampleTerrainWalkable) — the outdoor terrain - // Z is below the indoor floor due to the +0.02f Z-bump applied - // for render z-fight prevention. ValidateWalkable would then see - // the player 0.5m above the outdoor plane → marks them as - // airborne → walkable=False → falling animation, never recovers. + // ── Indoor walkable handling — A6.P3 slice 1 (2026-05-22) ─ + // Retail's CEnvCell::find_env_collisions (decomp + // acclient_2013_pseudo_c.txt:309573) returns OK after + // BSPTREE::find_collisions returns OK — NO call to + // set_contact_plane or any synthesis. ContactPlane is + // either: + // - Already valid from a previous frame's Path-6 land + // write inside BSPQuery.FindCollisions (Mechanism A). + // - 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 - // branch with the cell's walkable plane set — no fall-through - // to terrain. - bool walkableHit = TryFindIndoorWalkablePlane( - cellPhysics, localCenter, sphereRadius, - out var indoorPlane, - out var indoorVertices, - out uint hitPolyId); - - if (PhysicsDiagnostics.ProbeIndoorBspEnabled) - { - 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. + // The old TryFindIndoorWalkablePlane synthesis path is + // removed here; the function definition is retained for + // now and is deleted in A6.P4 along with the #90 + // workaround. + // + // If subsequent visual verification shows first-frame + // fall-through (LKCP invalid AND no Path-6 land happens + // for a flat-walk-only scenario), A6.P3 slice 2 adds + // Mechanism C (retail's frames_stationary_fall flat-CP + // synthesis at acclient_2013_pseudo_c.txt:272622+). + return TransitionState.OK; } } diff --git a/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs b/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs index a8e24a0..cc40c59 100644 --- a/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs @@ -230,36 +230,36 @@ public class IndoorContactPlaneRetentionTests public void IndoorFlatFloorWalking_60Frames_ProducesAtMost5ExtraCpWrites() { // ── Arrange ─────────────────────────────────────────────────────────── - // SpherePath.InitPath sets LocalSphere[0].Origin = (0,0,sphereRadius). - // After SetCheckPos(worldPos), the global sphere CENTER is at - // worldPos + (0,0,sphereRadius) = worldPos + (0,0,0.48). + // Post-fix grounded model: // - // For TryFindIndoorWalkablePlane's probe to reach the floor: - // sphereCenter.Z - PROBE_DIST (0.5m) < floorZ - // (worldPos.Z + 0.48) - 0.5 < floorZ - // worldPos.Z < floorZ + 0.02 + // SpherePath.InitPath sets LocalSphere[0].Origin = (0,0,sphereRadius). + // After SetCheckPos(worldPos), the global sphere CENTER is at + // worldPos + (0,0,SphereRadius) = (0,0,SphereRadius). // - // And for ValidateWalkable to call SetContactPlane (below-surface): - // sphereBottom = sphereCenter.Z - radius = worldPos.Z < floorZ - // i.e. worldPos.Z < floorZ + // A correctly-grounded sphere has its bottom exactly at the floor: + // sphereBottom = sphereCenter.Z - SphereRadius + // = worldPosZ + SphereRadius - SphereRadius + // = worldPosZ // - // Choose worldPos.Z = floorZ - 0.05 so sphere bottom is 0.05m below the - // floor. Sphere center is at floorZ + 0.43. Probe reaches floorZ + 0.43 - - // 0.5 = floorZ - 0.07 which is below the floor, so AdjustSphereToPlane - // returns true (dpPos = 0.43, dist = 0.43 - 0.48 = -0.05, iDist = 0.1). + // With worldPosZ = 0 (= floorZ), the bottom just touches the floor. + // SphereIntersectsPolyInternal uses a strict penetration check, so a + // sphere touching-but-not-penetrating does NOT count as a hit. + // Path 5 (Contact grounded) returns OK with no CP write. // - // ValidateWalkable: lowPoint.Z = floorZ + 0.43 - 0.48 = floorZ - 0.05 - // dist = -0.05 < -EPSILON → SetContactPlane fires every frame. - const float floorZ = 0f; - const float worldPosZ = floorZ - 0.05f; // character "foot" position (begin param) - // Sphere center in world space = worldPosZ + SphereRadius = -0.05 + 0.48 = 0.43 - const float sphereCenterZ = worldPosZ + SphereRadius; // = 0.43 + // Pre-fix: the sphere was positioned 5 cm BELOW the floor so that + // TryFindIndoorWalkablePlane → ValidateWalkable would fire every frame. + // Post-fix: synthesis is gone; the sphere must be at its natural + // grounded position (bottom at floorZ) so that BSP Path 5 finds no + // penetration and returns OK immediately — zero additional CP writes. + 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 worldPos = new Vector3(0f, 0f, worldPosZ); - // BSP bounding sphere centered at the actual sphere center (not worldPos), - // so NodeIntersects passes in FindWalkableInternal. + // BSP bounding sphere centered at the sphere center so NodeIntersects + // passes during the BSP traversal. var cell = BuildCellWithFloor(floorZ, bspCenterZ: sphereCenterZ); var engine = BuildEngine(IndoorCellId, cell); var t = BuildGroundedTransition( @@ -274,11 +274,11 @@ public class IndoorContactPlaneRetentionTests // ── Act — 60 frames of flat-floor walking ───────────────────────────── // Each iteration: nudge CheckPos a tiny bit forward in X (stays on the - // same floor), then call FindEnvCollisions as the indoor branch would - // call it each physics frame. + // same floor at the grounded Z), then call FindEnvCollisions as the + // indoor branch would call it each physics 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); t.SpherePath.SetCheckPos(newPos, IndoorCellId);