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
|
|
@ -481,7 +481,8 @@ internal sealed class LivePresentationCompositionPhase
|
|||
static value => value.Dispose());
|
||||
var ordinaryPhysicsUpdater = new LiveEntityOrdinaryPhysicsUpdater(
|
||||
d.EntityObjects.Physics,
|
||||
d.MotionBindings.GetSetupCylinder);
|
||||
d.MotionBindings.GetSetupCylinder,
|
||||
d.MotionBindings.GetSetupMoverShape);
|
||||
var animationScheduler = new LiveEntityAnimationScheduler(
|
||||
liveEntities,
|
||||
d.PlayerIdentity,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
|
|
@ -370,7 +372,7 @@ internal sealed class PlayerModeController :
|
|||
+ $"run={_skills.RunSkill} jump={_skills.JumpSkill}");
|
||||
}
|
||||
|
||||
ApplyStepHeights(controller, playerEntity);
|
||||
ApplyStepHeights(controller, playerEntity, playerGuid);
|
||||
uint initialCellId = ResolveInitialCell(playerGuid, playerEntity);
|
||||
|
||||
Action? drainPriorAnimationQueue = null;
|
||||
|
|
@ -532,7 +534,8 @@ internal sealed class PlayerModeController :
|
|||
|
||||
private void ApplyStepHeights(
|
||||
PlayerMovementController controller,
|
||||
WorldEntity playerEntity)
|
||||
WorldEntity playerEntity,
|
||||
uint playerGuid)
|
||||
{
|
||||
if ((playerEntity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
|
|
@ -544,22 +547,48 @@ internal sealed class PlayerModeController :
|
|||
_collisionAssets.CacheSetup(
|
||||
playerEntity.SourceGfxObjOrSetupId,
|
||||
setup);
|
||||
// TS-46 (2026-07-30): CPartArray::GetStepUpHeight/GetStepDownHeight
|
||||
// (0x005180d0/0x005180f0) return setup->step_up_height * this->scale
|
||||
// — apply the same ObjScale multiply the remote/ordinary paths now
|
||||
// use (LiveEntityMotionRuntimeController.GetSetupMoverShape), for
|
||||
// parity on a non-1.0-scale player (a rare but real case — e.g. a
|
||||
// disguise/size-changing effect). Human ObjScale is 1.0 in the
|
||||
// overwhelming common case, so this is a no-op there.
|
||||
float scale =
|
||||
_liveEntities.Snapshots.TryGetValue(playerGuid, out var sp)
|
||||
&& sp.ObjScale is { } objScale && objScale > 0f
|
||||
? objScale
|
||||
: (playerEntity.Scale > 0f ? playerEntity.Scale : 1f);
|
||||
controller.StepUpHeight = setup is { StepUpHeight: > 0f }
|
||||
? setup.StepUpHeight
|
||||
? setup.StepUpHeight * scale
|
||||
: 0.4f;
|
||||
controller.StepDownHeight = setup is { StepDownHeight: > 0f }
|
||||
? setup.StepDownHeight
|
||||
? setup.StepDownHeight * scale
|
||||
: 0.4f;
|
||||
// TS-46 (2026-07-30): the Setup's own ≤2-sphere list, verbatim —
|
||||
// retail CPhysicsObj::transition (0x00512dc0) seeds the sweep
|
||||
// from CPartArray::GetSphere, not a (radius, height) capsule
|
||||
// reconstruction. Empty (no Setup, or a Setup with no sphere
|
||||
// rows) leaves SphereList at its default empty value, which
|
||||
// ResolveWithTransition treats as "use the legacy scalar
|
||||
// reconstruction."
|
||||
controller.SphereList = setup?.Spheres is { Count: > 0 } spheres
|
||||
? spheres
|
||||
.Select(s => new FlatCollisionSphere(s.Origin, s.Radius))
|
||||
.ToImmutableArray()
|
||||
: ImmutableArray<FlatCollisionSphere>.Empty;
|
||||
Console.WriteLine(
|
||||
$"physics: player step heights — StepUp={controller.StepUpHeight:F3} m "
|
||||
+ $"(Setup.StepUpHeight={(setup?.StepUpHeight ?? 0f):F3}), "
|
||||
+ $"StepDown={controller.StepDownHeight:F3} m "
|
||||
+ $"(Setup.StepDownHeight={(setup?.StepDownHeight ?? 0f):F3})");
|
||||
+ $"(Setup.StepDownHeight={(setup?.StepDownHeight ?? 0f):F3}), "
|
||||
+ $"Spheres={controller.SphereList.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
controller.StepUpHeight = 0.4f;
|
||||
controller.StepDownHeight = 0.4f;
|
||||
controller.SphereList = ImmutableArray<FlatCollisionSphere>.Empty;
|
||||
Console.WriteLine(
|
||||
"physics: player step heights — defaulting to 0.4 m (no setup dat)");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -652,6 +652,7 @@ public sealed class GameWindow :
|
|||
_remotePhysicsUpdater = new AcDream.App.Physics.RemotePhysicsUpdater(
|
||||
_runtimeEntityObjects.Physics,
|
||||
_liveEntityMotionBindings.GetSetupCylinder,
|
||||
_liveEntityMotionBindings.GetSetupMoverShape,
|
||||
AcDream.App.Physics.RemoteServerControlledVelocityCycle.Apply);
|
||||
_remoteInboundMotion = new AcDream.App.Physics.RemoteInboundMotionDispatcher(
|
||||
(movement, cellId, update) =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
|
@ -1093,7 +1094,17 @@ public sealed class PhysicsEngine
|
|||
Vector3? localSphereOrigin = null,
|
||||
Quaternion? beginOrientation = null,
|
||||
Quaternion? endOrientation = null,
|
||||
uint designatedTargetId = 0)
|
||||
uint designatedTargetId = 0,
|
||||
// TS-46 (2026-07-30): the mover's own Setup ≤2-sphere list (retail
|
||||
// CPhysicsObj::transition 0x00512dc0 → SPHEREPATH::init_sphere
|
||||
// 0x0050c670), scaled by sphereScale (the object's own m_scale /
|
||||
// wire ObjScale) exactly as init_sphere applies it per-sphere.
|
||||
// Default/empty preserves the legacy sphereRadius/sphereHeight
|
||||
// two-scalar reconstruction below — every pre-existing caller
|
||||
// (camera probe, projectiles, captured-fixture replays) that omits
|
||||
// this parameter is byte-for-byte unaffected.
|
||||
ImmutableArray<FlatCollisionSphere> sphereList = default,
|
||||
float sphereScale = 1f)
|
||||
{
|
||||
// A6.P3 #98 (2026-05-23) live capture. Filtered to IsPlayer so NPC /
|
||||
// remote ResolveWithTransition calls don't pollute the capture. Snapshot
|
||||
|
|
@ -1185,15 +1196,33 @@ public sealed class PhysicsEngine
|
|||
transition.CollisionInfo.SetSlidingNormal(body.SlidingNormal);
|
||||
}
|
||||
|
||||
transition.SpherePath.InitPath(
|
||||
currentPos,
|
||||
targetPos,
|
||||
cellId,
|
||||
sphereRadius,
|
||||
sphereHeight,
|
||||
localSphereOrigin,
|
||||
beginOrientation,
|
||||
endOrientation);
|
||||
if (!sphereList.IsDefaultOrEmpty)
|
||||
{
|
||||
// TS-46: the Setup's verbatim sphere list, not the two-scalar
|
||||
// capsule reconstruction. localSphereOrigin has no meaning
|
||||
// here — every sphere already carries its own dat-authored
|
||||
// origin.
|
||||
transition.SpherePath.InitPath(
|
||||
currentPos,
|
||||
targetPos,
|
||||
cellId,
|
||||
sphereList,
|
||||
sphereScale,
|
||||
beginOrientation,
|
||||
endOrientation);
|
||||
}
|
||||
else
|
||||
{
|
||||
transition.SpherePath.InitPath(
|
||||
currentPos,
|
||||
targetPos,
|
||||
cellId,
|
||||
sphereRadius,
|
||||
sphereHeight,
|
||||
localSphereOrigin,
|
||||
beginOrientation,
|
||||
endOrientation);
|
||||
}
|
||||
|
||||
// #145: supply the carried cell-relative frame anchor to the outdoor
|
||||
// membership pick. body.Position - body.CellPosition.Frame.Origin is the TRUE
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
|
|
@ -970,7 +971,16 @@ public sealed class SpherePath
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the path for a simple point-to-point movement.
|
||||
/// Initialize the path for a simple point-to-point movement from a
|
||||
/// (radius, height) capsule reconstruction. TS-46 (2026-07-30): this is
|
||||
/// now the degenerate 2-scalar case of <see cref="InitPath(Vector3, Vector3, uint, ImmutableArray{FlatCollisionSphere}, float, Quaternion?, Quaternion?)"/> —
|
||||
/// retail's own <c>CPhysicsObj::transition</c> (0x00512dc0) seeds the
|
||||
/// sweep from the Setup's OWN sphere list (verbatim origin+radius per
|
||||
/// sphere), not from a symmetric two-scalar capsule. Callers with a
|
||||
/// resolved Setup should prefer the sphere-list overload; this scalar
|
||||
/// overload remains for callers without one (camera probe, projectiles,
|
||||
/// captured-fixture replays) and is unchanged byte-for-byte from the
|
||||
/// pre-TS-46 behavior.
|
||||
/// </summary>
|
||||
public void InitPath(
|
||||
Vector3 begin,
|
||||
|
|
@ -981,6 +991,97 @@ public sealed class SpherePath
|
|||
Vector3? localSphereOrigin = null,
|
||||
Quaternion? beginOrientation = null,
|
||||
Quaternion? endOrientation = null)
|
||||
{
|
||||
Vector3 origin0 = localSphereOrigin ?? new Vector3(0, 0, sphereRadius);
|
||||
if (sphereHeight > 0)
|
||||
{
|
||||
InitPathCore(
|
||||
begin, end, cellId, 2,
|
||||
origin0, sphereRadius,
|
||||
new Vector3(0, 0, sphereHeight - sphereRadius), sphereRadius,
|
||||
beginOrientation, endOrientation);
|
||||
}
|
||||
else
|
||||
{
|
||||
InitPathCore(
|
||||
begin, end, cellId, 1,
|
||||
origin0, sphereRadius,
|
||||
Vector3.Zero, 0f,
|
||||
beginOrientation, endOrientation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TS-46 (2026-07-30): initialize the path from the Setup's OWN authored
|
||||
/// sphere list — retail <c>CPhysicsObj::transition</c> (0x00512dc0) →
|
||||
/// <c>SPHEREPATH::init_sphere</c> (0x0050c670): up to 2 spheres, each
|
||||
/// origin AND radius independently scaled by the object's own
|
||||
/// <c>m_scale</c> (wire ObjScale), matching <c>init_sphere(count, src,
|
||||
/// scale)</c>'s exact signature. <paramref name="spheres"/> longer than 2
|
||||
/// is capped, matching retail's hard <c>num_sphere = min(count, 2)</c>.
|
||||
/// An empty list falls back to retail's own <c>numSphere == 0</c> arm
|
||||
/// (transition() passes a single dummy sphere, scale 1.0) rather than
|
||||
/// throwing, so a shapeless Setup degrades gracefully instead of
|
||||
/// degenerating the sweep with a zero radius.
|
||||
/// </summary>
|
||||
public void InitPath(
|
||||
Vector3 begin,
|
||||
Vector3 end,
|
||||
uint cellId,
|
||||
ImmutableArray<FlatCollisionSphere> spheres,
|
||||
float scale = 1f,
|
||||
Quaternion? beginOrientation = null,
|
||||
Quaternion? endOrientation = null)
|
||||
{
|
||||
if (spheres.IsDefaultOrEmpty)
|
||||
{
|
||||
InitPathCore(
|
||||
begin, end, cellId, 1,
|
||||
new Vector3(0, 0, PhysicsGlobals.DummySphereRadius), PhysicsGlobals.DummySphereRadius,
|
||||
Vector3.Zero, 0f,
|
||||
beginOrientation, endOrientation);
|
||||
return;
|
||||
}
|
||||
|
||||
int count = spheres.Length <= 2 ? spheres.Length : 2;
|
||||
FlatCollisionSphere s0 = spheres[0];
|
||||
if (count > 1)
|
||||
{
|
||||
FlatCollisionSphere s1 = spheres[1];
|
||||
InitPathCore(
|
||||
begin, end, cellId, 2,
|
||||
s0.Origin * scale, s0.Radius * scale,
|
||||
s1.Origin * scale, s1.Radius * scale,
|
||||
beginOrientation, endOrientation);
|
||||
}
|
||||
else
|
||||
{
|
||||
InitPathCore(
|
||||
begin, end, cellId, 1,
|
||||
s0.Origin * scale, s0.Radius * scale,
|
||||
Vector3.Zero, 0f,
|
||||
beginOrientation, endOrientation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared tail of both <c>InitPath</c> overloads: seeds begin/end/cell,
|
||||
/// orientation, the (≤2) local sphere slots, and the globalized
|
||||
/// <see cref="GlobalCurrCenter"/> array. Extracted so the sphere-list
|
||||
/// overload (TS-46) and the legacy scalar overload can never drift from
|
||||
/// each other on anything but sphere-source geometry.
|
||||
/// </summary>
|
||||
private void InitPathCore(
|
||||
Vector3 begin,
|
||||
Vector3 end,
|
||||
uint cellId,
|
||||
int numSphere,
|
||||
Vector3 origin0,
|
||||
float radius0,
|
||||
Vector3 origin1,
|
||||
float radius1,
|
||||
Quaternion? beginOrientation,
|
||||
Quaternion? endOrientation)
|
||||
{
|
||||
BeginPos = begin;
|
||||
EndPos = end;
|
||||
|
|
@ -992,18 +1093,13 @@ public sealed class SpherePath
|
|||
CurOrientation = BeginOrientation;
|
||||
CheckOrientation = BeginOrientation;
|
||||
|
||||
LocalSphere[0].Origin = localSphereOrigin ?? new Vector3(0, 0, sphereRadius);
|
||||
LocalSphere[0].Radius = sphereRadius;
|
||||
|
||||
if (sphereHeight > 0)
|
||||
NumSphere = numSphere;
|
||||
LocalSphere[0].Origin = origin0;
|
||||
LocalSphere[0].Radius = radius0;
|
||||
if (numSphere > 1)
|
||||
{
|
||||
NumSphere = 2;
|
||||
LocalSphere[1].Origin = new Vector3(0, 0, sphereHeight - sphereRadius);
|
||||
LocalSphere[1].Radius = sphereRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
NumSphere = 1;
|
||||
LocalSphere[1].Origin = origin1;
|
||||
LocalSphere[1].Radius = radius1;
|
||||
}
|
||||
|
||||
SetCheckPos(begin, cellId);
|
||||
|
|
|
|||
|
|
@ -461,5 +461,11 @@ internal sealed class HeadlessSessionWorldProjection
|
|||
controller.StepDownHeight = setup.StepDownHeight > 0f
|
||||
? setup.StepDownHeight
|
||||
: 0.4f;
|
||||
// TS-46 (2026-07-30): the prepared package already carries the
|
||||
// Setup's verbatim sphere list — no raw-DAT read needed here (unlike
|
||||
// the graphical PlayerModeController.ApplyStepHeights, which reads
|
||||
// DatReaderWriter.DBObjs.Setup directly). Empty falls back to
|
||||
// ResolveWithTransition's legacy scalar reconstruction.
|
||||
controller.SphereList = setup.Spheres;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,6 +163,21 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public float StepDownHeight { get; set; } = 0.4f;
|
||||
|
||||
/// <summary>
|
||||
/// TS-46 (2026-07-30): the player's own Setup ≤2-sphere list (dat
|
||||
/// <c>CSphere</c> Origin+Radius), verbatim per retail
|
||||
/// <c>CPhysicsObj::transition</c> (0x00512dc0) →
|
||||
/// <c>SPHEREPATH::init_sphere</c> (0x0050c670). Set at world-entry by
|
||||
/// <c>PlayerModeController.ApplyStepHeights</c> alongside
|
||||
/// <see cref="StepUpHeight"/>/<see cref="StepDownHeight"/>. Default
|
||||
/// (empty) falls back to <c>ResolveWithTransition</c>'s legacy
|
||||
/// (0.48, 1.835) two-scalar capsule reconstruction — the human Setup
|
||||
/// 0x02000001's authored spheres are (0,0,0.475) r=.48 and
|
||||
/// (0,0,1.350) r=.48, a 5 mm improvement over the reconstruction's
|
||||
/// (0,0,0.48) + (0,0,1.355).
|
||||
/// </summary>
|
||||
public System.Collections.Immutable.ImmutableArray<FlatCollisionSphere> SphereList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj::m_scale</c>. Grounded CSequence root
|
||||
/// displacement is multiplied by this value before PositionManager
|
||||
|
|
@ -1404,7 +1419,12 @@ public sealed class PlayerMovementController
|
|||
isOnGround: previousOnWalkable,
|
||||
body: _body,
|
||||
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: LocalEntityId);
|
||||
movingEntityId: LocalEntityId,
|
||||
// TS-46: the player's own Setup sphere list, scaled by
|
||||
// ObjectScale. Empty falls back to the 0.48/1.835
|
||||
// reconstruction above.
|
||||
sphereList: SphereList,
|
||||
sphereScale: ObjectScale);
|
||||
_body.CommitTransitionPosition(resolved.CellId, resolved.Position);
|
||||
PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
_body,
|
||||
|
|
@ -1849,7 +1869,14 @@ public sealed class PlayerMovementController
|
|||
// when the local player entity spawns (or stays 0 in tests, in
|
||||
// which case there's no registered ShadowEntry to collide with
|
||||
// anyway).
|
||||
movingEntityId: LocalEntityId);
|
||||
movingEntityId: LocalEntityId,
|
||||
// TS-46 (2026-07-30): the player's own Setup sphere list
|
||||
// (0x02000001: (0,0,0.475) r=.48 + (0,0,1.350) r=.48),
|
||||
// scaled by ObjectScale. Empty (unset/no Setup resolved yet)
|
||||
// falls back to the sphereRadius/sphereHeight reconstruction
|
||||
// above.
|
||||
sphereList: SphereList,
|
||||
sphereScale: ObjectScale);
|
||||
|
||||
// L.4-diag (2026-04-30): trace position transitions so we can see
|
||||
// whether the body is actually moving frame-to-frame on the steep
|
||||
|
|
|
|||
|
|
@ -49,7 +49,15 @@ internal sealed class RuntimeOrdinaryPhysicsUpdater
|
|||
AnimationSequencer? sequencer,
|
||||
Action<uint, AnimationSequencer> captureAnimationHooks,
|
||||
Func<bool>? externalOwnerValid,
|
||||
out RuntimeOrdinaryPhysicsCommit commit)
|
||||
out RuntimeOrdinaryPhysicsCommit commit,
|
||||
// TS-46 (2026-07-30): the Setup's own ≤2-sphere list + Setup-derived
|
||||
// step heights (LiveEntityMotionRuntimeController.GetSetupMoverShape).
|
||||
// Default/empty preserves the pre-TS-46 0.4 m literal fallback below.
|
||||
System.Collections.Immutable.ImmutableArray<FlatCollisionSphere>
|
||||
sphereList = default,
|
||||
float sphereScale = 1f,
|
||||
float stepUpHeight = 0.4f,
|
||||
float stepDownHeight = 0.4f)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(rootFrame);
|
||||
|
|
@ -130,14 +138,18 @@ internal sealed class RuntimeOrdinaryPhysicsUpdater
|
|||
sourceCellId,
|
||||
radius,
|
||||
height,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
stepUpHeight: stepUpHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
stepDownHeight: stepDownHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
isOnGround: previousOnWalkable,
|
||||
body: body,
|
||||
moverFlags: IsPlayerGuid(record.ServerGuid)
|
||||
? ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide
|
||||
: ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: movingEntityId);
|
||||
movingEntityId: movingEntityId,
|
||||
// TS-46: the Setup's own sphere list, scaled by ObjScale.
|
||||
// Empty falls back to the radius/height reconstruction above.
|
||||
sphereList: sphereList,
|
||||
sphereScale: sphereScale);
|
||||
|
||||
if (resolved.Ok)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -75,7 +75,15 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
System.Action<System.Numerics.Vector3>? applyStaleVelocityCycle = null,
|
||||
System.Func<RuntimeRemotePhysicsSnapshot, bool>?
|
||||
acknowledgeProjection = null,
|
||||
System.Func<bool>? externalOwnerValid = null)
|
||||
System.Func<bool>? externalOwnerValid = null,
|
||||
// TS-46 (2026-07-30): the Setup's own ≤2-sphere list + Setup-derived
|
||||
// step heights (LiveEntityMotionRuntimeController.GetSetupMoverShape).
|
||||
// Default/empty preserves the pre-TS-46 human-capsule fallback below.
|
||||
System.Collections.Immutable.ImmutableArray<AcDream.Core.Physics.FlatCollisionSphere>
|
||||
sphereList = default,
|
||||
float sphereScale = 1f,
|
||||
float stepUpHeight = 0.4f,
|
||||
float stepDownHeight = 0.4f)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(rm);
|
||||
|
|
@ -344,8 +352,13 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
preIntegratePos, postIntegratePos, rm.CellId,
|
||||
sphereRadius: deR,
|
||||
sphereHeight: deH,
|
||||
stepUpHeight: 0.4f, // L.2.3a: retail human-scale, was 2.0f
|
||||
stepDownHeight: 0.4f, // L.2.3a: retail human-scale, was 0.04f
|
||||
stepUpHeight: stepUpHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
stepDownHeight: stepDownHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
// TS-46: the Setup's own sphere list, scaled by the
|
||||
// creature's own ObjScale. Empty falls back to the
|
||||
// deR/deH two-scalar reconstruction above.
|
||||
sphereList: sphereList,
|
||||
sphereScale: sphereScale,
|
||||
// K-fix9 (2026-04-26): mirror the K-fix7 gate —
|
||||
// airborne remotes must NOT pre-seed the
|
||||
// ContactPlane, otherwise AdjustOffset's snap-to-plane
|
||||
|
|
@ -629,7 +642,13 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
AcDream.Core.Physics.AnimationSequencer? sequencer = null,
|
||||
System.Func<RuntimeRemotePhysicsSnapshot, bool>?
|
||||
acknowledgeProjection = null,
|
||||
System.Func<bool>? externalOwnerValid = null)
|
||||
System.Func<bool>? externalOwnerValid = null,
|
||||
// TS-46 (2026-07-30): see the visible Tick's identical parameters.
|
||||
System.Collections.Immutable.ImmutableArray<AcDream.Core.Physics.FlatCollisionSphere>
|
||||
sphereList = default,
|
||||
float sphereScale = 1f,
|
||||
float stepUpHeight = 0.4f,
|
||||
float stepDownHeight = 0.4f)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(rm);
|
||||
|
|
@ -700,15 +719,19 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
rm.CellId,
|
||||
radius,
|
||||
height,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
stepUpHeight: stepUpHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
stepDownHeight: stepDownHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
isOnGround: previousOnWalkable,
|
||||
body: rm.Body,
|
||||
moverFlags: IsPlayerGuid(record.ServerGuid)
|
||||
? AcDream.Core.Physics.ObjectInfoState.IsPlayer
|
||||
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide
|
||||
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: localEntityId);
|
||||
movingEntityId: localEntityId,
|
||||
// TS-46: the Setup's own sphere list, scaled by ObjScale.
|
||||
// Empty falls back to the radius/height reconstruction above.
|
||||
sphereList: sphereList,
|
||||
sphereScale: sphereScale);
|
||||
rm.Body.Position = resolved.Position;
|
||||
if (resolved.CellId != 0)
|
||||
committedCellId = resolved.CellId;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue