From 67d1e9b331f88d1846b983ed3611d450b4e83408 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 31 Jul 2026 12:31:34 +0200 Subject: [PATCH] fix(physics): preserve refreshed cell retry state --- src/AcDream.Core/Physics/PhysicsEngine.cs | 1 + src/AcDream.Core/Physics/TransitionTypes.cs | 26 +-- .../TransitionInsertIntoCellRetryTests.cs | 167 +++++++++++++++++- 3 files changed, 183 insertions(+), 11 deletions(-) diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 33fe27ea..e78d74df 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -70,6 +70,7 @@ public sealed class PhysicsEngine /// response coupling. /// internal Func< + Transition, TransitionCellCollisionPhase, uint, TransitionState, diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index ace80f6c..b8aa4093 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -1728,6 +1728,11 @@ public sealed class Transition /// The former flat per-landblock object query is gone. /// /// + 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 /// 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( diff --git a/tests/AcDream.Core.Tests/Physics/TransitionInsertIntoCellRetryTests.cs b/tests/AcDream.Core.Tests/Physics/TransitionInsertIntoCellRetryTests.cs index dfee3d58..b97985c1 100644 --- a/tests/AcDream.Core.Tests/Physics/TransitionInsertIntoCellRetryTests.cs +++ b/tests/AcDream.Core.Tests/Physics/TransitionInsertIntoCellRetryTests.cs @@ -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(); + var phases = new List(); + 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(), + Array.Empty(), + 0f, + 0f); + engine.AddLandblock( + 0xAAB4FFFFu, + new TerrainSurface(heights, heightTable), + Array.Empty(), + Array.Empty(), + 192f, + 0f); + return engine; + } }