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>
104 lines
3.7 KiB
C#
104 lines
3.7 KiB
C#
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;
|
|
|
|
namespace AcDream.App.Physics;
|
|
|
|
/// <summary>
|
|
/// Narrow reverse seam for collaborators constructed before the live network
|
|
/// owner. It carries no state or identity: every call resolves the one bound
|
|
/// update-thread owner, and use before bind fails immediately.
|
|
/// </summary>
|
|
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,
|
|
WorldSession.EntityMotionUpdate update);
|
|
void StickToObjectFromWire(IPhysicsObjHost? host, uint targetGuid);
|
|
void ClearTargetForHiddenEntity(uint serverGuid);
|
|
IPhysicsObjHost? ResolvePhysicsHost(uint serverGuid);
|
|
}
|
|
|
|
internal sealed class DeferredLiveEntityMotionRuntimeBindings
|
|
: ILiveEntityMotionRuntimeBindings
|
|
{
|
|
private ILiveEntityMotionRuntimeBindings? _target;
|
|
|
|
public void Bind(ILiveEntityMotionRuntimeBindings target)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(target);
|
|
if (_target is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Live entity motion runtime bindings are already bound.");
|
|
}
|
|
_target = target;
|
|
}
|
|
|
|
public IDisposable BindOwned(ILiveEntityMotionRuntimeBindings target)
|
|
{
|
|
Bind(target);
|
|
return new Binding(this, target);
|
|
}
|
|
|
|
private void Unbind(ILiveEntityMotionRuntimeBindings expected)
|
|
{
|
|
if (ReferenceEquals(_target, expected))
|
|
_target = null;
|
|
}
|
|
|
|
private ILiveEntityMotionRuntimeBindings Target =>
|
|
_target ?? throw new InvalidOperationException(
|
|
"Live entity motion runtime bindings were used before binding.");
|
|
|
|
public (float Radius, float Height) GetSetupCylinder(
|
|
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,
|
|
WorldSession.EntityMotionUpdate update) =>
|
|
Target.RouteServerMoveTo(movement, cellId, update);
|
|
|
|
public void StickToObjectFromWire(IPhysicsObjHost? host, uint targetGuid) =>
|
|
Target.StickToObjectFromWire(host, targetGuid);
|
|
|
|
public void ClearTargetForHiddenEntity(uint serverGuid) =>
|
|
Target.ClearTargetForHiddenEntity(serverGuid);
|
|
|
|
public IPhysicsObjHost? ResolvePhysicsHost(uint serverGuid) =>
|
|
Target.ResolvePhysicsHost(serverGuid);
|
|
|
|
private sealed class Binding : IDisposable
|
|
{
|
|
private DeferredLiveEntityMotionRuntimeBindings? _owner;
|
|
private readonly ILiveEntityMotionRuntimeBindings _expected;
|
|
|
|
public Binding(
|
|
DeferredLiveEntityMotionRuntimeBindings owner,
|
|
ILiveEntityMotionRuntimeBindings expected)
|
|
{
|
|
_owner = owner;
|
|
_expected = expected;
|
|
}
|
|
|
|
public void Dispose() =>
|
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
|
}
|
|
}
|