fix(physics): preserve retail step-down probe state
This commit is contained in:
parent
1fd5da67b4
commit
acec33eca8
5 changed files with 249 additions and 36 deletions
|
|
@ -130,7 +130,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
|
||||
| ~~AP-3~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `TransitionalInsert` now returns `OK_TS` immediately for every valid contact plane. Its ordinary StepDown tail is reachable only from invalid contact and retains the retail Contact / `!sphere_path.step_down` / check-cell / ObjectInfo.StepDown gates plus the exact one-versus-two-sphere probe split. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`TransitionalInsert`, `GetStepDownProbePlan`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::transitional_insert` 0x0050B6F0, named-retail pseudo-C pc:273191–273307 |
|
||||
| ~~AP-4~~ | **RETIRED 2026-07-31 (Campaign P Slice 1B).** `EdgeSlideAfterStepDownFailed` now evaluates retail Branch 1 (`!OnWalkable || !EdgeSlide` → restore + `OK_TS`) before the steep-contact `CliffSlide` branch. The former compensation is removed. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed`); `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::edge_slide` 0x0050B3D0, named-retail pseudo-C pc:273001–273090 |
|
||||
| ~~AP-5~~ | **RETIRED 2026-07-31 (Campaign P Slice 2A).** `DoStepDown` no longer accepts a caller-controlled `runPlacement` bypass. After the transitional support probe and retail `check_walkables` gate succeed, ordinary contact maintenance, edge-slide back-probes, and StepUp all switch to `PLACEMENT_INSERT`, reset `walk_interp` to 1, run the final insertion, restore the prior insert type, and accept only `OK_TS`. The former wall-slide justification is addressed at the actual placement dispatcher boundary: its retail epsilon-shaved overlap test permits exact wall tangency but rejects real penetration; no StepDown path skips validation. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`DoStepDown`); `src/AcDream.Core/Physics/BSPQuery.cs` / `FlatBspQuery.cs` (Placement dispatcher); `tests/AcDream.Core.Tests/Physics/RetailStepDownPlacementTests.cs` | — | — | `CTransition::step_down` 0x0050B2A0 pc:272946–272998; `BSPTREE::find_collisions` Placement branch 0x0053A440 pc:323742; `CSphere::intersects_sphere` 0x00537A80; `CCylSphere::intersects_sphere` 0x0053B440 |
|
||||
| ~~AP-5~~ | **RETIRED 2026-07-31 (Campaign P Slice 2A).** `DoStepDown` no longer accepts a caller-controlled `runPlacement` bypass. It resets `walk_interp` once at entry; after the transitional support probe and retail `check_walkables` gate succeed, ordinary contact maintenance, edge-slide back-probes, and StepUp all switch to `PLACEMENT_INSERT` with the exact carried interpolation value, run the final insertion, restore the prior insert type, and accept only `OK_TS`. Nested `DoCheckWalkable` uses local current-position saves and preserves the outer `SPHEREPATH` backup pair needed by edge-slide. The former wall-slide justification is addressed at the actual placement dispatcher boundary: its retail epsilon-shaved overlap test permits exact wall tangency but rejects real penetration; no StepDown path skips validation. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`DoStepDown`, `DoCheckWalkable`); `src/AcDream.Core/Physics/BSPQuery.cs` / `FlatBspQuery.cs` (Placement dispatcher); `tests/AcDream.Core.Tests/Physics/RetailStepDownPlacementTests.cs`; `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs` | — | — | `CTransition::check_walkable` 0x0050AFF0 pc:272811–272856; `CTransition::step_down` 0x0050B2A0 pc:272946–272998; `BSPTREE::find_collisions` Placement branch 0x0053A440 pc:323742; `CSphere::intersects_sphere` 0x00537A80; `CCylSphere::intersects_sphere` 0x0053B440 |
|
||||
| ~~AP-7~~ | **RETIRED 2026-07-30 (Campaign P Slice P2) — the "state gate" was a BN decompiler artifact, not a locomotion exemption.** `calc_friction` now ports retail's confirmed 0.25f threshold (`if (angle >= 0.25f) return;`) unconditionally, no special-cased gate. The "state check at pc:276702" the old row cited is `PhysicsState.Sledding` (confirmed via ACE's `PhysicsObj.calc_friction`, references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2120-2141, and `SLEDDING_PS=0x800000` in acclient.h:2838) — it gates the 1.5625/6.25/near-flat friction-value OVERRIDE, not the threshold return itself; acdream had no live Sledding setter then or now (see #166 research, docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §3), so the branch was simply unreachable dead code, not an exemption for ordinary walking. The reverted 2026-04-30 L.3c attempt (naive 0.0→0.25 bump, forward locomotion 3→0.16 m/s in `PlayerMovementControllerTests`) does not reproduce on the production graphical local-player path post-R6: `PlayerMovementController` zeroes `Velocity.X/Y` to exactly zero every tick before `calc_friction` runs whenever animation root motion drives the walk, so friction has no horizontal velocity left to hammer (pinned at the PhysicsBody level by `GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests`). The headless/`get_state_velocity` movement-controller path and remote/NPC movers still feed real velocity into this function and remain the ones to watch if a similar regression resurfaces there. **CORRECTION (2026-07-30, same day, #265/#166 capture bisect):** the sentence above undersold the gap — `calc_friction` wasn't merely "no horizontal velocity to hammer," it was structurally UNREACHABLE with meaningful data on ANY grounded path: (a) the animation-root-motion path zeroed `Velocity.X/Y` outright every tick (the actual #265/#166 root cause, ten days pre-existing, not a Campaign-P regression), and (b) `PhysicsBody.GroundNormal` — the vector `calc_friction` dots velocity against — had ZERO production writers anywhere and silently defaulted to `Vector3.UnitZ` forever, so even surviving velocity would have been tested against a fake flat-ground normal on any real slope. Both gaps are now closed: `PlayerMovementController.cs`'s grounded block no longer reconstructs `Velocity` for the animation-root-motion case, and `PhysicsEngine.cs` syncs `body.GroundNormal` from the committed `ContactPlane.Normal` at the same commit point that already publishes `ContactPlane`. The 0.25f threshold port itself (this row's original subject) was always correct — it just had nothing real to operate on until this fix. See `docs/research/2026-07-30-265-capture-bisect.md`'s as-fixed addendum. | `src/AcDream.Core/Physics/PhysicsBody.cs` (`calc_friction`); `src/AcDream.Core/Physics/PhysicsEngine.cs` (`GroundNormal` wiring); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (grounded-velocity fix); `tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs` (AP-7 test block); `tests/AcDream.Core.Tests/Physics/Issue265SteepSlopeCaptureBisectTests.cs`; `tests/AcDream.Runtime.Tests/Gameplay/PlayerMovementControllerTests.cs` | — | — | `CPhysicsObj::calc_friction` pc:276694-276822 (0050ee70); ACE `PhysicsObj.calc_friction` PhysicsObj.cs:2120-2141; `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §1; `docs/research/2026-07-30-265-capture-bisect.md` |
|
||||
| ~~AP-10~~ | **RETIRED 2026-07-30 (Campaign P Slice P4) — the retail 0.1 m dry-corner water sink-in is restored.** `TerrainSurface.SampleWaterDepth` (`src/AcDream.Core/Physics/TerrainSurface.cs`) now returns 0.1 for a partially-water cell's dry corner instead of the collapsed 0. The row's own "destabilizes the touch check" justification turned out to be structurally true of retail too (a skipped `SetContactPlane` reassertion is not a fall in ANY of retail/ACE/acdream, because `Contact`/`OnWalkable` are STICKY — `PhysicsEngine.ResolveWithTransition`'s `onGround` computation ORs the fresh per-call `ContactPlaneValid` with the seeded, persistent `PhysicsBody.TransientState.OnWalkable` bit) — traced and confirmed in this slice; see `docs/research/2026-07-29-remote-and-world-specials-pseudocode.md` §5.2. `PhysicsEngine.SampleTerrainWalkable`'s `isWater = waterDepth >= 0.45f` threshold means the restore does not flip the dry corner's water classification (0.1 still < 0.45) — only the sink-in depth changes. Full Release suite green (no regression) proves the sticky-bit argument held in practice, not just in theory. | `src/AcDream.Core/Physics/TerrainSurface.cs` (`SampleWaterDepth`) | — | — | `ObjCell.get_water_depth` / `calc_water_depth` (via ACE port); `docs/research/2026-07-29-remote-and-world-specials-pseudocode.md` §5.1-5.2 |
|
||||
| AP-11 | Hand-authored 4-keyframe fallback sky set (sunrise/noon/sunset, fog ~80–350 m) when the Region dat isn't loaded yet | `src/AcDream.Core/World/SkyState.cs:167` | A renderable sky is needed during boot before the Region dat parses; safety net on region-load failure | Any window where the fallback is active shows sky/fog lighting only roughly resembling retail's dat-driven values | SkyTimeOfDay keyframes, Region dat 0x13000000 |
|
||||
|
|
|
|||
|
|
@ -1146,6 +1146,14 @@ the `EdgeSlide && !StepUp` walkable-support gate, and then always:
|
|||
4. restores the saved insert type; and
|
||||
5. returns true only for `OK_TS`.
|
||||
|
||||
The `walk_interp = 1` assignment occurs once at `step_down` entry. Retail does
|
||||
not reset it again before the final Placement insertion: that insertion sees
|
||||
and carries the exact interpolation value left by the support probe. Likewise,
|
||||
`CTransition::check_walkable` (`0x0050AFF0`, pseudo-C:272811–272856) saves its
|
||||
temporary `check_pos` and cell in stack locals. It does not overwrite
|
||||
`SPHEREPATH::backup_check_pos`/`backup_cell`, because the enclosing edge-slide
|
||||
still needs that outer failed candidate after the nested support probe.
|
||||
|
||||
This is the same tail for normal grounded contact maintenance, the
|
||||
`edge_slide` current-position back-probe, and StepUp. The former acdream
|
||||
`runPlacement: false` argument on the first two callers was therefore a real
|
||||
|
|
@ -1161,12 +1169,16 @@ epsilon is rejected. With the response-order fixes from §8 in place, restoring
|
|||
the mandatory placement tail does not reproduce the old wall stall.
|
||||
|
||||
`RetailStepDownPlacementTests` pins the mechanism end to end for one- and
|
||||
two-sphere movers, ordinary contact maintenance, StepUp, and a supported
|
||||
candidate that overlaps only during final Placement. It also compares graph
|
||||
and prepared-flat Placement at exact wall tangency and beyond-epsilon overlap.
|
||||
The existing multi-frame roof/edge fixtures continue to compare the complete
|
||||
`ResolveResult` and persistent `PhysicsBody` state by raw bits, and the #273,
|
||||
#271, #185, StepUp, transition-retry, and TS-4 controls remain unchanged. AP-5
|
||||
is retired; TS-4 is intentionally untouched.
|
||||
two-sphere movers, including raw-bit `walk_interp` carry, nested
|
||||
`check_walkable` backup preservation, ordinary contact maintenance, StepUp,
|
||||
and a supported candidate that overlaps only during final Placement. It also
|
||||
compares graph and prepared-flat Placement at exact wall tangency and
|
||||
beyond-epsilon overlap. The full-engine floor-plus-wall replay drives ten
|
||||
grounded maintenance frames for both sphere counts, requires one Placement for
|
||||
every successful support maintenance, proves no freeze or penetration plus
|
||||
tangential progress, and compares complete `ResolveResult` and persistent
|
||||
`PhysicsBody` state by raw bits between parsed-graph and prepared-flat paths.
|
||||
The #273, #271, #185, StepUp, transition-retry, and TS-4 controls remain
|
||||
unchanged. AP-5 is retired; TS-4 is intentionally untouched.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -5234,29 +5234,12 @@ public sealed class Transition
|
|||
// overlapping solid geometry, for both ordinary contact
|
||||
// maintenance and StepUp.
|
||||
//
|
||||
// A6.P3 slice 4 (2026-05-22) — reset WalkInterp to 1.0 before
|
||||
// the placement_insert. The prior TransitionalInsert(5) probe
|
||||
// above may have consumed WalkInterp down to 0 (e.g. on a
|
||||
// step-up onto a sloped ramp — AdjustSphereToPlane lifted the
|
||||
// sphere by step_down_amt and ate all the interp). With
|
||||
// WalkInterp=0, subsequent AdjustSphereToPlane calls' `interp
|
||||
// >= path.WalkInterp` check fires immediately (0 >= 0) and
|
||||
// rejects any push-back — so any geometric overlap that needs
|
||||
// push-out resolution returns false → placement fails → step-up
|
||||
// returns failure → step_up_slide loop → player stuck.
|
||||
//
|
||||
// Retail's CTransition::step_down (acclient_2013_pseudo_c.txt:272952)
|
||||
// resets walk_interp = 1 at function entry. The placement_insert
|
||||
// (line 272989-272990) runs after that initial reset; we believe
|
||||
// placement-insert mode doesn't consume walk_interp the same way
|
||||
// because it's a "can we fit here" check, not a movement sweep.
|
||||
//
|
||||
// This fix is the cellar-up target (issue #98). May also help
|
||||
// other "step-up onto sloped surface" scenarios.
|
||||
// Retail resets walk_interp once at step_down entry. The final
|
||||
// placement consumes the exact value left by the support and
|
||||
// check-walkable probes; there is no second reset here.
|
||||
var savedInsert = sp.InsertType;
|
||||
float winterpBeforePlacement = sp.WalkInterp;
|
||||
sp.InsertType = InsertType.Placement;
|
||||
sp.WalkInterp = 1.0f;
|
||||
|
||||
var placeState = TransitionalInsert(1, engine);
|
||||
|
||||
|
|
@ -5453,12 +5436,9 @@ public sealed class Transition
|
|||
/// TransitionalInsert before re-testing as Placement.
|
||||
///
|
||||
/// <para>
|
||||
/// Returns true if a walkable surface was found within reach (i.e. the
|
||||
/// sphere can land here). Returns false if:
|
||||
/// - ObjectInfo.OnWalkable is NOT set (always walkable by convention).
|
||||
/// - CheckWalkables() already confirmed a walkable (skip the probe).
|
||||
/// - The downward probe returned OK (meaning: no walkable was found
|
||||
/// within reach, so we CANNOT land → transitState == OK → return false).
|
||||
/// Returns true when no support check is required or when a walkable
|
||||
/// surface is found within reach. Returns false when the downward probe
|
||||
/// returns OK (no walkable collision was found).
|
||||
/// </para>
|
||||
///
|
||||
/// ACE: Transition.CheckWalkable (Transition.cs:206-235).
|
||||
|
|
@ -5478,7 +5458,13 @@ public sealed class Transition
|
|||
if (sp.CheckWalkables())
|
||||
return true;
|
||||
|
||||
sp.SaveCheckPos();
|
||||
// Retail CTransition::check_walkable (0x0050AFF0) uses stack locals
|
||||
// for this nested probe. Preserve SPHEREPATH's outer backup pair for
|
||||
// edge_slide if the support check fails.
|
||||
Vector3 savedCheckPos = sp.CheckPos;
|
||||
uint savedCheckCellId = sp.CheckCellId;
|
||||
Vector3 savedBackupCheckPos = sp.BackupCheckPos;
|
||||
uint savedBackupCheckCellId = sp.BackupCheckCellId;
|
||||
|
||||
float stepHeight = oi.StepDownHeight;
|
||||
var globSphere = sp.GlobalSphere[0];
|
||||
|
|
@ -5496,7 +5482,9 @@ public sealed class Transition
|
|||
var transitState = TransitionalInsert(1, engine);
|
||||
|
||||
sp.CheckWalkable = false;
|
||||
sp.RestoreCheckPos();
|
||||
sp.SetCheckPos(savedCheckPos, savedCheckCellId);
|
||||
sp.BackupCheckPos = savedBackupCheckPos;
|
||||
sp.BackupCheckCellId = savedBackupCheckCellId;
|
||||
|
||||
// ACE returns (transitState != OK) — i.e. true when we DID find a
|
||||
// walkable (collision probe returned Adjusted/Collided).
|
||||
|
|
|
|||
|
|
@ -322,6 +322,52 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
$"{ledge[^1].Result.Position}.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void MultiFrameGroundedFloorWallSlide_FinalPlacementIsMandatoryAndGraphFlatExact(
|
||||
bool twoSpheres)
|
||||
{
|
||||
WallMaintenanceTrace graph = RunGroundedFloorWallSlide(
|
||||
preparedFlat: false,
|
||||
twoSpheres);
|
||||
WallMaintenanceTrace flat = RunGroundedFloorWallSlide(
|
||||
preparedFlat: true,
|
||||
twoSpheres);
|
||||
|
||||
Assert.Equal(graph.SupportPasses, flat.SupportPasses);
|
||||
Assert.Equal(graph.PlacementPasses, flat.PlacementPasses);
|
||||
Assert.True(graph.SupportPasses > 0,
|
||||
"The full-engine replay never entered grounded support maintenance.");
|
||||
Assert.Equal(graph.Frames.Count, graph.SupportPasses);
|
||||
Assert.Equal(graph.SupportPasses, graph.PlacementPasses);
|
||||
Assert.Equal(graph.Frames.Count, flat.Frames.Count);
|
||||
for (int i = 0; i < graph.Frames.Count; i++)
|
||||
{
|
||||
Assert.Equal(graph.Frames[i].ResolveBits, flat.Frames[i].ResolveBits);
|
||||
Assert.Equal(graph.Frames[i].BodyBits, flat.Frames[i].BodyBits);
|
||||
Assert.True(graph.Frames[i].Result.Ok,
|
||||
$"Grounded wall slide failed at frame {i}.");
|
||||
Assert.True(graph.Frames[i].Result.InContact,
|
||||
$"Ground contact was lost at frame {i}.");
|
||||
Assert.True(graph.Frames[i].Result.OnWalkable,
|
||||
$"Walkable state was lost at frame {i}.");
|
||||
|
||||
float wallLimit = 0.5f - BSPStepUpFixtures.SphereRadius
|
||||
+ PhysicsGlobals.EPSILON * 10f;
|
||||
Assert.True(graph.Frames[i].Result.Position.X <= wallLimit,
|
||||
$"Wall penetration at frame {i}: X={graph.Frames[i].Result.Position.X}, " +
|
||||
$"limit={wallLimit}.");
|
||||
}
|
||||
|
||||
AssertNoLongFrozenStreak(graph.Frames, maximumTicks: 1);
|
||||
Assert.True(
|
||||
graph.Frames[^1].Result.Position.Y
|
||||
> graph.Frames[0].Result.Position.Y + 0.25f,
|
||||
$"Wall response removed tangential progress: " +
|
||||
$"{graph.Frames[0].Result.Position} -> {graph.Frames[^1].Result.Position}.");
|
||||
}
|
||||
|
||||
private static Transition MakeFailedStepDownTransition()
|
||||
{
|
||||
Vector3 current = Vector3.Zero;
|
||||
|
|
@ -468,6 +514,80 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
return new TraceRun(trace, landedFrame, ledgeStartFrame);
|
||||
}
|
||||
|
||||
private static WallMaintenanceTrace RunGroundedFloorWallSlide(
|
||||
bool preparedFlat,
|
||||
bool twoSpheres)
|
||||
{
|
||||
var fixture = BSPStepUpFixtures.TallWall();
|
||||
PhysicsEngine engine = BuildCollisionEngine(
|
||||
fixture,
|
||||
preparedFlat,
|
||||
0x0100E103u);
|
||||
int supportPasses = 0;
|
||||
int placementPasses = 0;
|
||||
engine.TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||
{
|
||||
if (phase == TransitionCellCollisionPhase.Environment)
|
||||
{
|
||||
if (candidate.SpherePath.StepDown
|
||||
&& actual == TransitionState.OK
|
||||
&& candidate.CollisionInfo.ContactPlaneValid
|
||||
&& candidate.CollisionInfo.ContactPlane.Normal.Z
|
||||
>= candidate.SpherePath.WalkableAllowance)
|
||||
supportPasses++;
|
||||
else if (candidate.SpherePath.InsertType == InsertType.Placement)
|
||||
placementPasses++;
|
||||
}
|
||||
|
||||
return actual;
|
||||
};
|
||||
|
||||
float radius = BSPStepUpFixtures.SphereRadius;
|
||||
Vector3 position = new(0.5f - radius, -0.55f, 0f);
|
||||
var floor = fixture.Resolved[BSPStepUpFixtures.TallWall_FloorId];
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Position = position,
|
||||
Orientation = Quaternion.Identity,
|
||||
GroundNormal = Vector3.UnitZ,
|
||||
ContactPlaneValid = true,
|
||||
ContactPlane = floor.Plane,
|
||||
ContactPlaneCellId = Cell,
|
||||
WalkablePolygonValid = true,
|
||||
WalkablePlane = floor.Plane,
|
||||
WalkableVertices = floor.Vertices,
|
||||
WalkableUp = Vector3.UnitZ,
|
||||
TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable,
|
||||
};
|
||||
var trace = new List<TraceFrame>(10);
|
||||
|
||||
for (int tick = 0; tick < 10; tick++)
|
||||
{
|
||||
body.Velocity = new Vector3(1.8f, 2.1f, 0f);
|
||||
ResolveResult result = engine.ResolveWithTransition(
|
||||
position,
|
||||
position + new Vector3(0.06f, 0.07f, 0f),
|
||||
Cell,
|
||||
radius,
|
||||
sphereHeight: twoSpheres ? 1.835f : 0f,
|
||||
stepUpHeight: 0.04f,
|
||||
stepDownHeight: 0.04f,
|
||||
isOnGround: true,
|
||||
body,
|
||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: 0x01000002u);
|
||||
|
||||
position = result.Position;
|
||||
body.Position = position;
|
||||
ApplyContactResult(body, result);
|
||||
trace.Add(CaptureFrame(result, body));
|
||||
}
|
||||
|
||||
return new WallMaintenanceTrace(trace, supportPasses, placementPasses);
|
||||
}
|
||||
|
||||
private static PhysicsEngine BuildCollisionEngine(
|
||||
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture,
|
||||
bool preparedFlat,
|
||||
|
|
@ -690,6 +810,11 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
int LandedFrame,
|
||||
int LedgeStartFrame);
|
||||
|
||||
private sealed record WallMaintenanceTrace(
|
||||
List<TraceFrame> Frames,
|
||||
int SupportPasses,
|
||||
int PlacementPasses);
|
||||
|
||||
private sealed record TraceFrame(
|
||||
ResolveResult Result,
|
||||
string ResolveBits,
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ public sealed class RetailStepDownPlacementTests
|
|||
public void OrdinaryContactMaintenance_AlwaysRunsFinalPlacement(bool twoSpheres)
|
||||
{
|
||||
Transition transition = MakeGroundedTransition(twoSpheres);
|
||||
const float supportWalkInterp = 0.375f;
|
||||
int supportPasses = 0;
|
||||
int placementPasses = 0;
|
||||
uint placementWalkInterpBits = 0;
|
||||
var engine = new PhysicsEngine
|
||||
{
|
||||
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||
|
|
@ -36,12 +38,15 @@ public sealed class RetailStepDownPlacementTests
|
|||
if (candidate.SpherePath.StepDown)
|
||||
{
|
||||
supportPasses++;
|
||||
candidate.SpherePath.WalkInterp = supportWalkInterp;
|
||||
candidate.CollisionInfo.SetContactPlane(
|
||||
new Plane(Vector3.UnitZ, 0f), Cell);
|
||||
}
|
||||
else if (candidate.SpherePath.InsertType == InsertType.Placement)
|
||||
{
|
||||
placementPasses++;
|
||||
placementWalkInterpBits = BitConverter.SingleToUInt32Bits(
|
||||
candidate.SpherePath.WalkInterp);
|
||||
}
|
||||
|
||||
return actual;
|
||||
|
|
@ -53,10 +58,80 @@ public sealed class RetailStepDownPlacementTests
|
|||
Assert.Equal(TransitionState.OK, result);
|
||||
Assert.Equal(1, supportPasses);
|
||||
Assert.Equal(1, placementPasses);
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(supportWalkInterp),
|
||||
placementWalkInterpBits);
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(supportWalkInterp),
|
||||
BitConverter.SingleToUInt32Bits(transition.SpherePath.WalkInterp));
|
||||
Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType);
|
||||
Assert.False(transition.SpherePath.StepDown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckWalkable_FailedNestedProbe_PreservesOuterBackupForEdgeSlide()
|
||||
{
|
||||
Transition transition = MakeGroundedTransition(twoSpheres: true);
|
||||
var sp = transition.SpherePath;
|
||||
Vector3 outerBackup = new(91.125f, -17.25f, 333.5f);
|
||||
const uint outerBackupCell = 0xA9B40077u;
|
||||
Vector3 nestedOrigin = new(2.125f, 3.25f, 4.5f);
|
||||
|
||||
// Force check_walkables to fail so retail's nested downward probe is
|
||||
// exercised instead of the remembered-support early return.
|
||||
sp.SetCheckPos(nestedOrigin, Cell);
|
||||
sp.SetWalkable(
|
||||
new Plane(Vector3.UnitZ, 0f),
|
||||
[
|
||||
new(100f, 100f, 0f),
|
||||
new(101f, 100f, 0f),
|
||||
new(101f, 101f, 0f),
|
||||
new(100f, 101f, 0f),
|
||||
],
|
||||
Vector3.UnitZ);
|
||||
sp.BackupCheckPos = outerBackup;
|
||||
sp.BackupCheckCellId = outerBackupCell;
|
||||
|
||||
int nestedProbes = 0;
|
||||
var engine = new PhysicsEngine
|
||||
{
|
||||
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||
{
|
||||
if (phase == TransitionCellCollisionPhase.Environment
|
||||
&& candidate.SpherePath.CheckWalkable)
|
||||
{
|
||||
nestedProbes++;
|
||||
}
|
||||
|
||||
return actual;
|
||||
},
|
||||
};
|
||||
|
||||
bool walkable = transition.DoCheckWalkable(PhysicsGlobals.FloorZ, engine);
|
||||
|
||||
Assert.False(walkable);
|
||||
Assert.True(nestedProbes > 0);
|
||||
AssertVectorBits(nestedOrigin, sp.CheckPos);
|
||||
Assert.Equal(Cell, sp.CheckCellId);
|
||||
AssertVectorBits(outerBackup, sp.BackupCheckPos);
|
||||
Assert.Equal(outerBackupCell, sp.BackupCheckCellId);
|
||||
|
||||
// Branch 1 is the first retail edge-slide branch. Its restore must use
|
||||
// the distinctive outer failed candidate, not the nested probe origin.
|
||||
transition.ObjectInfo.State &= ~ObjectInfoState.EdgeSlide;
|
||||
sp.SetCheckPos(new Vector3(-8f, -9f, -10f), Cell);
|
||||
bool stop = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||
engine,
|
||||
stepDownHeight: 0.04f,
|
||||
zVal: PhysicsGlobals.FloorZ,
|
||||
out TransitionState state);
|
||||
|
||||
Assert.True(stop);
|
||||
Assert.Equal(TransitionState.OK, state);
|
||||
AssertVectorBits(outerBackup, sp.CheckPos);
|
||||
Assert.Equal(outerBackupCell, sp.CheckCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportedCandidate_OverlappingDuringPlacement_IsRejected()
|
||||
{
|
||||
|
|
@ -234,6 +309,19 @@ public sealed class RetailStepDownPlacementTests
|
|||
return transition;
|
||||
}
|
||||
|
||||
private static void AssertVectorBits(Vector3 expected, Vector3 actual)
|
||||
{
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(expected.X),
|
||||
BitConverter.SingleToUInt32Bits(actual.X));
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(expected.Y),
|
||||
BitConverter.SingleToUInt32Bits(actual.Y));
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(expected.Z),
|
||||
BitConverter.SingleToUInt32Bits(actual.Z));
|
||||
}
|
||||
|
||||
private static (
|
||||
PhysicsBSPNode Root,
|
||||
Dictionary<ushort, ResolvedPolygon> Resolved) BuildWall()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue