diff --git a/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs b/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs
new file mode 100644
index 0000000..fadfc7a
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs
@@ -0,0 +1,294 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using DatReaderWriter.Enums;
+using DatReaderWriter.Types;
+using AcDream.Core.Physics;
+using Xunit;
+
+namespace AcDream.Core.Tests.Physics;
+
+///
+/// Regression test for A6.P3 slice 1 — Finding 2 (ContactPlane resynthesis blowup).
+///
+///
+/// Pre-fix behavior: enters the
+/// indoor BSP branch every frame, calls
+/// on every OK-result frame, which calls
+/// which calls 1–2 times per frame.
+/// Over 60 frames of flat-floor walking: ~60–120 additional ContactPlane writes.
+///
+///
+///
+/// Post-fix behavior: the contact plane is carried forward from LKCP (Last Known
+/// ContactPlane) when already grounded on the same surface; synthesis fires at most
+/// once on entry. Over 60 flat-floor frames: ≤5 additional CP writes.
+///
+///
+///
+/// Template: fixture pattern matches
+/// (engine + DataCache + RegisterCellStructForTest + AddLandblock + MakeGroundedTransition).
+/// Tested cell cell ID 0xA9B40166 (low 16 bits = 0x0166 ≥ 0x0100 → indoor branch fires).
+///
+///
+///
+/// Retail oracle: A6.P2 findings doc
+/// (docs/research/2026-05-21-a6-cdb-capture-findings.md), Finding 2.
+///
+///
+public class IndoorContactPlaneRetentionTests
+{
+ // ── Cell ID ───────────────────────────────────────────────────────────────
+ // Low 16 bits = 0x0166 ≥ 0x0100 → indoor branch of FindEnvCollisions fires.
+ // This is a real Holtburg inn cell from A6.P1 scen3 captures.
+ private const uint IndoorCellId = 0xA9B40166u;
+
+ // ── Sphere radius matching BSPStepUpFixtures convention ──────────────────
+ private const float SphereRadius = 0.48f;
+
+ // ── Maximum allowed additional CP writes in 60 frames (post-fix budget) ──
+ private const int MaxAdditionalCpWrites = 5;
+
+ // ── Number of simulated frames ────────────────────────────────────────────
+ private const int SimulatedFrames = 60;
+
+ // =========================================================================
+ // Helpers
+ // =========================================================================
+
+ ///
+ /// Build a BSP leaf node that contains the given polygon ids.
+ ///
+ /// The bounding sphere must cover the test sphere position so that
+ /// NodeIntersects passes and the BSP traversal reaches the leaf.
+ /// We parameterise the center so the caller can align it with the test geometry.
+ ///
+ private static PhysicsBSPTree BuildLeafBsp(
+ IEnumerable polyIds,
+ Vector3 bspCenter,
+ float bspRadius = 10f)
+ {
+ var node = new PhysicsBSPNode
+ {
+ Type = BSPNodeType.Leaf,
+ BoundingSphere = new Sphere { Origin = bspCenter, Radius = bspRadius },
+ };
+ foreach (var id in polyIds)
+ node.Polygons.Add(id);
+ return new PhysicsBSPTree { Root = node };
+ }
+
+ ///
+ /// Build a CellPhysics with a single large upward-facing floor polygon
+ /// (a 20×20 square in the XY plane at local/world Z = ),
+ /// identity transforms, and a BSP leaf whose bounding sphere is centered at
+ /// so that NodeIntersects always passes.
+ ///
+ /// The floor is large enough (±10 in both X and Y) that any test position
+ /// within 10m of the origin in XY is always over the floor — no XY misses.
+ ///
+ /// Using identity transforms means local space == world space, which keeps
+ /// the geometry traceable without transform arithmetic.
+ ///
+ /// IMPORTANT — sphere center computation:
+ /// places LocalSphere[0].Origin = (0,0,sphereRadius).
+ /// After SetCheckPos(worldPos, cellId), the global sphere center is
+ /// worldPos + (0,0,sphereRadius). The BSP bounding sphere must be
+ /// centered near this sphere center (NOT at worldPos) so that NodeIntersects
+ /// passes during TryFindIndoorWalkablePlane → FindWalkableSphere → FindWalkableInternal.
+ ///
+ private static CellPhysics BuildCellWithFloor(float floorZ, float bspCenterZ)
+ {
+ // 20×20 upward-facing floor at local Z = floorZ.
+ var verts = new[]
+ {
+ new Vector3(-10f, -10f, floorZ),
+ new Vector3( 10f, -10f, floorZ),
+ new Vector3( 10f, 10f, floorZ),
+ new Vector3(-10f, 10f, floorZ),
+ };
+ var normal = Vector3.UnitZ; // straight up
+ float d = -floorZ; // N·p + D = 0 → D = -floorZ
+
+ var floorPoly = new ResolvedPolygon
+ {
+ Vertices = verts,
+ Plane = new Plane(normal, d),
+ NumPoints = 4,
+ SidesType = CullMode.None,
+ };
+
+ // BSP leaf bounding sphere centered at the actual sphere center
+ // (worldPos + (0,0,sphereRadius)). Radius 10 covers the 20×20 floor
+ // polygon and the test sphere.
+ var bspCenter = new Vector3(0f, 0f, bspCenterZ);
+
+ return new CellPhysics
+ {
+ BSP = BuildLeafBsp(new ushort[] { 0 }, bspCenter, bspRadius: 10f),
+ WorldTransform = Matrix4x4.Identity,
+ InverseWorldTransform = Matrix4x4.Identity,
+ Resolved = new Dictionary { [0] = floorPoly },
+ // CellBSP intentionally left null: ResolveCellId sees
+ // indoorCell.CellBSP?.Root == null → returns the indoor id unchanged
+ // (the "can't verify; trust FindCellList" branch), so the cell ID
+ // stays indoor throughout every loop iteration.
+ CellBSP = null,
+ };
+ }
+
+ ///
+ /// Build a minimal PhysicsEngine with a flat landblock registered so that
+ /// FindEnvCollisions's AddLandblock / SampleTerrainWalkable / ResolveCellId
+ /// paths all have a coherent landblock context if they ever reach outdoors.
+ ///
+ private static PhysicsEngine BuildEngine(uint indoorCellId, CellPhysics cell)
+ {
+ var engine = new PhysicsEngine();
+ engine.DataCache = new PhysicsDataCache();
+
+ // Flat terrain height table: all zeroes.
+ var heights = new byte[81];
+ Array.Fill(heights, (byte)0);
+ var ht = new float[256];
+ for (int i = 0; i < 256; i++) ht[i] = (float)i;
+
+ // Use the high 16 bits of IndoorCellId as the landblock prefix.
+ // The landblock key is constructed with 0xFFFF suffix (the "all cells"
+ // landblock sentinel used throughout the test suite).
+ uint landblockKey = (indoorCellId & 0xFFFF0000u) | 0xFFFFu;
+ engine.AddLandblock(landblockKey,
+ new TerrainSurface(heights, ht),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 0f, worldOffsetY: 0f);
+
+ engine.DataCache.RegisterCellStructForTest(indoorCellId, cell);
+ return engine;
+ }
+
+ ///
+ /// Build a grounded Transition positioned on the floor at
+ /// and headed to .
+ ///
+ /// Sets Contact + OnWalkable (grounded mover), seeds LastKnownContactPlane
+ /// with the floor plane, and calls
+ /// once so the CP is considered "already established."
+ ///
+ /// The initial SetContactPlane call is factored into
+ /// (which the test snapshots before the loop).
+ ///
+ private static Transition BuildGroundedTransition(
+ Vector3 worldPos,
+ Vector3 worldTarget,
+ uint cellId,
+ Plane floorPlane)
+ {
+ var t = new Transition();
+ t.SpherePath.InitPath(worldPos, worldTarget, cellId, SphereRadius);
+ t.SpherePath.SetCheckPos(worldPos, cellId);
+
+ // Grounded state — mirrors BSPStepUpFixtures.MakeGroundedTransition.
+ t.ObjectInfo.State = ObjectInfoState.Contact | ObjectInfoState.OnWalkable;
+ t.ObjectInfo.StepUpHeight = 0.04f;
+ t.ObjectInfo.StepDownHeight = 0.04f;
+ t.ObjectInfo.StepDown = true;
+
+ // Seed LastKnownContactPlane — the resolver's "we were on this floor last frame" memory.
+ t.CollisionInfo.LastKnownContactPlane = floorPlane;
+ t.CollisionInfo.LastKnownContactPlaneValid = true;
+
+ // Seed ContactPlane so ValidateWalkable sees us as already grounded.
+ // This call increments ContactPlaneWriteCount to 1 (the "seeded" count).
+ t.CollisionInfo.SetContactPlane(floorPlane, cellId, isWater: false);
+
+ return t;
+ }
+
+ // =========================================================================
+ // Tests
+ // =========================================================================
+
+ ///
+ /// Sixty frames of indoor flat-floor walking must not produce more than
+ /// additional ContactPlane writes
+ /// beyond the initial seeding write.
+ ///
+ ///
+ /// Pre-fix (broken): TryFindIndoorWalkablePlane fires + ValidateWalkable
+ /// calls SetContactPlane 1–2×/frame → ~60–120 additional writes → FAIL.
+ /// Post-fix: LKCP is restored early; synthesis skipped for already-grounded
+ /// movers → ≤5 additional writes → PASS.
+ ///
+ ///
+ [Fact]
+ 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).
+ //
+ // 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
+ //
+ // And for ValidateWalkable to call SetContactPlane (below-surface):
+ // sphereBottom = sphereCenter.Z - radius = worldPos.Z < floorZ
+ // i.e. worldPos.Z < floorZ
+ //
+ // 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).
+ //
+ // 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
+
+ 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.
+ var cell = BuildCellWithFloor(floorZ, bspCenterZ: sphereCenterZ);
+ var engine = BuildEngine(IndoorCellId, cell);
+ var t = BuildGroundedTransition(
+ worldPos,
+ worldPos + new Vector3(0.001f, 0f, 0f), // tiny horizontal target
+ IndoorCellId,
+ floorPlane);
+
+ // Snapshot the write count right after seeding (should be exactly 1).
+ int seededWrites = t.CollisionInfo.ContactPlaneWriteCount;
+ Assert.Equal(1, seededWrites);
+
+ // ── 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.
+ for (int frame = 0; frame < SimulatedFrames; frame++)
+ {
+ // Advance position by 1 mm forward — same Z, still over the floor.
+ var newPos = new Vector3(frame * 0.001f, 0f, worldPosZ);
+ t.SpherePath.SetCheckPos(newPos, IndoorCellId);
+
+ // Simulate FindEnvCollisions as the physics loop calls it.
+ t.FindEnvCollisions(engine);
+ }
+
+ // ── Assert ────────────────────────────────────────────────────────────
+ int totalWrites = t.CollisionInfo.ContactPlaneWriteCount;
+ int additionalWrites = totalWrites - seededWrites;
+
+ Assert.True(
+ additionalWrites <= MaxAdditionalCpWrites,
+ $"Expected ≤{MaxAdditionalCpWrites} additional CP writes across " +
+ $"{SimulatedFrames} flat-floor frames, got {additionalWrites}. " +
+ "Finding 2 fix not complete.");
+ }
+}