merge: Campaign P Slice P2 response-layer (TS-1 resolved, AP-7 ported, TS-4 stopped at escape valve)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> # Conflicts: # docs/architecture/retail-divergence-register.md
This commit is contained in:
commit
26e0334af3
9 changed files with 746 additions and 63 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1985,6 +1985,23 @@ public sealed class Transition
|
|||
// PrecipiceSlide. CliffSlide deflects motion along the ridge
|
||||
// between current-steep and last-known-walkable; gravity then
|
||||
// produces visible downhill drift.
|
||||
//
|
||||
// TS-1 gap #3 (register AD-54, Campaign P Slice P2 2026-07-30):
|
||||
// retail's raw SPHEREPATH::edge_slide has NO steepness branch here
|
||||
// — `if (walkable != null) { ... precipice_slide(...) }` unconditionally
|
||||
// (acclient_2013_pseudo_c.txt:364-370 per the P2 research quote). The
|
||||
// LandingZ permissive acceptance itself IS retail-faithful — confirmed
|
||||
// by TransitionalInsert's own Path-4 Collide branch
|
||||
// (TransitionTypes.cs, `DoCheckWalkable(PhysicsGlobals.LandingZ, engine)`
|
||||
// above) and TS-4's BSPTREE::find_collisions read
|
||||
// (pc:323740-323783: `sphere_path.walkable_allowance = LandingZ`
|
||||
// unconditionally, no slope test) — so a steep roof really is
|
||||
// "walkable" in retail too. What is NOT independently verified from
|
||||
// the raw decomp is whether retail's OUTER caller (transitional_insert)
|
||||
// absorbs a same-polygon-standing Collided from precipice_slide via
|
||||
// its own retry loop rather than needing this reroute; see
|
||||
// docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §2
|
||||
// gap #3.
|
||||
if (sp.WalkablePlane.Normal.Z < PhysicsGlobals.FloorZ)
|
||||
{
|
||||
var cliffPlane = sp.WalkablePlane;
|
||||
|
|
@ -2015,6 +2032,32 @@ public sealed class Transition
|
|||
// Retail back-probes from the current sphere center to rediscover the
|
||||
// walkable polygon we just left, then restores the failed candidate and
|
||||
// runs precipice_slide against that polygon.
|
||||
//
|
||||
// TS-1 gap #1 research (Campaign P Slice P2, 2026-07-30,
|
||||
// docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §2):
|
||||
// retail's SPHEREPATH::edge_slide back-probe branch re-caches
|
||||
// walkable_check_pos/localspace_sphere here — SPHEREPATH::get_walkable_pos
|
||||
// (0050a8f0) -> SPHEREPATH::cache_localspace_sphere (0050c9d0) ->
|
||||
// SPHEREPATH::set_walkable_check_pos (00509ce0), acclient_2013_pseudo_c.txt
|
||||
// :274318-274326 (0050b4e0-0050b507) — before its second precipice_slide
|
||||
// call. That machinery re-projects the restored check_pos sphere into the
|
||||
// walkable polygon's OWN per-cell local frame via Position::localtolocal,
|
||||
// and precipice_slide itself then applies a LandDefs::get_block_offset
|
||||
// landblock correction (pc:274341) before testing find_crossed_edge.
|
||||
// Deliberately NOT ported here: acdream's SpherePath.WalkableVertices/
|
||||
// WalkablePlane are populated in UNIFIED WORLD SPACE at assignment time
|
||||
// (SetWalkable/SetWalkableTransformed above bake worldOrigin+scale in
|
||||
// immediately), and GlobalSphere is likewise always world-space
|
||||
// (SetCheckPos/RestoreCheckPos). Both operands BSPQuery.FindCrossedEdge
|
||||
// compares are therefore already commensurable with no landblock/cell
|
||||
// reprojection needed — retail's local-frame recache is a no-op
|
||||
// correction under this architecture, and FindCrossedEdge never reads a
|
||||
// sphere radius, so retail's walkable_scale radius correction has no
|
||||
// acdream counterpart either. Pinned by
|
||||
// EdgeSlideBackProbePrecipiceSlideTests (direct SpherePath.PrecipiceSlide
|
||||
// state test): a walkable polygon rediscovered near GlobalCurrCenter,
|
||||
// tested against GlobalSphere[0] restored to the original failed target,
|
||||
// crosses the edge and slides — it does not wedge into Collided.
|
||||
Vector3 backToCurrent = sp.GlobalCurrCenter[0].Origin - sp.GlobalSphere[0].Origin;
|
||||
sp.AddOffsetToCheckPos(backToCurrent);
|
||||
|
||||
|
|
@ -2048,6 +2091,25 @@ public sealed class Transition
|
|||
// deflection. Using LastWalkable preserves the prior flat-ground
|
||||
// plane across continuous-slope frames; world-up gives a guaranteed
|
||||
// non-zero deflection when no walkable history exists at all.
|
||||
//
|
||||
// TS-1 gap #2 (register AD-53, Campaign P Slice P2 2026-07-30): retail's
|
||||
// raw CTransition::cliff_slide (pc:272397, 0050a6d0) uses
|
||||
// this->collision_info.last_known_contact_plane.N DIRECTLY as the second
|
||||
// cross-product operand — no fallback chain. Confirmed by a fresh read of
|
||||
// last_known_contact_plane's own maintenance
|
||||
// (acclient_2013_pseudo_c.txt:272659-272668, pc ~0050ad07): retail
|
||||
// overwrites last_known_contact_plane from contact_plane UNCONDITIONALLY
|
||||
// on every validate_transition pass, the same "gets overwritten by
|
||||
// whatever's current, including a steep plane" behavior this file's
|
||||
// ContactPlane/LastKnownContactPlane tracking already has — retail does
|
||||
// NOT maintain a separately-preserved flat-ground history there either.
|
||||
// This three-source chain (LastWalkablePlane -> LastKnownContactPlane ->
|
||||
// UnitZ) is therefore a genuine acdream invention, not a retail-matching
|
||||
// read — kept because it compensates for AP-4's incomplete OnWalkable
|
||||
// bookkeeping (see DO-NOT-RETRY item 9 in
|
||||
// docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §0)
|
||||
// and removing it reintroduces the degenerate-cross "stay on the roof"
|
||||
// wedge the L.4 session (2026-04-30) fought. See that doc's §2 gap #2.
|
||||
Vector3 referenceNormal;
|
||||
string refSource;
|
||||
if (sp.HasLastWalkablePolygon && sp.LastWalkablePlane.Normal.Z >= PhysicsGlobals.FloorZ)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue