using System.Collections.Generic;
using System.Numerics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using AcDream.Core.Physics;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics;
///
/// Issue #265 capture-driven bisection: "Steep-slope response set" (uphill-jump
/// bounce, lost roof slide, edge wedge). This harness replays a REAL trajectory
/// mined from a live capture (ACDREAM_CAPTURE_RESOLVE,
/// artifacts/matrix-session2-resolve.jsonl, records 3415-3434) through a
/// synthetic single-polygon built from the EXACT
/// polygon the live capture landed on
/// (bodyAfter.walkableVertices: (240,0,88),(264,0,80),(264,24,68),
/// normal (2,3,6)/7 = (0.2857,0.4286,0.8571) — a moderate, WALKABLE-BY-THRESHOLD
/// roof slope, normal.Z=0.857 > PhysicsGlobals.FloorZ (0.6642)).
///
///
/// Live symptom this reproduces (mining evidence): the player falls
/// (v ≈ (11.15, 14.13, -23.14) m/s at landing) onto this roof slope. Live capture
/// record 3433 shows collisionNormalValid=true, the correct real polygon
/// normal, and walkablePolygonValid=true — a legitimate walkable landing.
/// Record 3434 (the very next tick) shows the body FROZEN: velocity forced to
/// exactly (0,0,0), transientState=7 (Contact|OnWalkable|Sliding), and the
/// position stays byte-identical for the remaining 12,292 captured ticks (to
/// the end of the file) — i.e. the player never moves again. A second,
/// independent instance of the same shape appears at records 3153-3199+ (a
/// shallower ~18° roof edge, frozen for 46+ captured ticks). Full mining
/// evidence: docs/research/2026-07-30-265-capture-bisect.md.
///
///
///
/// Candidate mechanism (S1): commit db2889af ("#116 shape-1")
/// changed BSPQuery.cs's Path-6 hasSphere1 (head-sphere-only hit
/// while airborne, foot sphere clear) branch from a steepness-gated
/// SetCollide→Adjusted (shallow, Z≥FloorZ) / slide-tangent-then-Slid (steep,
/// Z<FloorZ) dual path — IDENTICAL in shape to the still-unchanged sphere0
/// (foot) branch a few lines above it — to an UNCONDITIONAL
/// SetCollisionNormal + return Collided, regardless of steepness. A
/// `Collided` return short-circuits TransitionalInsert immediately
/// (if (transitState == TransitionState.Collided) return
/// TransitionState.Collided;) — it never reaches the retry loop's Phase 3
/// (if (sp.Collide) ...DoCheckWalkable...Placement retry...), which is
/// the ONLY place a shallow/walkable head-sphere hit can smoothly commit to a
/// real ContactPlane + OnWalkable via the SetCollide+Adjusted
/// path. This test's real captured polygon has normal.Z=0.857 — well
/// ABOVE FloorZ (0.6642) — so it is the SHALLOW case, not the steep one;
/// S1 removed the steepness branch entirely, so this shallow graze now takes
/// the SAME hard-stop path a steep hit would.
///
///
///
/// Method: integrates the EXACT
/// captured ballistic state (position + velocity) from record 3415 forward
/// with real gravity (dt=1/30s, matching retail's tick rate), calling
/// every tick exactly like
/// does at the
/// Core boundary (this harness intentionally stops at that boundary — it does
/// NOT call PhysicsObjUpdate.HandleAllCollisions or model the R6
/// animation-root-motion grounded-movement zeroing, both of which live outside
/// Core and are confirmed NOT part of the S1/S2 candidate set — see the research
/// doc). Once the mover reports IsOnGround, the harness keeps REQUESTING
/// forward motion each tick (simulating held input) so a genuine "does the
/// engine allow continued advance across this surface" signal is observable,
/// rather than trivially replaying the live capture's own (already-frozen,
/// no-input) subsequent targets.
///
///
///
/// A/B protocol (see the research doc for the executed results): this
/// same test is run unmodified against (i) HEAD, (ii) BSPQuery.cs with
/// the S1 sphere1 branch reverted to mirror the still-current sphere0 shape
/// (local, uncommitted diagnostic edit), (iii) production unaffected by S2
/// (calc_friction's AP-7 threshold) since S2 has zero call sites outside its
/// own unit test — confirmed by grep -rn "\.calc_friction(" src/ — so no
/// S2 toggle is needed for THIS harness, and (iv) both. The per-tick dump
/// ( list, printed via )
/// is the diff target.
///
///
public class Issue265SteepSlopeCaptureBisectTests
{
private readonly ITestOutputHelper _out;
public Issue265SteepSlopeCaptureBisectTests(ITestOutputHelper output) => _out = output;
// ── Real captured polygon (bodyAfter.walkableVertices, record 3433) ──────
// artifacts/matrix-session2-resolve.jsonl, tick 3433, cell 0xAAB40011.
// normal = cross(v1-v0, v2-v0) normalized = (2,3,6)/7 exactly.
private static readonly Vector3 RoofV0 = new(240f, 0f, 88f);
private static readonly Vector3 RoofV1 = new(264f, 0f, 80f);
private static readonly Vector3 RoofV2 = new(264f, 24f, 68f);
// Outdoor cell suffix MUST be < 0x0100 (retail's indoor/outdoor LandCell
// convention — CellTransit.BuildShadowCellSet branches on it) and its
// block index must be (0,0) so that the flood's landblock-local grid math
// (CellTransit.AddAllOutsideCells, an 8x8 24-m-cell grid over the 192-m
// landblock) treats this harness's coordinates as directly landblock-local
// — CellGraph.TryGetTerrainOrigin has no registered terrain for this
// synthetic landblock so it Zero-falls-back, meaning raw "world" position
// IS landblock-local position (documented anchor-frame convention, same
// one Ts4SteepRoofWedgeCaptureTests/DoorBugTrajectoryReplayTests rely on).
// The harness's very first run registered the shadow object at the REAL
// captured world coordinates (X≈256) under this convention — 256 is
// outside the valid [0,192) landblock-local range, so the flood produced
// an empty cell set and the object was silently never registered at all
// (zero collisions the whole replay). Fix: re-anchor the entire synthetic
// scene (triangle + approach trajectory) at the roof centroid so every
// coordinate here is small and landblock-local (see the research doc's
// harness-commissioning note).
// Suffix 0x0001 is the canonical (gridX=0, gridY=0) outdoor LandCell — the
// grid cell whose local origin is (0,0) — matching this harness's
// re-anchored roof centroid at world (0,0,0) (see the note above). An
// earlier attempt used suffix 0x0011; CellTransit.AddAllOutsideCells'
// LandDefs.AdjustToOutside re-derives the (lx,ly) grid cell from the
// sphere's ACTUAL position and silently corrects a mismatched seed, so the
// registration landed in cell 0x00000001 regardless of the literal seed
// passed — GetObjectsInCell(0x00000011) found nothing (see the research
// doc's harness-commissioning note).
private const uint CellId = 0x00000001u;
private const uint LandblockId = 0x00000000u;
private const uint SyntheticGfxId = 0x265BEEF1u;
private const int TicksPerSecond = 30;
private const float Gravity = -9.8f;
private const float SphereRadius = 0.48f; // production human Setup 0x02000001
private const float SphereHeight = 1.835f; // production human Setup 0x02000001
// Real captured state, record index 3415 (session2, tick 3415) — 18 ticks
// before the landing/freeze at record 3433/3434. vx/vy are constant across
// this whole approach (pure ballistic fall, no further horizontal drive).
// Re-anchored: subtract RoofCentroid from the real captured world position
// (see the landblock-local note above) — the RELATIVE approach geometry
// (distance, direction, velocity) is preserved exactly.
private static readonly Vector3 ApproachStartPosReal = new(244.59f, -0.81f, 92.79f);
private static readonly Vector3 ApproachStartVel = new(11.1509495f, 14.129979f, -16.72f);
// ShadowObjects.Register's broad-phase culls candidates by distance from
// `worldPos` within `radius` — registering at Vector3.Zero with the real
// (far-from-origin) captured world coordinates put the polygon ~264 units
// from the query point, well outside any sane radius, so the very first
// run of this harness found ZERO collisions at all (see the research doc's
// "harness commissioning" note). Fix: register the entity at the
// triangle's centroid and express the polygon in LOCAL coordinates
// relative to that centroid (identity rotation, scale 1 — world = local +
// worldPos reconstructs the exact real-world triangle).
private static readonly Vector3 RoofCentroid = (RoofV0 + RoofV1 + RoofV2) / 3f;
private static PhysicsEngine MakeRoofEngine()
{
var resolved = new Dictionary();
var verts = new[] { RoofV0 - RoofCentroid, RoofV1 - RoofCentroid, RoofV2 - RoofCentroid };
var normal = Vector3.Normalize(Vector3.Cross(verts[1] - verts[0], verts[2] - verts[0]));
float d = -Vector3.Dot(normal, verts[0]);
resolved[1] = new ResolvedPolygon
{
Vertices = verts,
Plane = new Plane(normal, d),
NumPoints = 3,
SidesType = CullMode.None,
};
var leaf = new PhysicsBSPNode
{
Type = BSPNodeType.Leaf,
BoundingSphere = new Sphere { Origin = Vector3.Zero, Radius = 30f },
};
leaf.Polygons.Add(1);
var heights = new byte[81];
var heightTab = new float[256];
for (int i = 0; i < 256; i++) heightTab[i] = -1000f; // terrain never interferes
var engine = new PhysicsEngine();
engine.AddLandblock(
LandblockId,
new TerrainSurface(heights, heightTab),
System.Array.Empty(),
System.Array.Empty(),
worldOffsetX: 0f, worldOffsetY: 0f);
var cache = new PhysicsDataCache();
var bspTree = new PhysicsBSPTree { Root = leaf };
var physics = new GfxObjPhysics
{
BSP = bspTree,
PhysicsPolygons = new Dictionary(),
Vertices = new VertexArray(),
Resolved = resolved,
BoundingSphere = new Sphere { Origin = Vector3.Zero, Radius = 30f },
};
cache.RegisterGfxObjForTest(SyntheticGfxId, physics);
engine.DataCache = cache;
// ShadowObjectRegistry is the per-cell shadow-object index (BR-7/A6.P4):
// Register() FLOODS from a SEED CELL outward and registers the entity
// into the resulting cell set; the query side (GetObjectsInCell) looks
// up strictly by the mover's CURRENT cell id. Leaving seedCellId at its
// default (0u) makes Register() call DeriveOutdoorSeed(worldPos, ...),
// which computes its OWN outdoor landcell id from world position — for
// the real captured coordinates used here (worldPos.X=256, well outside
// landblock 0xAAB40000's own 192 m span) that derives to a DIFFERENT
// cell than the literal CellId this harness resolves against, so the
// very first run of this fixture found zero collisions (see the
// research doc's harness-commissioning note). Passing seedCellId
// explicitly bypasses the derivation and floods from the exact cell
// the replay loop queries.
engine.ShadowObjects.Register(
entityId: SyntheticGfxId,
gfxObjId: SyntheticGfxId,
worldPos: Vector3.Zero,
rotation: Quaternion.Identity,
radius: 30f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: LandblockId,
collisionType: ShadowCollisionType.BSP,
scale: 1.0f,
seedCellId: CellId);
return engine;
}
public sealed record TickSample(
int Tick,
Vector3 Pos,
float Advance,
bool CollisionNormalValid,
Vector3 CollisionNormal,
bool OnGround,
int FrozenStreak);
///
/// Replays the real captured ballistic approach + landing, then keeps
/// REQUESTING forward motion (simulating held input) for
/// additional ticks once grounded, to
/// see whether the engine allows continued advance across the roof surface
/// or wedges in place. Returns one per tick.
///
public static List ReplayRealRoofLanding(int postLandingTicks = 60)
{
var engine = MakeRoofEngine();
const float dt = 1f / TicksPerSecond;
var body = new PhysicsBody { TransientState = TransientStateFlags.Active };
Vector3 pos = ApproachStartPosReal - RoofCentroid;
Vector3 vel = ApproachStartVel;
uint cell = CellId;
bool grounded = false;
int frozenStreak = 0;
int ticksSinceGrounded = -1;
var samples = new List();
// Budget: enough ticks to cover the ~18-tick ballistic approach plus the
// requested post-landing continuation window.
int maxTicks = 18 + postLandingTicks + 20;
for (int tick = 0; tick < maxTicks; tick++)
{
if (!grounded)
vel = new Vector3(vel.X, vel.Y, vel.Z + Gravity * dt);
// Once grounded, keep requesting the SAME horizontal advance each
// tick (simulating held forward input) — this is the "does a
// slide continue" probe. Vertical requested delta is zero (resting
// against the surface, not still falling).
Vector3 requestedVel = grounded ? new Vector3(vel.X, vel.Y, 0f) : vel;
Vector3 target = pos + requestedVel * dt;
var result = engine.ResolveWithTransition(
currentPos: pos,
targetPos: target,
cellId: cell,
sphereRadius: SphereRadius,
sphereHeight: SphereHeight,
stepUpHeight: 0.6f,
stepDownHeight: 1.5f,
isOnGround: grounded,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0x01000000u);
float advance = Vector3.Distance(result.Position, pos);
if (advance < 0.001f)
frozenStreak++;
else
frozenStreak = 0;
samples.Add(new TickSample(
tick, result.Position, advance,
result.CollisionNormalValid, result.CollisionNormal,
result.IsOnGround, frozenStreak));
pos = result.Position;
cell = result.CellId;
body.Position = pos;
if (!grounded && result.IsOnGround)
{
grounded = true;
ticksSinceGrounded = 0;
}
else if (grounded)
{
ticksSinceGrounded++;
if (ticksSinceGrounded >= postLandingTicks)
break;
}
}
return samples;
}
///
/// Characterization test: dumps the full per-tick trajectory so the A/B
/// bisect (this file's class doc) can diff HEAD vs the S1-reverted local
/// edit. Always passes — this is a diagnostic capture, matching the
/// project's existing LiveCompare_FirstCap_DiagnosticDump-style
/// tests. The actual pass/fail verdict is recorded in
/// docs/research/2026-07-30-265-capture-bisect.md, not as a
/// hardcoded assertion here, because the correct fix shape (and therefore
/// the correct future regression assertion) is still being decided.
///
[Fact]
public void RealCapturedRoofLanding_CharacterizeCurrentBehavior()
{
PhysicsDiagnostics.ResetForTest();
PhysicsDiagnostics.ProbeIndoorBspEnabled = true;
PhysicsDiagnostics.ProbeBuildingEnabled = true;
try
{
var samples = ReplayRealRoofLanding();
int maxFrozen = 0;
int landedAtTick = -1;
foreach (var s in samples)
{
maxFrozen = System.Math.Max(maxFrozen, s.FrozenStreak);
if (landedAtTick < 0 && s.OnGround) landedAtTick = s.Tick;
_out.WriteLine(string.Format(
System.Globalization.CultureInfo.InvariantCulture,
"t{0,3}: pos=({1:F3},{2:F3},{3:F3}) adv={4:F4} cnv={5} n=({6:F3},{7:F3},{8:F3}) onGround={9} frozen={10}",
s.Tick, s.Pos.X, s.Pos.Y, s.Pos.Z, s.Advance,
s.CollisionNormalValid, s.CollisionNormal.X, s.CollisionNormal.Y, s.CollisionNormal.Z,
s.OnGround, s.FrozenStreak));
}
_out.WriteLine($"=== landedAtTick={landedAtTick} maxFrozenStreak={maxFrozen} totalTicks={samples.Count} ===");
// Sanity-only assertion: the replay must actually reach the roof
// (land) within the ballistic approach window — if this fails the
// synthetic fixture itself is wrong, not a physics-engine finding.
Assert.True(landedAtTick is >= 0 and < 30,
$"Replay never reached the synthetic roof polygon (landedAtTick={landedAtTick}); " +
"fixture geometry or approach trajectory needs adjustment before this is a valid oracle.");
}
finally
{
PhysicsDiagnostics.ResetForTest();
}
}
}