fix(physics): preserve refreshed cell retry state

This commit is contained in:
Erik 2026-07-31 12:31:34 +02:00
parent e5f855ac40
commit 67d1e9b331
3 changed files with 183 additions and 11 deletions

View file

@ -70,6 +70,7 @@ public sealed class PhysicsEngine
/// response coupling.
/// </summary>
internal Func<
Transition,
TransitionCellCollisionPhase,
uint,
TransitionState,

View file

@ -1728,6 +1728,11 @@ public sealed class Transition
/// The former flat per-landblock object query is gone.
/// </para>
/// </summary>
internal TransitionState TransitionalInsertForTest(
int numAttempts,
PhysicsEngine engine)
=> TransitionalInsert(numAttempts, engine);
private TransitionState TransitionalInsert(int numAttempts, PhysicsEngine engine)
{
if (SpherePath.CheckCellId == 0) return TransitionState.OK;
@ -2133,7 +2138,7 @@ public sealed class Transition
TransitionState state = TransitionState.OK;
for (int attempt = 0; attempt < numAttempts; attempt++)
{
state = FindPrimaryCellCollisions(engine, cellId, attempt);
state = FindPrimaryCellCollisions(engine, attempt);
if (state is TransitionState.OK or TransitionState.Collided)
return state;
@ -2154,41 +2159,42 @@ public sealed class Transition
/// </summary>
private TransitionState FindPrimaryCellCollisions(
PhysicsEngine engine,
uint cellId,
int innerAttempt)
{
TransitionState actualEnvironment = FindEnvCollisions(engine);
uint currentCellId = SpherePath.CheckCellId;
TransitionState environment = ObservePrimaryCellPhase(
engine,
TransitionCellCollisionPhase.Environment,
cellId,
FindEnvCollisions(engine));
currentCellId,
actualEnvironment);
if (environment != TransitionState.OK)
return environment;
TransitionState building = ObservePrimaryCellPhase(
engine,
TransitionCellCollisionPhase.Building,
cellId,
FindBuildingCollisions(engine, cellId));
currentCellId,
FindBuildingCollisions(engine, currentCellId));
if (building != TransitionState.OK)
return building;
TransitionState objects = ObservePrimaryCellPhase(
engine,
TransitionCellCollisionPhase.Objects,
cellId,
FindObjCollisionsInCell(engine, cellId));
currentCellId,
FindObjCollisionsInCell(engine, currentCellId));
DumpPhase2(innerAttempt, environment, objects);
return objects;
}
private static TransitionState ObservePrimaryCellPhase(
private TransitionState ObservePrimaryCellPhase(
PhysicsEngine engine,
TransitionCellCollisionPhase phase,
uint cellId,
TransitionState actual)
=> engine.TransitionCellCollisionTestHook is { } hook
? hook(phase, cellId, actual)
? hook(this, phase, cellId, actual)
: actual;
private TransitionState EdgeSlideAfterStepDownFailed(

View file

@ -31,7 +31,7 @@ public sealed class TransitionInsertIntoCellRetryTests
int buildingCalls = 0;
int objectCalls = 0;
engine.TransitionCellCollisionTestHook = (phase, cellId, actual) =>
engine.TransitionCellCollisionTestHook = (_, phase, cellId, actual) =>
{
Assert.Equal(Cell, cellId);
Assert.Equal(TransitionState.OK, actual);
@ -109,6 +109,148 @@ public sealed class TransitionInsertIntoCellRetryTests
+ "outer N=3 budget while remaining inside retail's N×N budget.");
}
[Fact]
public void NestedRetry_AlwaysAdjusted_ExhaustsExactNByNBudget()
{
PhysicsEngine engine = BuildEngine(preparedFlat: false);
int calls = 0;
engine.TransitionCellCollisionTestHook =
(_, phase, cellId, actual) =>
{
Assert.Equal(TransitionCellCollisionPhase.Environment, phase);
Assert.Equal(Cell, cellId);
Assert.Equal(TransitionState.OK, actual);
calls++;
return TransitionState.Adjusted;
};
Vector3 current = new(10f, 10f, 5f);
Vector3 target = current + new Vector3(0.05f, 0f, 0f);
Transition transition = BSPStepUpFixtures.MakeAirborneTransition(
current,
target,
Cell);
transition.SpherePath.SetCheckPos(target, Cell);
TransitionState result = transition.TransitionalInsertForTest(3, engine);
Assert.Equal(TransitionState.Adjusted, result);
Assert.Equal(target, transition.SpherePath.CheckPos);
Assert.Equal(9, calls);
}
[Fact]
public void NestedRetry_SlidClearsInnerContact_ThenOuterNegPoly()
{
PhysicsEngine engine = BuildEngine(preparedFlat: false);
int environmentCalls = 0;
var contactPlane = new Plane(Vector3.UnitZ, -5f);
engine.TransitionCellCollisionTestHook =
(transition, phase, _, actual) =>
{
if (phase != TransitionCellCollisionPhase.Environment)
return actual;
environmentCalls++;
if (environmentCalls <= 3)
{
if (environmentCalls > 1)
{
Assert.False(transition.CollisionInfo.ContactPlaneValid);
Assert.False(transition.CollisionInfo.ContactPlaneIsWater);
Assert.True(transition.SpherePath.NegPolyHit);
}
transition.CollisionInfo.SetContactPlane(
contactPlane,
Cell,
isWater: true);
transition.SpherePath.NegPolyHit = true;
return TransitionState.Slid;
}
// Three Slid responses exhaust the first inner N=3 budget.
// Its final Slid clear removes contact/water; the outer
// transitional_insert boundary additionally removes neg-poly.
Assert.False(transition.CollisionInfo.ContactPlaneValid);
Assert.False(transition.CollisionInfo.ContactPlaneIsWater);
Assert.False(transition.SpherePath.NegPolyHit);
return actual;
};
Vector3 current = new(10f, 10f, 5f);
ResolveResult result = ResolveAirborne(
engine,
current,
current + new Vector3(0.05f, 0f, 0f),
Cell);
Assert.True(result.Ok);
Assert.Equal(4, environmentCalls);
}
[Fact]
public void NoDataCache_CrossLandblockRepick_UsesRefreshedPrimaryCell()
{
PhysicsEngine engine = BuildCrossLandblockEngine();
var cells = new List<uint>();
var phases = new List<TransitionCellCollisionPhase>();
engine.TransitionCellCollisionTestHook =
(_, phase, cellId, actual) =>
{
phases.Add(phase);
cells.Add(cellId);
return actual;
};
Vector3 current = new(191.99f, 10f, 5f);
Vector3 target = new(192.01f, 10f, 5f);
ResolveResult result = ResolveAirborne(
engine,
current,
target,
0xA9B40039u);
Assert.True(result.Ok);
Assert.Equal(target, result.Position);
Assert.Equal(0xAAB40001u, result.CellId);
Assert.Equal(
new[]
{
TransitionCellCollisionPhase.Environment,
TransitionCellCollisionPhase.Building,
TransitionCellCollisionPhase.Objects,
},
phases);
Assert.All(cells, cellId => Assert.Equal(0xAAB40001u, cellId));
}
private static ResolveResult ResolveAirborne(
PhysicsEngine engine,
Vector3 current,
Vector3 target,
uint cellId)
{
var body = new PhysicsBody
{
Position = current,
Orientation = Quaternion.Identity,
TransientState = TransientStateFlags.Active,
};
return engine.ResolveWithTransition(
current,
target,
cellId,
sphereRadius: 0.48f,
sphereHeight: 1.835f,
stepUpHeight: 0.6f,
stepDownHeight: 1.5f,
isOnGround: false,
body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0x000F4243u);
}
private static PhysicsEngine BuildEngine(bool preparedFlat)
{
var (root, resolved) = BSPStepUpFixtures.FlatRoof();
@ -166,4 +308,27 @@ public sealed class TransitionInsertIntoCellRetryTests
0f);
return engine;
}
private static PhysicsEngine BuildCrossLandblockEngine()
{
var engine = new PhysicsEngine { DataCache = null };
var heights = new byte[81];
var heightTable = new float[256];
Array.Fill(heightTable, -1000f);
engine.AddLandblock(
0xA9B4FFFFu,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
engine.AddLandblock(
0xAAB4FFFFu,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
192f,
0f);
return engine;
}
}