fix(physics): TS-46 - seed the sweep from the Setup's own sphere list
Campaign P Slice P3 item 1. Retail CPhysicsObj::transition (0x00512dc0) seeds the collision sweep from CPartArray::GetSphere (the Setup's own <=2-sphere list, each origin+radius scaled by m_scale) via SPHEREPATH::init_sphere (0x0050c670) -- not from a symmetric two-scalar (radius, height) capsule reconstruction. The human Setup 0x02000001's authored spheres are (0,0,0.475) r=.48 and (0,0,1.350) r=.48; the old reconstruction from (0.48, 1.835) produced (0,0,0.48) + (0,0,1.355), a 5 mm head-center offset the TS-46 register row documented as a residual. Port: - SpherePath.InitPath gains a sphere-list overload (ImmutableArray< FlatCollisionSphere>, scale) sharing a new InitPathCore with the existing (radius, height) overload, which is now the degenerate 2-scalar case of the same code -- byte-for-byte unchanged, so every captured-fixture replay (CellarUpTrajectoryReplayTests, DoorBugTrajectoryReplayTests, CellarLipWedgeTests) keeps passing unmodified. - PhysicsEngine.ResolveWithTransition gains optional sphereList/ sphereScale parameters; empty/default preserves the legacy scalar path for every pre-existing caller. - LiveEntityMotionRuntimeController.GetSetupMoverShape is a new sibling of GetSetupCylinder (left untouched) that resolves the Setup's own sphere list plus Setup-derived step-up/step-down (CPartArray::GetStepUpHeight/GetStepDownHeight, 0x005180d0/0x005180f0, x ObjScale, 0.4 m fallback matching the pre-existing literal). - Threaded through PlayerMovementController (both resolve call sites, new SphereList property set by PlayerModeController.ApplyStepHeights and the Headless world projection), RuntimeRemotePhysicsUpdater (Tick + TickHidden), and RuntimeOrdinaryPhysicsUpdater.TryBegin. Remote/ordinary step heights are now Setup-derived instead of a hardcoded 0.4f literal. Projectile and camera-probe sweeps are untouched (already single-sphere-exact). - PlayerModeController.ApplyStepHeights also now applies the x ObjScale multiply to the player's own step heights (previously only the remote/ordinary paths did), closing an adjacent gap the P3 research flagged. Ts46SphereListConformanceTests proves the sphere-list overload sees the exact dat spheres (not the reconstruction), that the scalar overload is unchanged, and that ResolveWithTransition's sphereList parameter actually drives the sweep (a decoy-scalar control pair using a head-height obstacle sphere). Register: TS-46 retired (both residuals it named are closed); header count corrected to 40 active TS rows. dotnet build + dotnet test (Core.Tests 3991/2 skip, Runtime.Tests 425/0, App.Tests 3968/3 skip, complete solution build) all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3dc10accb0
commit
dae5b1ea68
21 changed files with 648 additions and 50 deletions
|
|
@ -1,6 +1,8 @@
|
|||
using System.Collections.Immutable;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
|
||||
|
|
@ -14,6 +16,11 @@ namespace AcDream.App.Physics;
|
|||
internal interface ILiveEntityMotionRuntimeBindings
|
||||
{
|
||||
(float Radius, float Height) GetSetupCylinder(uint serverGuid, WorldEntity entity);
|
||||
|
||||
/// <summary>TS-46 sibling of <see cref="GetSetupCylinder"/> — see
|
||||
/// <c>LiveEntityMotionRuntimeController.GetSetupMoverShape</c>.</summary>
|
||||
(ImmutableArray<FlatCollisionSphere> Spheres, float Scale, float StepUpHeight, float StepDownHeight)
|
||||
GetSetupMoverShape(uint serverGuid, WorldEntity entity);
|
||||
bool RouteServerMoveTo(
|
||||
MovementManager movement,
|
||||
uint cellId,
|
||||
|
|
@ -59,6 +66,10 @@ internal sealed class DeferredLiveEntityMotionRuntimeBindings
|
|||
uint serverGuid,
|
||||
WorldEntity entity) => Target.GetSetupCylinder(serverGuid, entity);
|
||||
|
||||
public (ImmutableArray<FlatCollisionSphere> Spheres, float Scale, float StepUpHeight, float StepDownHeight)
|
||||
GetSetupMoverShape(uint serverGuid, WorldEntity entity) =>
|
||||
Target.GetSetupMoverShape(serverGuid, entity);
|
||||
|
||||
public bool RouteServerMoveTo(
|
||||
MovementManager movement,
|
||||
uint cellId,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Immutable;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -283,6 +284,45 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
return (setup.Radius * scale, setup.Height * scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TS-46 (2026-07-30) sibling of <see cref="GetSetupCylinder"/>: the
|
||||
/// Setup's own ≤2-sphere list (retail <c>CPhysicsObj::transition</c>
|
||||
/// 0x00512dc0 → <c>SPHEREPATH::init_sphere</c> 0x0050c670) plus
|
||||
/// Setup-derived step-up/step-down (<c>CPartArray::GetStepUpHeight</c>/
|
||||
/// <c>GetStepDownHeight</c>, 0x005180d0/0x005180f0 — both scaled by the
|
||||
/// object's own ObjScale, matching the existing 0.4 m literal fallback
|
||||
/// the remote/ordinary callers already carried). <see cref="GetSetupCylinder"/>
|
||||
/// is deliberately UNTOUCHED — its callers want the single-radius/height
|
||||
/// CYLINDER for sticky/moveto math, not the collision sweep shape.
|
||||
/// Returns an empty sphere list (and the 0.4 m fallbacks) when the
|
||||
/// entity has no resolvable/prepared Setup, or when the Setup carries no
|
||||
/// sphere rows — <see cref="PhysicsEngine.ResolveWithTransition"/>'s
|
||||
/// <c>sphereList</c> parameter treats empty as "use the legacy
|
||||
/// two-scalar reconstruction", so this degrades gracefully rather than
|
||||
/// degenerating the sweep.
|
||||
/// </summary>
|
||||
public (ImmutableArray<FlatCollisionSphere> Spheres, float Scale, float StepUpHeight, float StepDownHeight)
|
||||
GetSetupMoverShape(uint serverGuid, AcDream.Core.World.WorldEntity entity)
|
||||
{
|
||||
FlatSetupCollision? setup =
|
||||
_physicsDataCache.GetFlatSetup(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is null)
|
||||
return (ImmutableArray<FlatCollisionSphere>.Empty, 1f, 0.4f, 0.4f);
|
||||
|
||||
// Same scale resolution as GetSetupCylinder (see its own comment):
|
||||
// the spawn record's ObjScale is authoritative for live spawns; a
|
||||
// non-spawn entity (scenery) falls back to WorldEntity.Scale.
|
||||
float scale =
|
||||
_liveEntities.Snapshots.TryGetValue(serverGuid, out var sp)
|
||||
&& sp.ObjScale is { } objScale && objScale > 0f
|
||||
? objScale
|
||||
: (entity.Scale > 0f ? entity.Scale : 1f);
|
||||
|
||||
float stepUp = setup.StepUpHeight > 0f ? setup.StepUpHeight * scale : 0.4f;
|
||||
float stepDown = setup.StepDownHeight > 0f ? setup.StepDownHeight * scale : 0.4f;
|
||||
return (setup.Spheres, scale, stepUp, stepDown);
|
||||
}
|
||||
|
||||
// #184 Slice 2a: ApplyPositionManagerDelta + SyncRemoteShadowToBody moved to
|
||||
// AcDream.App.Physics.RemotePhysicsUpdater. ApplyPositionManagerDelta had no
|
||||
// caller outside the DR tick; SyncRemoteShadowToBody is now called back via
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Immutable;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
|
|
@ -17,15 +18,23 @@ internal sealed class LiveEntityOrdinaryPhysicsUpdater
|
|||
private readonly RuntimeOrdinaryPhysicsUpdater _runtime;
|
||||
private readonly Func<uint, WorldEntity, (float Radius, float Height)>
|
||||
_getSetupCylinder;
|
||||
private readonly Func<uint, WorldEntity,
|
||||
(ImmutableArray<FlatCollisionSphere> Spheres, float Scale, float StepUpHeight, float StepDownHeight)>
|
||||
_getSetupMoverShape;
|
||||
|
||||
public LiveEntityOrdinaryPhysicsUpdater(
|
||||
RuntimePhysicsState physics,
|
||||
Func<uint, WorldEntity, (float Radius, float Height)> getSetupCylinder)
|
||||
Func<uint, WorldEntity, (float Radius, float Height)> getSetupCylinder,
|
||||
Func<uint, WorldEntity,
|
||||
(ImmutableArray<FlatCollisionSphere> Spheres, float Scale, float StepUpHeight, float StepDownHeight)>
|
||||
getSetupMoverShape)
|
||||
{
|
||||
_runtime = new RuntimeOrdinaryPhysicsUpdater(
|
||||
physics ?? throw new ArgumentNullException(nameof(physics)));
|
||||
_getSetupCylinder = getSetupCylinder
|
||||
?? throw new ArgumentNullException(nameof(getSetupCylinder));
|
||||
_getSetupMoverShape = getSetupMoverShape
|
||||
?? throw new ArgumentNullException(nameof(getSetupMoverShape));
|
||||
}
|
||||
|
||||
public bool Tick(
|
||||
|
|
@ -49,6 +58,7 @@ internal sealed class LiveEntityOrdinaryPhysicsUpdater
|
|||
if (record.PhysicsBody is not { } body)
|
||||
return false;
|
||||
var (radius, height) = _getSetupCylinder(record.ServerGuid, entity);
|
||||
var shape = _getSetupMoverShape(record.ServerGuid, entity);
|
||||
bool ExternalOwnerValid() =>
|
||||
IsCurrent(
|
||||
runtime,
|
||||
|
|
@ -68,7 +78,11 @@ internal sealed class LiveEntityOrdinaryPhysicsUpdater
|
|||
sequencer,
|
||||
captureAnimationHooks,
|
||||
ExternalOwnerValid,
|
||||
out RuntimeOrdinaryPhysicsCommit commit))
|
||||
out RuntimeOrdinaryPhysicsCommit commit,
|
||||
sphereList: shape.Spheres,
|
||||
sphereScale: shape.Scale,
|
||||
stepUpHeight: shape.StepUpHeight,
|
||||
stepDownHeight: shape.StepDownHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -18,6 +19,9 @@ internal sealed class RemotePhysicsUpdater
|
|||
private readonly RuntimeRemotePhysicsUpdater _runtime;
|
||||
private readonly Func<uint, WorldEntity, (float Radius, float Height)>
|
||||
_getSetupCylinder;
|
||||
private readonly Func<uint, WorldEntity,
|
||||
(ImmutableArray<FlatCollisionSphere> Spheres, float Scale, float StepUpHeight, float StepDownHeight)>
|
||||
_getSetupMoverShape;
|
||||
private readonly Action<uint, LiveEntityAnimationState, RemoteMotion, Vector3>
|
||||
_applyServerControlledVelocityCycle;
|
||||
private readonly List<LiveEntityRecord> _spatialRemoteSnapshot = new();
|
||||
|
|
@ -25,6 +29,9 @@ internal sealed class RemotePhysicsUpdater
|
|||
internal RemotePhysicsUpdater(
|
||||
RuntimePhysicsState physics,
|
||||
Func<uint, WorldEntity, (float Radius, float Height)> getSetupCylinder,
|
||||
Func<uint, WorldEntity,
|
||||
(ImmutableArray<FlatCollisionSphere> Spheres, float Scale, float StepUpHeight, float StepDownHeight)>
|
||||
getSetupMoverShape,
|
||||
Action<uint, LiveEntityAnimationState, RemoteMotion, Vector3>
|
||||
applyServerControlledVelocityCycle)
|
||||
{
|
||||
|
|
@ -32,6 +39,8 @@ internal sealed class RemotePhysicsUpdater
|
|||
physics ?? throw new ArgumentNullException(nameof(physics)));
|
||||
_getSetupCylinder = getSetupCylinder
|
||||
?? throw new ArgumentNullException(nameof(getSetupCylinder));
|
||||
_getSetupMoverShape = getSetupMoverShape
|
||||
?? throw new ArgumentNullException(nameof(getSetupMoverShape));
|
||||
_applyServerControlledVelocityCycle =
|
||||
applyServerControlledVelocityCycle
|
||||
?? throw new ArgumentNullException(
|
||||
|
|
@ -189,6 +198,7 @@ internal sealed class RemotePhysicsUpdater
|
|||
ownerClockEpoch);
|
||||
var (radius, height) =
|
||||
_getSetupCylinder(ownerRecord.ServerGuid, entity);
|
||||
var shape = _getSetupMoverShape(ownerRecord.ServerGuid, entity);
|
||||
Action<Vector3>? staleCycle =
|
||||
animationForVelocityCycle is null
|
||||
? null
|
||||
|
|
@ -221,7 +231,11 @@ internal sealed class RemotePhysicsUpdater
|
|||
entity.Rotation = snapshot.Orientation;
|
||||
return OwnerValid();
|
||||
},
|
||||
OwnerValid);
|
||||
OwnerValid,
|
||||
sphereList: shape.Spheres,
|
||||
sphereScale: shape.Scale,
|
||||
stepUpHeight: shape.StepUpHeight,
|
||||
stepDownHeight: shape.StepDownHeight);
|
||||
}
|
||||
|
||||
public bool TickHidden(
|
||||
|
|
@ -251,6 +265,7 @@ internal sealed class RemotePhysicsUpdater
|
|||
ownerClockEpoch);
|
||||
var (radius, height) =
|
||||
_getSetupCylinder(ownerRecord.ServerGuid, entity);
|
||||
var shape = _getSetupMoverShape(ownerRecord.ServerGuid, entity);
|
||||
return _runtime.TickHidden(
|
||||
ownerRecord.Canonical,
|
||||
remote,
|
||||
|
|
@ -270,7 +285,11 @@ internal sealed class RemotePhysicsUpdater
|
|||
entity.Rotation = snapshot.Orientation;
|
||||
return OwnerValid();
|
||||
},
|
||||
OwnerValid);
|
||||
OwnerValid,
|
||||
sphereList: shape.Spheres,
|
||||
sphereScale: shape.Scale,
|
||||
stepUpHeight: shape.StepUpHeight,
|
||||
stepDownHeight: shape.StepDownHeight);
|
||||
}
|
||||
|
||||
public void SyncRemoteShadowToBody(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue