using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
///
/// Pins retail's two-level collision retry structure:
/// transitional_insert(N) wraps insert_into_cell(N), and every
/// inner retry restarts the complete cell transaction in
/// environment → building → objects order.
///
public sealed class TransitionInsertIntoCellRetryTests
{
private const uint Landblock = 0xA9B40000u;
private const uint Cell = 0xA9B40001u;
private const uint ShellGfxObj = 0x0100F001u;
[Theory]
[InlineData(false)]
[InlineData(true)]
public void NestedRetry_RecomputesAtomicCellPipeline_AndExceedsOuterBudget(
bool preparedFlat)
{
PhysicsEngine engine = BuildEngine(preparedFlat);
var phases = new List();
int environmentCalls = 0;
int buildingCalls = 0;
int objectCalls = 0;
engine.TransitionCellCollisionTestHook = (phase, cellId, actual) =>
{
Assert.Equal(Cell, cellId);
Assert.Equal(TransitionState.OK, actual);
phases.Add(phase);
switch (phase)
{
case TransitionCellCollisionPhase.Environment:
environmentCalls++;
return environmentCalls == 1
? TransitionState.Adjusted
: TransitionState.OK;
case TransitionCellCollisionPhase.Building:
buildingCalls++;
return buildingCalls == 1
? TransitionState.Adjusted
: TransitionState.OK;
case TransitionCellCollisionPhase.Objects:
objectCalls++;
return objectCalls == 1
? TransitionState.Adjusted
: TransitionState.OK;
default:
throw new ArgumentOutOfRangeException(nameof(phase));
}
};
Vector3 current = new(10f, 10f, 5f);
Vector3 target = current + new Vector3(0.05f, 0f, 0f);
var body = new PhysicsBody
{
Position = current,
Orientation = Quaternion.Identity,
TransientState = TransientStateFlags.Active,
};
ResolveResult result = engine.ResolveWithTransition(
current,
target,
Cell,
sphereRadius: 0.48f,
sphereHeight: 1.835f,
stepUpHeight: 0.6f,
stepDownHeight: 1.5f,
isOnGround: false,
body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0x000F4243u);
Assert.True(result.Ok);
Assert.Equal(target, result.Position);
// The first inner budget of three passes stops successively at env,
// building, and objects. The second outer attempt starts a fourth
// complete pass and succeeds. A flattened N=3 loop cannot reach it.
TransitionCellCollisionPhase[] expected =
[
TransitionCellCollisionPhase.Environment,
TransitionCellCollisionPhase.Environment,
TransitionCellCollisionPhase.Building,
TransitionCellCollisionPhase.Environment,
TransitionCellCollisionPhase.Building,
TransitionCellCollisionPhase.Objects,
TransitionCellCollisionPhase.Environment,
TransitionCellCollisionPhase.Building,
TransitionCellCollisionPhase.Objects,
];
Assert.Equal(expected, phases);
Assert.Equal(4, environmentCalls);
Assert.Equal(3, buildingCalls);
Assert.Equal(2, objectCalls);
Assert.True(
environmentCalls > 3,
"The fixture must require more complete cell passes than the "
+ "outer N=3 budget while remaining inside retail's N×N budget.");
}
private static PhysicsEngine BuildEngine(bool preparedFlat)
{
var (root, resolved) = BSPStepUpFixtures.FlatRoof();
var normalized = new Dictionary(resolved.Count);
foreach ((ushort id, ResolvedPolygon polygon) in resolved)
{
normalized.Add(id, new ResolvedPolygon
{
Id = id,
Vertices = polygon.Vertices,
Plane = polygon.Plane,
NumPoints = polygon.NumPoints,
SidesType = polygon.SidesType,
});
}
var physics = new GfxObjPhysics
{
SourceId = ShellGfxObj,
BSP = new PhysicsBSPTree { Root = root },
Resolved = normalized,
BoundingSphere = root.BoundingSphere,
};
var cache = new PhysicsDataCache();
if (preparedFlat)
{
cache.CollisionTraversalMode = CollisionTraversalMode.Flat;
cache.CacheGfxObj(
ShellGfxObj,
FlatCollisionAssetBuilder.FlattenGfxObj(physics));
}
else
{
cache.RegisterGfxObjForTest(ShellGfxObj, physics);
}
// The far-away shell makes the building channel execute its actual
// graph/flat traversal and return OK without affecting the mover.
cache.CacheBuilding(
Cell,
Array.Empty(),
Matrix4x4.CreateTranslation(100f, 100f, 100f),
ShellGfxObj);
var engine = new PhysicsEngine { DataCache = cache };
var heights = new byte[81];
var heightTable = new float[256];
Array.Fill(heightTable, -1000f);
engine.AddLandblock(
Landblock,
new TerrainSurface(heights, heightTable),
Array.Empty(),
Array.Empty(),
0f,
0f);
return engine;
}
}