fix(physics): AP-7 - port calc_friction's retail 0.25f threshold; retire AP-7, file AD-55
Campaign P Slice P2 step 3 (docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1, §6 Step 5). The named retail decomp (CPhysicsObj::calc_friction, pseudo-C:276694-276822, 0050ee70) independently re-confirms the 0.25f threshold (derived twice, once per BN-rendered branch); the in-code claim that "the decompile uses 0.0" traced to the older, unnamed FUN_0050f940 Ghidra chunk at a different address -- per CLAUDE.md the named decomp wins. calc_friction now reads angle = dot(Velocity, GroundNormal); if (angle >= 0.25f) return; then unconditionally removes the normal-aligned velocity component, then applies the existing (already-present but previously unreachable) PhysicsState.Sledding-gated friction overrides. The BN-rendered "two duplicated branches" around the state check is adopted as a single linear function matching ACE's PhysicsObj.calc_friction shape -- the branch split is most likely a BN decompiler artifact around one `if (state & SLEDDING_PS)` block (ACE-derived, Ghidra-verify; low implementation risk either way since ACE's reading is adopted regardless). Why this doesn't repeat the reverted 2026-04-30 L.3c regression (naive 0.0 -> 0.25f bump dropped forward locomotion 3 -> 0.16 m/s): that test predates the 2026-07-17 R6 "local player animation-owned grounded movement" landing. PlayerMovementController (Runtime/Gameplay, out of this slice's scope) zeroes Velocity.X/Y to exactly zero every tick before calc_friction runs whenever animation root motion drives the walk, so friction has nothing horizontal left to hammer on the production graphical local-player path. Pinned at the PhysicsBody level (the only file this slice may touch) by GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests. The headless/get_state_velocity path and remote/NPC movers still feed real velocity into this function and remain the ones to watch if a similar regression resurfaces there -- flagged in the retired AP-7 row for future sessions working in Runtime/Gameplay. Left an open, explicitly-flagged discrepancy: the raw decomp's Sledding slope-flatness test computes cos(10 deg) (~0.984808) while ACE's port (and acdream's prior dead code) compares GroundNormal.Z > 0.99999536f (~0.175 deg from flat) -- physically different tests, neither confirmed this pass (Ghidra MCP down). Kept 0.99999536f provisionally (least churn) and filed AD-55 for just that constant rather than silently picking one. Register: AP-7 retired with a corrected citation; AD-55 filed for the cos(10 deg) question. Core.Tests: 3916 passed, 2 skipped (both pre-existing and unrelated), 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
325fee7cbb
commit
4f7e29f7cf
3 changed files with 174 additions and 30 deletions
|
|
@ -8,7 +8,8 @@ namespace AcDream.Core.Tests.Physics;
|
|||
/// <summary>
|
||||
/// Unit tests for PhysicsBody — the C# port of CPhysicsObj's core simulation
|
||||
/// from acclient.exe (FUN_005111d0, FUN_00511420, FUN_00511ec0, FUN_00511fa0,
|
||||
/// FUN_00511de0, FUN_0050f940, FUN_00515020).
|
||||
/// FUN_00511de0, FUN_00515020, and the named
|
||||
/// <c>CPhysicsObj::calc_friction</c> at 0050ee70).
|
||||
/// </summary>
|
||||
public sealed class PhysicsBodyTests
|
||||
{
|
||||
|
|
@ -454,6 +455,106 @@ public sealed class PhysicsBodyTests
|
|||
Assert.Equal(1f, body.Velocity.X, precision: 4);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// AP-7 (Campaign P Slice P2, 2026-07-30): calc_friction's 0.25f threshold
|
||||
// docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void calc_friction_dot_between_zero_and_quarter_now_engages_friction()
|
||||
{
|
||||
// dot(velocity, groundNormal) = 0.1 — ABOVE the old 0.0 threshold
|
||||
// (no friction pre-fix) but BELOW the new retail 0.25f threshold
|
||||
// (friction now engages). This is exactly the window the 0.0 -> 0.25f
|
||||
// port changes; pinning it here documents the intentional behavior
|
||||
// change the AP-7 register row used to warn about.
|
||||
var body = MakeGrounded();
|
||||
body.GroundNormal = Vector3.UnitZ;
|
||||
body.Friction = 0.95f;
|
||||
body.Velocity = new Vector3(5f, 0f, 0.1f);
|
||||
float mag2 = body.Velocity.LengthSquared();
|
||||
|
||||
body.calc_friction(1f / 60f, mag2);
|
||||
|
||||
Assert.True(body.Velocity.Length() < 5f,
|
||||
"Retail's 0.25f threshold means dot=0.1 (below 0.25) engages friction, " +
|
||||
"unlike the old 0.0 threshold which would have returned early here.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void calc_friction_dot_at_quarter_threshold_returns_early_no_change()
|
||||
{
|
||||
// dot(velocity, groundNormal) = 0.25 exactly -> angle >= 0.25f is true
|
||||
// -> early return, matching ACE's `if (angle >= 0.25f) return;`.
|
||||
var body = MakeGrounded();
|
||||
body.GroundNormal = Vector3.UnitZ;
|
||||
body.Velocity = new Vector3(5f, 0f, 0.25f);
|
||||
var before = body.Velocity;
|
||||
float mag2 = body.Velocity.LengthSquared();
|
||||
|
||||
body.calc_friction(1f / 60f, mag2);
|
||||
|
||||
Assert.Equal(before, body.Velocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests()
|
||||
{
|
||||
// Campaign P Slice P2 research finding: the reverted 2026-04-30 L.3c
|
||||
// regression (forward locomotion 3 -> 0.16 m/s) cannot reproduce on
|
||||
// the production graphical local-player path post-R6, because
|
||||
// PlayerMovementController zeroes Velocity.X/Y to exactly zero every
|
||||
// tick BEFORE UpdatePhysicsInternal/calc_friction runs whenever
|
||||
// animation root motion drives the walk (walking displacement comes
|
||||
// from the animation Frame delta applied directly to Position, not
|
||||
// from integrating Velocity). This test pins that specific state at
|
||||
// the PhysicsBody level (the only file this slice may change):
|
||||
// Velocity.XY == 0 on flat ground is IDENTICAL after calc_friction
|
||||
// whether the threshold is the old 0.0 or the new retail 0.25 --
|
||||
// friction has nothing to hammer because there is no horizontal
|
||||
// velocity for it to act on. Only the residual vertical (gravity)
|
||||
// component may be affected by the normal-removal step, exactly as
|
||||
// retail's own contact handling expects.
|
||||
var body = MakeGrounded();
|
||||
body.GroundNormal = Vector3.UnitZ;
|
||||
body.Friction = 0.95f;
|
||||
// Root-motion path's exact per-tick shape: horizontal zeroed, only
|
||||
// the world Z survives (a small residual downward settle velocity).
|
||||
body.Velocity = new Vector3(0f, 0f, -0.05f);
|
||||
|
||||
body.calc_friction(1f / 60f, body.Velocity.LengthSquared());
|
||||
|
||||
Assert.Equal(0f, body.Velocity.X, precision: 5);
|
||||
Assert.Equal(0f, body.Velocity.Y, precision: 5);
|
||||
Assert.True(MathF.Abs(body.Velocity.Z) < 0.05f,
|
||||
$"Root-motion horizontal speed must stay exactly at full (zero) " +
|
||||
$"speed under the new threshold; got Velocity={body.Velocity}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void calc_friction_sledding_state_gate_reachable_with_new_threshold()
|
||||
{
|
||||
// The Sledding-gated overrides (1.5625/6.25/near-flat) were already
|
||||
// present but structurally unreachable in production (nothing sets
|
||||
// PhysicsStateFlags.Sledding, see #166 research §3) -- this test only
|
||||
// confirms the branch still behaves once the outer 0.25f gate is
|
||||
// passed, so a future data-authored Sledding toggle lands on tested
|
||||
// code.
|
||||
var body = MakeGrounded();
|
||||
body.GroundNormal = Vector3.UnitZ;
|
||||
body.State |= PhysicsStateFlags.Sledding;
|
||||
body.Velocity = new Vector3(3f, 0f, -0.5f); // velocityMag2 = 9.25, >= 6.25
|
||||
float mag2 = body.Velocity.LengthSquared();
|
||||
|
||||
body.calc_friction(1f / 60f, mag2);
|
||||
|
||||
// friction should be 0.2f (near-flat, fast sled) rather than the
|
||||
// default 0.95f -- less decay, so speed should stay closer to 3.
|
||||
Assert.True(body.Velocity.Length() > 2.9f,
|
||||
$"Fast near-flat sledding should use the light 0.2f friction override; " +
|
||||
$"got speed {body.Velocity.Length()}");
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// update_object — per-frame driver
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue