acdream/tests/AcDream.Core.Tests/Physics/TransitionInsertIntoCellRetryTests.cs

169 lines
5.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Pins retail's two-level collision retry structure:
/// <c>transitional_insert(N)</c> wraps <c>insert_into_cell(N)</c>, and every
/// inner retry restarts the complete cell transaction in
/// environment → building → objects order.
/// </summary>
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<TransitionCellCollisionPhase>();
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<ushort, ResolvedPolygon>(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<BldPortalInfo>(),
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<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
return engine;
}
}