Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Measured live at Rithwic 2026-08-06 with ACDREAM_DUMP_EDGE_SLIDE=1. Six branch2/steep-cliffslide events, every one reporting curN=(-0.954,0.000,0.301) lastN=(-0.954,0.000,0.301) angle=0.0000 apply=False, outcome degenerate-cross/last-known. That is decision-table row 1 of the research doc, verbatim. CTransition::cliff_slide @0x0050a6d0 takes its slide direction from cross(steep contact normal, last_known_contact_plane.N) — it needs the surface the mover was STANDING ON as the second vector. acdream's CollisionInfo.SetContactPlane latched the last-known group on every call, so by the time cliff_slide ran, last-known had already been overwritten with the steep face itself: the cross product of a vector with itself, which is zero. Degenerate direction, no slide, walk off the cliff. Retail's COLLISIONINFO::set_contact_plane @0x00509d80 is 22 bytes and writes the CONTACT group only; the last-known group has four writers, none of them that function. So the four writes are DELETED and a new InitContactPlane mirrors CTransition::init_contact_plane @0x0050e850, writing both — the start-of-transition seed, where there is no earlier surface to remember. Only check_contact's SUCCESS branch calls it. The other eleven call sites keep the narrowed setter. This is a port, not a suppression: no guard, no grace period, no flag. The user's own A/B was the discriminator: Neftet's block plateaus hold (188 branch3/precipice-slide events, all before the teleport) while Rithwic's terrain cliff fails (6 branch2 events, all after). I had predicted the opposite — that terrain would be the flat-normal case — and position plus timeline corrected me, not reasoning. NEW DISCRIMINATING TEST, because the suite had none. It was green both before and after the production change, so nothing in it defended this behaviour. Issue32LastKnownContactPlaneTests seeds a walkable plane, asserts a steep mid-transition contact leaves it intact, and asserts the resulting cross product is non-degenerate. Sabotage-verified: restore the four writes and both discriminating rows fail while the InitContactPlane control keeps passing — the pair separates 'the latch is gone' from 'nothing writes last-known at all'. Two existing tests corrected rather than deleted. PhysicsSetPositionTests.FailedCheck_MapsCollisionHandlerResultToRetailError passed BECAUSE of the latch (the file the research named); its hook now populates both groups explicitly, since it asserts report plumbing, not setter semantics. RetailEdgeResponseOrderingTests.TransitionalInsert_ DegenerateCliffSlideOk_ContinuesOuterRetry was predicted to fail and did not — it now passes for a DIFFERENT reason (last-known absent rather than clobbered, which retail also answers with OK_TS). Its comment described the deleted behaviour and is corrected to say so, and to say it does not discriminate this fix. Also repairs the #338 probe. Its first placement in PlayerMovementController printed nothing across 11,523 live log lines — the wrong one of two resolve call sites — so it moves to PhysicsEngine.ResolveWithTransition where every caller passes through, filtered to the player. The dead site is removed rather than left in place; a probe that never fires is worse than none. The flag test now precedes the interpolated string: building it eagerly cost 128 B per resolve with the probe OFF, which Slice I1's zero-allocation gate caught. Suite 11,234 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
140 lines
5.6 KiB
C#
140 lines
5.6 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
/// <summary>
|
|
/// #32 regression — the last-known contact plane must survive a steep
|
|
/// mid-transition contact.
|
|
///
|
|
/// <para>
|
|
/// <c>CTransition::cliff_slide</c> @0x0050a6d0 takes its slide direction from
|
|
/// <c>cross(steep contact normal, last_known_contact_plane.N)</c>. It needs
|
|
/// the surface the mover was STANDING ON as the second vector. acdream's
|
|
/// <c>CollisionInfo.SetContactPlane</c> used to latch the last-known group on
|
|
/// every call, so by the time <c>cliff_slide</c> ran, last-known had already
|
|
/// been overwritten with the steep face itself — the cross product of a vector
|
|
/// with itself, which is zero. Degenerate direction, no slide applied, and the
|
|
/// player walked off the cliff.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Retail's <c>COLLISIONINFO::set_contact_plane</c> @0x00509d80 is 22 bytes
|
|
/// and writes the CONTACT group only. The last-known group has four writers,
|
|
/// none of them that function.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <b>Measured live at Rithwic on 2026-08-06</b> with
|
|
/// <c>ACDREAM_DUMP_EDGE_SLIDE=1</c>: six <c>branch2/steep-cliffslide</c>
|
|
/// events, every one reporting
|
|
/// <c>curN=(-0.954,0.000,0.301) lastN=(-0.954,0.000,0.301) angle=0.0000
|
|
/// apply=False</c> with outcome <c>degenerate-cross/last-known</c> — the two
|
|
/// vectors identical to three decimals. That capture is decision-table row 1
|
|
/// of <c>docs/research/2026-08-06-32-local-edge-slide-research.md</c>, and the
|
|
/// normals below are taken from it verbatim rather than invented.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <b>Sabotage check.</b> Restore the four last-known writes at the tail of
|
|
/// <c>SetContactPlane</c> and
|
|
/// <see cref="MidTransitionSteepContact_LeavesTheWalkableLastKnownPlaneIntact"/>
|
|
/// fails on its <c>lastN</c> assertion while
|
|
/// <see cref="InitContactPlane_SeedsBothGroups"/> keeps passing — the pair
|
|
/// separates "the latch is gone" from "nothing writes last-known at all",
|
|
/// which a single test could not do.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class Issue32LastKnownContactPlaneTests
|
|
{
|
|
private const uint Cell = 0x0001_0001u;
|
|
|
|
/// <summary>The flat ground the mover is standing on before the rim.</summary>
|
|
private static readonly Plane Walkable = new(Vector3.UnitZ, 0f);
|
|
|
|
/// <summary>
|
|
/// The Rithwic cliff face, verbatim from the live capture: a normal whose
|
|
/// Z of 0.301 is about 72.5° from horizontal, far below
|
|
/// <see cref="PhysicsGlobals.FloorZ"/>, so it is rejected as walkable and
|
|
/// routes to the cliff-slide branch.
|
|
/// </summary>
|
|
private static readonly Plane Steep =
|
|
new(Vector3.Normalize(new Vector3(-0.954f, 0f, 0.301f)), 0f);
|
|
|
|
[Fact]
|
|
public void MidTransitionSteepContact_LeavesTheWalkableLastKnownPlaneIntact()
|
|
{
|
|
var ci = new CollisionInfo();
|
|
|
|
// Start of transition: standing on flat ground. Retail's
|
|
// init_contact_plane seeds BOTH groups here, because there is no
|
|
// earlier surface to remember.
|
|
ci.InitContactPlane(Walkable, Cell);
|
|
|
|
// Mid-transition: the sweep meets the cliff face. Retail's
|
|
// set_contact_plane writes the contact group and nothing else.
|
|
ci.SetContactPlane(Steep, Cell);
|
|
|
|
Assert.True(ci.ContactPlaneValid);
|
|
Assert.Equal(Steep.Normal, ci.ContactPlane.Normal);
|
|
|
|
Assert.True(
|
|
ci.LastKnownContactPlaneValid,
|
|
"the seeded last-known plane must survive a steep contact — "
|
|
+ "cliff_slide has no second vector without it.");
|
|
Assert.Equal(Walkable.Normal, ci.LastKnownContactPlane.Normal);
|
|
|
|
// The consequence that actually mattered in play: a non-degenerate
|
|
// slide direction. With the latch in place both vectors were the steep
|
|
// face and this cross product was the zero vector.
|
|
Vector3 slide = Vector3.Cross(Steep.Normal, ci.LastKnownContactPlane.Normal);
|
|
Assert.True(
|
|
slide.Length() > 1e-4f,
|
|
$"cliff_slide's direction is degenerate: |cross| = {slide.Length():E3}. "
|
|
+ $"contactN = {Steep.Normal}, lastN = {ci.LastKnownContactPlane.Normal}.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// CONTROL. The seed path must still write both groups, or the test above
|
|
/// would pass simply because nothing ever populates last-known.
|
|
/// </summary>
|
|
[Fact]
|
|
public void InitContactPlane_SeedsBothGroups()
|
|
{
|
|
var ci = new CollisionInfo();
|
|
|
|
Assert.False(ci.LastKnownContactPlaneValid);
|
|
|
|
ci.InitContactPlane(Walkable, Cell, isWater: true);
|
|
|
|
Assert.True(ci.ContactPlaneValid);
|
|
Assert.Equal(Walkable.Normal, ci.ContactPlane.Normal);
|
|
Assert.Equal(Cell, ci.ContactPlaneCellId);
|
|
Assert.True(ci.ContactPlaneIsWater);
|
|
|
|
Assert.True(ci.LastKnownContactPlaneValid);
|
|
Assert.Equal(Walkable.Normal, ci.LastKnownContactPlane.Normal);
|
|
Assert.Equal(Cell, ci.LastKnownContactPlaneCellId);
|
|
Assert.True(ci.LastKnownContactPlaneIsWater);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A second steep contact must not resurrect the defect through the
|
|
/// no-op-if-unchanged guard: the guard returns early when the CONTACT
|
|
/// group already matches, and an early return must not be the only thing
|
|
/// protecting last-known.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RepeatedSteepContact_StillLeavesLastKnownIntact()
|
|
{
|
|
var ci = new CollisionInfo();
|
|
ci.InitContactPlane(Walkable, Cell);
|
|
|
|
ci.SetContactPlane(Steep, Cell);
|
|
ci.SetContactPlane(Steep, Cell); // hits the no-op guard
|
|
ci.SetContactPlane(Steep, Cell);
|
|
|
|
Assert.Equal(Walkable.Normal, ci.LastKnownContactPlane.Normal);
|
|
}
|
|
}
|