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,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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue