fix(physics): AD-25 - remote collision response through ported HandleAllCollisions
Campaign P Slice P3 item 2. CPhysicsObj::handle_all_collisions (0x00514780, pc:282647) is one uniform function retail calls unconditionally after every SetPositionInternal, player or remote. The gate is shouldReflect = !(prevOnWalkable && nowOnWalkable && !sledding). RuntimeRemotePhysicsUpdater.Tick's post-resolve reflect was still the 2026-07-05 (#173) hand-inlined block, gated on resolveResult.CollisionNormalValid and using two ad-hoc branches that diverge from retail in exactly the cases the register row named: - non-sledding: old = "!prevOnWalkable && !nowOnWalkable" (reflects ONLY airborne-before-AND-after); retail reflects on every transition except grounded-before-AND-after. - sledding: old = "!(prevOnWalkable && nowOnWalkable)" (suppresses the bounce exactly when both grounded); retail's "!sledding" term forces shouldReflect = true unconditionally when sledding, the opposite polarity. Both gaps meant a remote's post-landing reflect never ran on a grounded-transition tick at all -- the "acdream lands clean and dead" half of #166's slope-landing composite. Replace the hand-inlined block with a direct call to PhysicsObjUpdate.HandleAllCollisions -- the same verbatim port the local player and every ordinary body already use via CommitSetPositionTransition -- passing the same prevContact/prevOnWalkable/nowOnWalkable values the old code already computed. Narrower swap per the research's explicit recommendation: does not fold in CommitSetPositionTransition's HitGround/LeaveGround dispatch, leaving the remote's bespoke landing-detection block (interp-queue-clear, animation-hook-specific logic) untouched. The call is now unconditional (matching retail's own unconditional call site) rather than gated behind CollisionNormalValid, since HandleAllCollisions already no-ops the reflect step internally when no normal was found but still runs the frames-stationary-fall bleed regardless. PhysicsObjUpdate.HandleAllCollisionsTests already exhaustively pins the retail formula in isolation; this change is a mechanical wiring swap to the already-tested function using values the removed block already computed. Full regression suites (Core.Tests 3991/2 skip, Runtime.Tests 425/0, App.Tests 3968/3 skip) pass unchanged -- no existing test pinned the old broken formula. Register: AD-25 retired (both the local-player and remote halves are now the ported HandleAllCollisions); #166's reattribution note updated to reflect the closure, leaving only TS-4 as the remaining blocker on that issue's downhill-jump-glide acceptance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
dae5b1ea68
commit
8b5425498c
3 changed files with 65 additions and 67 deletions
|
|
@ -334,20 +334,25 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
// setup.Height) × ObjScale — the creature's own dat Setup
|
||||
// scaled by its wire ObjScale, the same source the local
|
||||
// player + moveto/sticky use, and consistent with the
|
||||
// spawn-time shadow registration's entScale. Retail seeds
|
||||
// the transition from the object's own Setup sphere list ×
|
||||
// m_scale (CPhysicsObj::transition 0x00512dc0 → init_sphere;
|
||||
// ObjScale from set_description 0x00514f40). This narrows
|
||||
// TS-46 (remotes no longer use human dims); the two-scalar
|
||||
// API is still a lossy stand-in for retail's full (≤2)
|
||||
// sphere list, and stepUp/stepDown stay 0.4 (retail derives
|
||||
// those from the Setup too — an adjacent divergence left as-is).
|
||||
// spawn-time shadow registration's entScale. TS-46 (2026-07-30)
|
||||
// closed the remaining residual: sphereList/sphereScale below
|
||||
// now carry the Setup's own verbatim sphere list (falling back
|
||||
// to this deR/deH reconstruction only when the Setup has no
|
||||
// sphere rows), and stepUpHeight/stepDownHeight are
|
||||
// Setup-derived rather than a hardcoded 0.4f.
|
||||
// Fallback to the human capsule for a shapeless / unresolvable
|
||||
// Setup (GetSetupCylinder returns (0,0)); a zero radius would
|
||||
// degenerate the sweep.
|
||||
float deR = radius;
|
||||
float deH = height;
|
||||
if (deR < 0.05f) { deR = 0.48f; deH = 1.835f; }
|
||||
// AD-25 (2026-07-30): retail handle_all_collisions (pc:282647)
|
||||
// reads the mover's own transient_state CONTACT_TS/
|
||||
// ON_WALKABLE_TS bits BEFORE this SetPositionInternal-equivalent
|
||||
// resolve — capture them here, matching TickHidden's identical
|
||||
// pre-resolve capture below.
|
||||
bool previousContact = rm.Body.InContact;
|
||||
bool previousOnWalkable = rm.Body.OnWalkable;
|
||||
var resolveResult = _physics.Engine.ResolveWithTransition(
|
||||
preIntegratePos, postIntegratePos, rm.CellId,
|
||||
sphereRadius: deR,
|
||||
|
|
@ -425,54 +430,35 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
// to actually-moving remotes — the perf risk the review flagged for
|
||||
// a packed town. (In-place shadow-move + cell-relink-on-change is a
|
||||
// further optimization if profiling still shows churn.)
|
||||
// #173 (2026-07-05): retail CPhysicsObj::handle_all_collisions
|
||||
// (pc:282699-282715) runs after EVERY SetPositionInternal —
|
||||
// remote objects included; a VectorUpdate-launched jump arc
|
||||
// is ordinary object physics in retail. acdream ported the
|
||||
// velocity reflection for the LOCAL player only (L.3a,
|
||||
// PlayerMovementController ~:940), so a remote jumping into
|
||||
// a dungeon ceiling had its POSITION pinned by the sweep
|
||||
// while its +Z velocity kept integrating — the char hovered
|
||||
// at the roof until gravity burned the arc off, landing
|
||||
// late (user report, 0x0007 dungeon). Mirror the local
|
||||
// site exactly:
|
||||
// v_new = v − (1 + elasticity)·dot(v, n)·n
|
||||
// with the AD-25 suppression (bounce only when airborne
|
||||
// before AND after — corridor slides and landings don't
|
||||
// reflect; the landing snap below keeps its
|
||||
// `Velocity.Z <= 0` gate intact). Inelastic movers
|
||||
// (missiles, later) zero out instead.
|
||||
if (resolveResult.CollisionNormalValid)
|
||||
{
|
||||
bool prevOnWalkable = rm.Body.OnWalkable;
|
||||
bool nowOnWalkable = resolveResult.IsOnGround;
|
||||
bool applyBounce = rm.Body.State.HasFlag(
|
||||
AcDream.Core.Physics.PhysicsStateFlags.Sledding)
|
||||
? !(prevOnWalkable && nowOnWalkable)
|
||||
: (!prevOnWalkable && !nowOnWalkable);
|
||||
if (applyBounce)
|
||||
{
|
||||
if (rm.Body.State.HasFlag(
|
||||
AcDream.Core.Physics.PhysicsStateFlags.Inelastic))
|
||||
{
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
var vRem = rm.Body.Velocity;
|
||||
var nRem = resolveResult.CollisionNormal;
|
||||
float dotVN = System.Numerics.Vector3.Dot(vRem, nRem);
|
||||
if (dotVN < 0f)
|
||||
{
|
||||
rm.Body.Velocity =
|
||||
vRem + nRem * (-(dotVN * (rm.Body.Elasticity + 1f)));
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
Console.WriteLine(
|
||||
$"VU.bounce guid=0x{serverGuid:X8} n=({nRem.X:F2},{nRem.Y:F2},{nRem.Z:F2}) vZ {vRem.Z:F2}->{rm.Body.Velocity.Z:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// AD-25 (2026-07-30): retail CPhysicsObj::handle_all_collisions
|
||||
// (0x00514780, pc:282647) runs UNCONDITIONALLY after EVERY
|
||||
// SetPositionInternal — remote objects included; a
|
||||
// VectorUpdate-launched jump arc is ordinary object physics in
|
||||
// retail. #173 (2026-07-05) first mirrored the local player's
|
||||
// reflect math here by hand, but with a narrower gate than
|
||||
// retail's: `shouldReflect = !(prevOnWalkable && nowOnWalkable
|
||||
// && !sledding)` collapses to the two ad-hoc branches this
|
||||
// block used to hand-roll, and got BOTH wrong — the sledding
|
||||
// branch suppressed the bounce exactly when retail's
|
||||
// `!sledding` term forces it UNCONDITIONALLY, and the
|
||||
// non-sledding branch only reflected airborne→airborne where
|
||||
// retail reflects on every transition except grounded→grounded.
|
||||
// PhysicsObjUpdate.HandleAllCollisions is the same verbatim
|
||||
// port the local player and every ordinary body already use
|
||||
// (PhysicsObjUpdate.CommitSetPositionTransition); call it
|
||||
// directly instead of re-deriving the gate. It already
|
||||
// no-ops the reflect step when collisionNormalValid is false,
|
||||
// but — unlike the old wrapper this replaces — still runs the
|
||||
// fsf>1 unconditional velocity-zero "bleed" regardless of
|
||||
// whether this tick found a collision normal, matching
|
||||
// retail's own unconditional call site.
|
||||
AcDream.Core.Physics.PhysicsObjUpdate.HandleAllCollisions(
|
||||
rm.Body,
|
||||
resolveResult.CollisionNormalValid,
|
||||
resolveResult.CollisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable,
|
||||
resolveResult.IsOnGround);
|
||||
|
||||
// K-fix15 (2026-04-26): post-resolve landing
|
||||
// detection for airborne remotes. Mirrors
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue