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
|
|
@ -12,9 +12,12 @@ namespace AcDream.Core.Physics;
|
|||
// FUN_00511ec0 set_velocity — store + clamp to MaxVelocity
|
||||
// FUN_00511fa0 set_local_velocity — body→world transform then set_velocity
|
||||
// FUN_00511de0 set_on_walkable — set/clear OnWalkable transient flag
|
||||
// FUN_0050f940 calc_friction — ground-contact friction
|
||||
// FUN_00515020 update_object — per-frame top-level driver
|
||||
//
|
||||
// calc_friction is now cited against the NAMED retail decomp instead of the
|
||||
// older unnamed FUN_0050f940 chunk — see its own doc comment below
|
||||
// (CPhysicsObj::calc_friction, acclient_2013_pseudo_c.txt:276694, 0050ee70).
|
||||
//
|
||||
// Cross-checked against ACE PhysicsObj.cs and PhysicsGlobals.cs.
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -542,48 +545,87 @@ public sealed class PhysicsBody
|
|||
calc_acceleration();
|
||||
}
|
||||
|
||||
// ── FUN_0050f940 ───────────────────────────────────────────────────────
|
||||
// ── CPhysicsObj::calc_friction (0050ee70) ───────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Apply friction deceleration to the velocity when the body is standing
|
||||
/// on a walkable surface.
|
||||
///
|
||||
/// Decompiled logic (FUN_0050f940):
|
||||
/// AP-7 resolved (Campaign P Slice P2, 2026-07-30,
|
||||
/// docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §1).
|
||||
/// The named retail decomp (<c>CPhysicsObj::calc_friction</c>,
|
||||
/// pseudo-C:276694-276822, 0050ee70) independently re-confirms the
|
||||
/// <b>0.25f</b> threshold (derived twice, once per BN-rendered branch) —
|
||||
/// the OLD in-code claim that "the decompile uses 0.0" traced to the
|
||||
/// unnamed, superseded <c>FUN_0050f940</c> Ghidra chunk at a DIFFERENT
|
||||
/// address; per CLAUDE.md the named decomp wins. Cross-checked against
|
||||
/// ACE <c>PhysicsObj.calc_friction</c>
|
||||
/// (references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2120-2141),
|
||||
/// which reads as ONE linear function rather than the BN-rendered "two
|
||||
/// duplicated branches" — the branch split is most likely a BN decompiler
|
||||
/// artifact around a single <c>if (state & SLEDDING_PS)</c> block
|
||||
/// (ACE-derived, Ghidra-verify: see the research doc §7 items 1-2 for the
|
||||
/// still-open BN-artifact-vs-genuine-duplication question; low
|
||||
/// implementation risk either way since ACE's single-linear-function
|
||||
/// reading is adopted regardless).
|
||||
///
|
||||
/// Decompiled logic (retail, ACE-derived shape):
|
||||
/// if NOT OnWalkable → return
|
||||
/// fVar1 = dot(groundNormal, velocity)
|
||||
/// if fVar1 < 0:
|
||||
/// velocity -= fVar1 * groundNormal (remove inward normal component)
|
||||
/// scalar = pow(1 - friction, dt)
|
||||
/// velocity *= scalar
|
||||
/// angle = dot(velocity, contactPlane.N)
|
||||
/// if angle >= 0.25f → return (moving away fast enough — no friction)
|
||||
/// velocity -= angle * contactPlane.N (remove inward normal component, unconditional)
|
||||
/// friction = this->friction (same baseline in every case)
|
||||
/// if Sledding: velocityMag2-banded override (see below)
|
||||
/// velocity *= pow(1 - friction, dt)
|
||||
///
|
||||
/// The threshold (0.0 from _DAT_007c78a0) means any velocity with a
|
||||
/// downward component relative to the normal gets friction applied.
|
||||
/// Positive dot means moving away from the surface — no friction.
|
||||
/// L.3c attempt (2026-04-30, REVERTED): a bare 0.25f bump with no other
|
||||
/// change dropped measured forward locomotion from ~3 m/s to ~0.16 m/s
|
||||
/// in PlayerMovementControllerTests — friction engaged EVERY tick because
|
||||
/// flat-ground walking has dot(velocity, groundNormal) ≈ 0, which is
|
||||
/// < 0.25f. The research pass's math check: DefaultFriction=0.95f (both
|
||||
/// PhysicsBody.cs:120 and ACE PhysicsGlobals.cs:15 agree — not a divergent
|
||||
/// constant), so pow(0.05, dt) at 60 Hz over 1s ≈ 0.951^60 ≈ 4.9% velocity
|
||||
/// remaining — matches the observed hammering almost exactly.
|
||||
///
|
||||
/// Cross-checked with ACE PhysicsObj.calc_friction which uses 0.25f as
|
||||
/// the threshold instead; the decompile uses 0.0. We match the decompile.
|
||||
/// Why this is safe to land now: the L.3c test predates the 2026-07-17
|
||||
/// "local player animation-owned grounded movement" landing (R6).
|
||||
/// PlayerMovementController.cs (~line 1742) zeroes Velocity.X/Y to 0
|
||||
/// immediately before UpdatePhysicsInternal runs whenever animation root
|
||||
/// motion drives the walk (the production graphical local-player path
|
||||
/// since R6) — walking displacement comes from the animation Frame delta
|
||||
/// applied directly to Position, not from integrating Velocity. Friction
|
||||
/// decaying an already-zero horizontal Velocity is a no-op, so the L.3c
|
||||
/// mechanism does not reproduce on that path. The `else` branch (no
|
||||
/// animation root motion — headless/test-controller movers using
|
||||
/// `get_state_velocity`, and remote/NPC movers) DOES still feed real XY
|
||||
/// speed into Velocity and remains exposed; see
|
||||
/// GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests for
|
||||
/// the regression pin on the root-motion path specifically.
|
||||
///
|
||||
/// L.3c attempt (2026-04-30, REVERTED): tried bumping to 0.25f per
|
||||
/// retail acclient_2013_pseudo_c.txt:276705. Build green but
|
||||
/// PlayerMovementControllerTests showed forward locomotion dropping
|
||||
/// from ~3m/s to ~0.16m/s — friction now hammers normal walking.
|
||||
/// Retail's friction block is gated by an additional state check at
|
||||
/// line 276702 (`(this->state & ...) == 0`) that we didn't decode
|
||||
/// fully; locomotion is probably skipped from the friction path
|
||||
/// while actively walking. Filed as L.3c-followup; keeping the
|
||||
/// matching-the-decompile-as-read 0.0 threshold for now.
|
||||
/// ⚠️ Constant discrepancy (Ghidra-verify, NOT resolved this pass): the
|
||||
/// raw decomp's Sledding slope-flatness test literally computes
|
||||
/// __fcos(0.17453292519943295) (= cos(10°) ≈ 0.984808) and compares
|
||||
/// against contact_plane.N.z; ACE's port instead compares
|
||||
/// ContactPlane.Normal.Z > 0.99999536f directly (≈0.175° from flat, NOT
|
||||
/// 10°) — physically very different tests. Neither hypothesis (a BN
|
||||
/// misdecompile of a raw float load as __fcos, vs. an independent ACE
|
||||
/// port error) is confirmed; a live Ghidra decompile of 0050ee70 settles
|
||||
/// it. 0.99999536f is kept provisionally (least churn — it's what
|
||||
/// acdream's own prior dead code already had); do not silently resolve
|
||||
/// this without the Ghidra check. See register row AD-55.
|
||||
/// </summary>
|
||||
public void calc_friction(float dt, float velocityMag2)
|
||||
{
|
||||
if ((TransientState & TransientStateFlags.OnWalkable) == 0)
|
||||
return;
|
||||
|
||||
float dot = Vector3.Dot(GroundNormal, Velocity);
|
||||
if (dot >= 0f)
|
||||
float angle = Vector3.Dot(Velocity, GroundNormal);
|
||||
if (angle >= 0.25f)
|
||||
return;
|
||||
|
||||
// Remove the component of velocity that presses into the ground normal.
|
||||
Velocity -= dot * GroundNormal;
|
||||
// Unconditional past the threshold check — no separate inner guard.
|
||||
Velocity -= angle * GroundNormal;
|
||||
|
||||
float friction = Friction;
|
||||
|
||||
|
|
@ -592,7 +634,7 @@ public sealed class PhysicsBody
|
|||
{
|
||||
if (velocityMag2 < 1.5625f) // 1.25² — slow sled
|
||||
friction = 1.0f;
|
||||
else if (velocityMag2 >= 6.25f && GroundNormal.Z > 0.99999536f) // near-flat
|
||||
else if (velocityMag2 >= 6.25f && GroundNormal.Z > 0.99999536f) // near-flat, Ghidra-verify (see doc comment)
|
||||
friction = 0.2f;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue