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;
///
/// Pins the retail halt semantics of
/// (0x0050ae50), plus the available outdoor cell's terrain → building →
/// objects dispatch (CLandCell::find_collisions 0x00532d60). Neighbor fixtures
/// use prepared-flat production traversal with no replacement result hooks.
///
public class TransitionCheckOtherCellsTests
{
private static Transition MakeTransition(bool contactFlag = false)
{
var t = new Transition();
t.SpherePath.InitPath(Vector3.Zero, Vector3.Zero, cellId: 0xA9B40100u, sphereRadius: 0.48f);
t.ObjectInfo.State = contactFlag ? ObjectInfoState.Contact : ObjectInfoState.None;
// Pre-set CP fields to non-default so the Slid-clears-CP assertion
// can detect the clear.
t.CollisionInfo.ContactPlaneValid = true;
t.CollisionInfo.ContactPlaneIsWater = true;
return t;
}
[Fact]
public void OK_ContinuesIteration_DoesNotMutate()
{
var t = MakeTransition();
bool halt = t.ApplyOtherCellResult(TransitionState.OK, out var finalState);
Assert.False(halt);
Assert.Equal(TransitionState.OK, finalState);
Assert.True(t.CollisionInfo.ContactPlaneValid);
Assert.True(t.CollisionInfo.ContactPlaneIsWater);
Assert.False(t.CollisionInfo.CollidedWithEnvironment);
}
[Fact]
public void Collided_HaltsAndSetsCollidedWithEnvironment_WhenNotInContact()
{
var t = MakeTransition(contactFlag: false);
bool halt = t.ApplyOtherCellResult(TransitionState.Collided, out var finalState);
Assert.True(halt);
Assert.Equal(TransitionState.Collided, finalState);
Assert.True(t.CollisionInfo.CollidedWithEnvironment);
}
[Fact]
public void Collided_DoesNotSetCollidedWithEnvironment_WhenInContact()
{
// Retail oracle gating: the CollidedWithEnvironment flip mirrors
// the existing primary-cell behavior in FindEnvCollisions —
// skipped when ObjectInfo.State has Contact bit set.
var t = MakeTransition(contactFlag: true);
bool halt = t.ApplyOtherCellResult(TransitionState.Collided, out var finalState);
Assert.True(halt);
Assert.Equal(TransitionState.Collided, finalState);
Assert.False(t.CollisionInfo.CollidedWithEnvironment);
}
[Fact]
public void Adjusted_HaltsAndSetsCollidedWithEnvironment_WhenNotInContact()
{
var t = MakeTransition(contactFlag: false);
bool halt = t.ApplyOtherCellResult(TransitionState.Adjusted, out var finalState);
Assert.True(halt);
Assert.Equal(TransitionState.Adjusted, finalState);
Assert.True(t.CollisionInfo.CollidedWithEnvironment);
}
[Fact]
public void Slid_HaltsAndClearsContactPlaneFields()
{
// Retail oracle: acclient_2013_pseudo_c.txt:272746-272750
// case 4:
// this->collision_info.contact_plane_valid = 0;
// this->collision_info.contact_plane_is_water = 0;
// return result;
var t = MakeTransition();
Assert.True(t.CollisionInfo.ContactPlaneValid); // pre-condition
Assert.True(t.CollisionInfo.ContactPlaneIsWater); // pre-condition
bool halt = t.ApplyOtherCellResult(TransitionState.Slid, out var finalState);
Assert.True(halt);
Assert.Equal(TransitionState.Slid, finalState);
Assert.False(t.CollisionInfo.ContactPlaneValid);
Assert.False(t.CollisionInfo.ContactPlaneIsWater);
}
[Fact]
public void CheckOtherCells_CellWithNullBspRoot_IsSkippedNoCrash()
{
// Iteration safety: a CellPhysics in the candidate set with
// `BSP = null` (loaded for render but not physics) must be skipped,
// not crash. Matches the spec's R2 guard at design §Edge cases E2.
var cell = new CellPhysics
{
BSP = null, // <-- the guard target
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Resolved = new Dictionary(),
};
var engine = new PhysicsEngine();
engine.DataCache = new PhysicsDataCache(); // PhysicsEngine has nullable DataCache
// FindEnvCollisions has terrain probes downstream; populate a
// minimal landblock so the cache + engine are coherent. The cell
// we test against doesn't need a real landblock entry.
var heights = new byte[81];
Array.Fill(heights, (byte)0);
var ht = new float[256];
for (int i = 0; i < 256; i++) ht[i] = i * 1.0f;
engine.AddLandblock(0xA9B4FFFFu, new TerrainSurface(heights, ht),
Array.Empty(), Array.Empty(),
worldOffsetX: 0f, worldOffsetY: 0f);
engine.DataCache.RegisterCellStructForTest(0xA9B40157u, cell);
var t = MakeTransition();
var cellSet = new HashSet { 0xA9B40157u };
// Call CheckOtherCells directly via the internal seam.
var result = t.CheckOtherCells(engine, Vector3.Zero, 0.48f, cellSet);
Assert.Equal(TransitionState.OK, result);
}
[Fact]
public void CheckOtherCells_OutdoorLandcellCandidate_UsesTerrainWalkable()
{
var engine = new PhysicsEngine();
engine.DataCache = new PhysicsDataCache();
var heights = new byte[81];
Array.Fill(heights, (byte)0);
var ht = new float[256];
for (int i = 0; i < 256; i++) ht[i] = i * 1.0f;
engine.AddLandblock(0xA9B4FFFFu, new TerrainSurface(heights, ht),
Array.Empty(), Array.Empty(),
worldOffsetX: 0f, worldOffsetY: 0f);
var t = MakeTransition(contactFlag: true);
t.SpherePath.InitPath(new Vector3(10f, 10f, -0.28f), new Vector3(10f, 10f, -0.28f),
cellId: 0xA9B40147u, sphereRadius: 0.48f);
var footCenter = new Vector3(10f, 10f, 0.20f);
var cellSet = new HashSet { 0xA9B40001u };
var result = t.CheckOtherCells(engine, footCenter, 0.48f, cellSet);
Assert.Equal(TransitionState.Adjusted, result);
Assert.True(t.CollisionInfo.ContactPlaneValid);
Assert.Equal(0xA9B40001u, t.CollisionInfo.ContactPlaneCellId);
Assert.True(t.SpherePath.CheckPos.Z > -0.28f);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void OutdoorTerrainMiss_RetainsPreparedBuildingAndObjectCollision(bool objectChannel)
{
PhysicsEngine engine = NeighborWall(objectChannel);
Transition transition = NeighborTransition();
Assert.NotNull(engine.DataCache!.CellGraph.GetVisible(NeighborCell));
Assert.Null(engine.SampleTerrainWalkableInCell(NeighborCell, 23.8f, 12f));
TransitionState result = transition.CheckOtherCells(engine,
transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell });
// Actual flat BSP Path 6 calls SetCollide and returns Adjusted for
// an unclipped airborne sphere; its normal belongs to StepUpNormal.
Assert.Equal(TransitionState.Adjusted, result);
Assert.True(transition.SpherePath.Collide);
Assert.Equal(-Vector3.UnitX, transition.SpherePath.StepUpNormal);
if (objectChannel)
Assert.Equal(NeighborObject, transition.CollisionInfo.LastCollidedObjectGuid);
else
Assert.True(transition.CollisionInfo.CollidedWithEnvironment);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void OutdoorTerrainMiss_NonintersectingPreparedNeighborRemainsClear(bool objectChannel)
{
PhysicsEngine engine = NeighborWall(objectChannel, wallOriginX: 30f);
Transition transition = NeighborTransition();
Vector3 target = transition.SpherePath.CheckPos;
TransitionState result = transition.CheckOtherCells(engine,
transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell });
Assert.Equal(TransitionState.OK, result);
Assert.Equal(target, transition.SpherePath.CheckPos);
Assert.False(transition.CollisionInfo.CollisionNormalValid);
Assert.Null(transition.CollisionInfo.LastCollidedObjectGuid);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void UnavailableOutdoorCell_DoesNotDispatchRetainedBuildingOrObjects(bool objectChannel)
{
PhysicsEngine engine = NeighborWall(objectChannel, wallOriginX: 24f);
Transition available = NeighborTransition(targetX: 24.3f);
Assert.Equal(NeighborCell, engine.SampleTerrainWalkable(24.3f, 12f)!.Value.CellId);
Assert.Equal(TransitionState.Adjusted, available.CheckOtherCells(engine,
available.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell }));
// Deliberately leave stale cached geometry and engine terrain behind:
// CellGraph availability, not a terrain sample, governs dispatch.
engine.DataCache!.CellGraph.RemoveLandblock(NeighborCell);
Assert.Null(engine.DataCache.CellGraph.GetVisible(NeighborCell));
Assert.NotNull(engine.DataCache.GetGfxObj(NeighborGfx));
if (objectChannel)
Assert.NotEmpty(engine.ShadowObjects.GetObjectsInCell(NeighborCell));
else
Assert.NotNull(engine.DataCache.GetBuilding(NeighborCell));
Transition transition = NeighborTransition(targetX: 24.3f);
Vector3 target = transition.SpherePath.CheckPos;
TransitionState result = transition.CheckOtherCells(engine,
transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell });
Assert.Equal(TransitionState.OK, result);
Assert.Equal(target, transition.SpherePath.CheckPos);
Assert.False(transition.CollisionInfo.CollisionNormalValid);
Assert.Null(transition.CollisionInfo.LastCollidedObjectGuid);
}
[Fact]
public void OutdoorPrimaryCell_IsNotDispatchedAgain()
{
PhysicsEngine engine = NeighborWall(objectChannel: false);
Transition transition = NeighborTransition();
transition.SpherePath.SetCheckPos(transition.SpherePath.CheckPos, NeighborCell);
Assert.Equal(TransitionState.OK, transition.CheckOtherCells(engine,
transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell }));
Assert.False(transition.CollisionInfo.CollisionNormalValid);
}
[Fact]
public void NeighborBuildingNonOk_StopsBeforeNeighborObjects()
{
PhysicsEngine engine = NeighborWall(objectChannel: false);
engine.ShadowObjects.Register(NeighborObject, NeighborGfx, new Vector3(23.5f, 12f, 0f),
Quaternion.Identity, 10f, 0f, 0f, 0xA9B40000u,
collisionType: ShadowCollisionType.BSP, state: 0x00010000u,
seedCellId: NeighborCell, isStatic: true);
Assert.NotEmpty(engine.ShadowObjects.GetObjectsInCell(NeighborCell));
Transition transition = NeighborTransition();
Assert.Equal(TransitionState.Adjusted, transition.CheckOtherCells(engine,
transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell }));
Assert.True(transition.CollisionInfo.CollidedWithEnvironment);
Assert.Null(transition.CollisionInfo.LastCollidedObjectGuid);
}
private const uint NeighborCell = 0xA9B40009u;
private const uint NeighborGfx = 0x0100F482u;
private const uint NeighborObject = 0xCA9B4482u;
private static Transition NeighborTransition(float targetX = 23.8f)
{
Vector3 target = new(targetX, 12f, 1f);
var transition = new Transition();
transition.SpherePath.InitPath(new Vector3(targetX - 0.4f, 12f, 1f), target,
0xA9B40001u, 0.48f);
transition.SpherePath.SetCheckPos(target, 0xA9B40001u);
return transition;
}
private static PhysicsEngine NeighborWall(bool objectChannel, float wallOriginX = 23.5f)
{
var (root, polygons) = BSPStepUpFixtures.TallWall();
var normalized = new Dictionary();
foreach ((ushort id, ResolvedPolygon polygon) in polygons)
normalized.Add(id, new ResolvedPolygon
{
Id = id, Vertices = polygon.Vertices, Plane = polygon.Plane,
NumPoints = polygon.NumPoints, SidesType = polygon.SidesType,
});
var graph = new GfxObjPhysics
{
SourceId = NeighborGfx,
BSP = new PhysicsBSPTree { Root = root },
Resolved = normalized,
BoundingSphere = root.BoundingSphere,
};
var cache = PhysicsDataCache.CreateProduction();
cache.CacheGfxObj(NeighborGfx, FlatCollisionAssetBuilder.FlattenGfxObj(graph));
Assert.Equal(CollisionTraversalMode.Flat, cache.CollisionTraversalMode);
Assert.Null(cache.GetGfxObj(NeighborGfx)!.BSP);
Assert.NotNull(cache.GetGfxObj(NeighborGfx)!.FlatPhysicsBsp);
var engine = new PhysicsEngine { DataCache = cache };
var heightTable = new float[256];
Array.Fill(heightTable, -1000f);
engine.AddLandblock(0xA9B4FFFFu, new TerrainSurface(new byte[81], heightTable),
Array.Empty(), Array.Empty(), 0f, 0f);
Vector3 origin = new(wallOriginX, 12f, 0f);
if (objectChannel)
{
engine.ShadowObjects.Register(NeighborObject, NeighborGfx, origin,
Quaternion.Identity, 10f, 0f, 0f, 0xA9B40000u,
collisionType: ShadowCollisionType.BSP, state: 0x00010000u,
seedCellId: NeighborCell, isStatic: true);
Assert.Contains(engine.ShadowObjects.GetObjectsInCell(NeighborCell),
entry => entry.EntityId == NeighborObject);
}
else
{
cache.CacheBuilding(NeighborCell, Array.Empty(),
Matrix4x4.CreateTranslation(origin), NeighborGfx);
}
return engine;
}
}