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>
118 lines
4.1 KiB
C#
118 lines
4.1 KiB
C#
using System.Collections.Immutable;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using AcDream.Core.World;
|
|
using AcDream.Runtime.Physics;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.App.Physics;
|
|
|
|
/// <summary>
|
|
/// Projects the presentation-free Runtime result of retail
|
|
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0) into App's
|
|
/// <see cref="WorldEntity"/> and spatial buckets.
|
|
/// </summary>
|
|
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,
|
|
(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(
|
|
LiveEntityRuntime runtime,
|
|
LiveEntityRecord record,
|
|
WorldEntity entity,
|
|
Frame rootFrame,
|
|
float objectScale,
|
|
float quantum,
|
|
int liveCenterX,
|
|
int liveCenterY,
|
|
ulong objectClockEpoch,
|
|
AnimationSequencer? sequencer,
|
|
Action<uint, AnimationSequencer> captureAnimationHooks)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(runtime);
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
ArgumentNullException.ThrowIfNull(rootFrame);
|
|
ArgumentNullException.ThrowIfNull(captureAnimationHooks);
|
|
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,
|
|
record,
|
|
entity,
|
|
body,
|
|
objectClockEpoch);
|
|
|
|
if (!_runtime.TryBegin(
|
|
record.Canonical,
|
|
rootFrame,
|
|
objectScale,
|
|
quantum,
|
|
radius,
|
|
height,
|
|
objectClockEpoch,
|
|
sequencer,
|
|
captureAnimationHooks,
|
|
ExternalOwnerValid,
|
|
out RuntimeOrdinaryPhysicsCommit commit,
|
|
sphereList: shape.Spheres,
|
|
sphereScale: shape.Scale,
|
|
stepUpHeight: shape.StepUpHeight,
|
|
stepDownHeight: shape.StepDownHeight))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _runtime.Complete(
|
|
commit,
|
|
liveCenterX,
|
|
liveCenterY,
|
|
snapshot =>
|
|
{
|
|
if (!ExternalOwnerValid())
|
|
return false;
|
|
|
|
entity.SetPosition(snapshot.Position);
|
|
entity.Rotation = snapshot.Orientation;
|
|
entity.ParentCellId = snapshot.FullCellId;
|
|
return ExternalOwnerValid();
|
|
});
|
|
}
|
|
|
|
private static bool IsCurrent(
|
|
LiveEntityRuntime runtime,
|
|
LiveEntityRecord record,
|
|
WorldEntity entity,
|
|
PhysicsBody body,
|
|
ulong objectClockEpoch) =>
|
|
runtime.IsCurrentSpatialRootObject(record)
|
|
&& record.ObjectClockEpoch == objectClockEpoch
|
|
&& ReferenceEquals(record.WorldEntity, entity)
|
|
&& ReferenceEquals(record.PhysicsBody, body)
|
|
&& record.RemoteMotionRuntime is null
|
|
&& record.ProjectileRuntime is null;
|
|
}
|