feat(physics): S2 chunk 1b - callers supply the visual part array; installed-DAT comparator
Every production registration now hands ShadowObjectRegistry the object's whole visual part array beside its collision dispatch: static publication (App LandblockPhysicsPublisher and the headless Content twin) through ShadowShapeBuilder.FromStaticRenderParts, live entities (Runtime LiveEntityCollisionBuilder) through the new FromSetupRenderParts, which walks every Setup part with the same physics-sphere-else-drawing-sphere and part-box rule from the PhysicsDataCache Runtime already reaches. Nothing consumes the retail products yet; the App hermetic lane still passes 6,758/6,758. The Lane=InstalledDat comparator registers five real fixtures through the real publication inputs and prints retail CELLARRAY, collision cells, and the old render cells side by side: Facility Hub stair Setup 0x02000623 (7 cells incl. 0x8A02015F/015E), cathedral ramp 0x020009A2 (3 cells, the genuine multi-part case), the #334 Neftet formation (25 cells), and a landblock-edge crosser (6 cells, 2 in the neighbor block). All three answers agree for BSP-bearing objects, as the shared primitive predicts; the divergence chunk 3 expects appears only for decorative non-BSP parts. Core Physics 2,202/2,202; Runtime 1,884/1,884; App hermetic 6,758/6,758; comparator + stair pin 5/5; solution Release build 0 warnings / 0 errors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
059b8883ab
commit
5a2792d689
7 changed files with 908 additions and 11 deletions
|
|
@ -953,6 +953,20 @@ public sealed class LandblockPhysicsPublisher
|
|||
entity.MeshRefs,
|
||||
entity.IsBuildingShell,
|
||||
publication.StagingCache.GetGfxObj);
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 1b: retail's WHOLE visual part array —
|
||||
// every visual part of this static, colliding or not — beside the
|
||||
// BSP-only/primitive-only COLLISION dispatch above and below. The
|
||||
// SAME resolver pair WalkProductionWorldData.ResolveStaticRenderCells
|
||||
// already reads off this landblock's PhysicsDataCache. A side
|
||||
// product only: no consumer reads it yet.
|
||||
IReadOnlyList<ShadowShape> partArray =
|
||||
ShadowShapeBuilder.FromStaticRenderParts(
|
||||
entity.MeshRefs,
|
||||
publication.StagingCache.GetGfxObj,
|
||||
publication.StagingCache.GetVisualBounds,
|
||||
out _);
|
||||
|
||||
entityBspCount = bspShapes.Count;
|
||||
if (entityBspCount > 0)
|
||||
{
|
||||
|
|
@ -967,7 +981,8 @@ public sealed class LandblockPhysicsPublisher
|
|||
publication.Origin.Y,
|
||||
landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
isStatic: true,
|
||||
partArray: partArray);
|
||||
LogMultipartRegistration(landblock, entity, bspShapes);
|
||||
}
|
||||
|
||||
|
|
@ -1060,7 +1075,8 @@ public sealed class LandblockPhysicsPublisher
|
|||
publication.Origin.Y,
|
||||
landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
isStatic: true,
|
||||
partArray: partArray);
|
||||
LogSetupRegistration(landblock, entity, setupShapes);
|
||||
entityCylinderCount = setupShapes.Count;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -614,6 +614,18 @@ public static class LandblockPhysicsContentBuilder
|
|||
entity.MeshRefs,
|
||||
entity.IsBuildingShell,
|
||||
cache.GetGfxObj);
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 1b: retail's WHOLE visual part
|
||||
// array, mirroring LandblockPhysicsPublisher.PublishStaticEntity
|
||||
// exactly — same PhysicsDataCache resolver pair, same
|
||||
// FromStaticRenderParts primitive. A side product only.
|
||||
IReadOnlyList<ShadowShape> partArray =
|
||||
ShadowShapeBuilder.FromStaticRenderParts(
|
||||
entity.MeshRefs,
|
||||
cache.GetGfxObj,
|
||||
cache.GetVisualBounds,
|
||||
out _);
|
||||
|
||||
if (bspShapes.Count > 0)
|
||||
{
|
||||
engine.ShadowObjects.RegisterMultiPart(
|
||||
|
|
@ -627,7 +639,8 @@ public static class LandblockPhysicsContentBuilder
|
|||
worldOffsetY: origin.Y,
|
||||
landblockId: landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
isStatic: true,
|
||||
partArray: partArray);
|
||||
bspOwners++;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -711,7 +724,8 @@ public static class LandblockPhysicsContentBuilder
|
|||
worldOffsetY: origin.Y,
|
||||
landblockId: landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
isStatic: true,
|
||||
partArray: partArray);
|
||||
setupOwners++;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -429,6 +429,110 @@ public static class ShadowShapeBuilder
|
|||
return parts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves EVERY part of a live entity's <see cref="Setup"/> into the
|
||||
/// paired sphere + vertex-box geometry used by retail's render-shadow
|
||||
/// cell registration — the live-entity analog of
|
||||
/// <see cref="FromStaticRenderParts"/>. Unlike <see cref="FromSetup"/>'s
|
||||
/// step 3 (which emits a BSP shape only for a part whose GfxObj carries a
|
||||
/// physics BSP), this walks every Setup part unconditionally:
|
||||
/// <c>CEnvCell::init_static_objects</c>/<c>CPartArray::AddPartsShadow</c>
|
||||
/// registers every visual part in each crossed cell even when that part
|
||||
/// cannot collide, and a live Setup is no exception.
|
||||
///
|
||||
/// <para>The returned values are geometry carriers only. Callers must not
|
||||
/// publish them to the collision registry.</para>
|
||||
/// </summary>
|
||||
/// <param name="setup">The live entity's Setup.</param>
|
||||
/// <param name="entScale">The entity's spawn scale, applied uniformly —
|
||||
/// same composition as <see cref="FromSetup"/>.</param>
|
||||
/// <param name="effectivePartGfxObjIds">Current part identities after
|
||||
/// retail <c>AnimPartChanged</c> processing (see <see cref="FromSetup"/>).
|
||||
/// Null or short lists fall back to the Setup identity.</param>
|
||||
/// <param name="partPoseOverride">Pose priority 1: the motion-table
|
||||
/// default-state pose (#175). Falls back to the Setup's own placement
|
||||
/// frame, then identity — the same priority <see cref="FromSetup"/>'s
|
||||
/// step 3 uses.</param>
|
||||
/// <param name="getGfxObj">Resolves a GfxObj id to its cached physics
|
||||
/// (BSP + bounding sphere + visual bounds). Production:
|
||||
/// <c>id => physicsData.GetGfxObj(id)</c> — the SAME resolver shape
|
||||
/// <see cref="FromStaticRenderParts"/> takes, so a static and a live
|
||||
/// Setup sharing a GfxObj id can never disagree about its render
|
||||
/// geometry.</param>
|
||||
/// <param name="getVisualBounds">Fallback drawing-sphere/AABB source for
|
||||
/// a part whose GfxObj carries no physics BSP. Production:
|
||||
/// <c>id => physicsData.GetVisualBounds(id)</c>.</param>
|
||||
public static List<ShadowShape> FromSetupRenderParts(
|
||||
Setup setup,
|
||||
float entScale,
|
||||
IReadOnlyList<uint>? effectivePartGfxObjIds,
|
||||
IReadOnlyList<Frame>? partPoseOverride,
|
||||
Func<uint, GfxObjPhysics?> getGfxObj,
|
||||
Func<uint, GfxObjVisualBounds?> getVisualBounds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(setup);
|
||||
ArgumentNullException.ThrowIfNull(getGfxObj);
|
||||
ArgumentNullException.ThrowIfNull(getVisualBounds);
|
||||
|
||||
var parts = new List<ShadowShape>(setup.Parts.Count);
|
||||
AnimationFrame? placementFrame = ResolvePlacementFrame(setup);
|
||||
for (int i = 0; i < setup.Parts.Count; i++)
|
||||
{
|
||||
uint gfxId = EffectivePartGfxObjId(setup, effectivePartGfxObjIds, i);
|
||||
|
||||
GfxObjPhysics? physics = getGfxObj(gfxId);
|
||||
bool partHasPhysicsBsp = physics?.FlatPhysicsBsp is { RootIndex: >= 0 }
|
||||
|| physics?.BSP?.Root is not null;
|
||||
|
||||
FlatGfxObjVisualBounds? flatBounds = physics?.VisualBounds;
|
||||
if (flatBounds is null && getVisualBounds(gfxId) is { } visual)
|
||||
{
|
||||
flatBounds = new FlatGfxObjVisualBounds(
|
||||
visual.Min,
|
||||
visual.Max,
|
||||
visual.Center,
|
||||
visual.Radius,
|
||||
visual.HalfExtents);
|
||||
}
|
||||
if (flatBounds is not { } bounds)
|
||||
continue;
|
||||
|
||||
Frame partFrame;
|
||||
if (partPoseOverride is not null && i < partPoseOverride.Count)
|
||||
partFrame = partPoseOverride[i];
|
||||
else if (placementFrame is not null && i < placementFrame.Frames.Count)
|
||||
partFrame = placementFrame.Frames[i];
|
||||
else
|
||||
partFrame = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
|
||||
|
||||
FlatCollisionSphere sphere;
|
||||
if (partHasPhysicsBsp)
|
||||
{
|
||||
FlatPhysicsBsp? flat = physics!.FlatPhysicsBsp;
|
||||
sphere = flat is { RootIndex: >= 0 }
|
||||
? flat.Nodes[flat.RootIndex].BoundingSphere
|
||||
: new FlatCollisionSphere(
|
||||
physics.BoundingSphere?.Origin ?? bounds.Center,
|
||||
physics.BoundingSphere?.Radius ?? bounds.Radius);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Retail falls back from physics_sphere to the GfxObj drawing
|
||||
// sphere — same fallback as FromStaticRenderParts.
|
||||
sphere = new FlatCollisionSphere(bounds.Center, bounds.Radius);
|
||||
}
|
||||
|
||||
parts.Add(ShadowShape.Bsp(
|
||||
gfxId,
|
||||
new Vector3(partFrame.Origin.X, partFrame.Origin.Y, partFrame.Origin.Z) * entScale,
|
||||
partFrame.Orientation,
|
||||
entScale,
|
||||
ShadowPartGeometry.Create(sphere, bounds)));
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The collision identity of part <paramref name="index"/>: the installed
|
||||
/// <c>AnimPartChanged</c> replacement when one was supplied, else the
|
||||
|
|
|
|||
|
|
@ -24,7 +24,14 @@ internal sealed record LiveEntityCollisionRegistration(
|
|||
float WorldOffsetX,
|
||||
float WorldOffsetY,
|
||||
uint LandblockId,
|
||||
uint SeedCellId);
|
||||
uint SeedCellId,
|
||||
// Campaign OVERHAUL S2 chunk 1b: retail's WHOLE visual part array for
|
||||
// this live entity's Setup (ShadowShapeBuilder.FromSetupRenderParts) —
|
||||
// every part, colliding or not, physics sphere else drawing sphere,
|
||||
// exact part box. Distinct from Shapes, which stays the BSP-exclusive
|
||||
// COLLISION dispatch (AP-152). A side product only: no consumer reads
|
||||
// it yet.
|
||||
IReadOnlyList<ShadowShape> RenderParts);
|
||||
|
||||
/// <summary>
|
||||
/// Ports the live-object collision-shape policy used by
|
||||
|
|
@ -72,6 +79,21 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
private readonly Func<uint, bool> _hasPhysicsBsp;
|
||||
private readonly LiveEntityDefaultPoseResolver _defaultPose;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S2 chunk 1b: the SAME resolver shape
|
||||
/// <c>WalkProductionWorldData.ResolveStaticRenderCells</c> passes to
|
||||
/// <see cref="ShadowShapeBuilder.FromStaticRenderParts"/> — a static and a
|
||||
/// live Setup sharing a GfxObj id can never disagree about its render
|
||||
/// geometry. Production: <c>PhysicsDataCache.GetGfxObj</c>.
|
||||
/// </summary>
|
||||
private readonly Func<uint, GfxObjPhysics?> _getGfxObj;
|
||||
|
||||
/// <summary>Fallback drawing-sphere/AABB source, paired with
|
||||
/// <see cref="_getGfxObj"/> exactly as <see cref="ShadowShapeBuilder.FromStaticRenderParts"/>
|
||||
/// pairs its own two resolvers. Production:
|
||||
/// <c>PhysicsDataCache.GetVisualBounds</c>.</summary>
|
||||
private readonly Func<uint, GfxObjVisualBounds?> _getVisualBounds;
|
||||
|
||||
public LiveEntityCollisionBuilder(
|
||||
PhysicsDataCache physicsData,
|
||||
LiveEntityDefaultPoseResolver defaultPose)
|
||||
|
|
@ -86,7 +108,9 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
asset!.VisualBounds)
|
||||
: (ShadowPartGeometry?)null;
|
||||
},
|
||||
defaultPose)
|
||||
defaultPose,
|
||||
id => physicsData.GetGfxObj(id),
|
||||
id => physicsData.GetVisualBounds(id))
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(physicsData);
|
||||
}
|
||||
|
|
@ -101,13 +125,21 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
/// walk cannot be left without a box (#334).</param>
|
||||
internal LiveEntityCollisionBuilder(
|
||||
Func<uint, ShadowPartGeometry?> physicsBspBounds,
|
||||
LiveEntityDefaultPoseResolver defaultPose)
|
||||
LiveEntityDefaultPoseResolver defaultPose,
|
||||
Func<uint, GfxObjPhysics?>? getGfxObj = null,
|
||||
Func<uint, GfxObjVisualBounds?>? getVisualBounds = null)
|
||||
{
|
||||
_physicsBspBounds = physicsBspBounds
|
||||
?? throw new ArgumentNullException(nameof(physicsBspBounds));
|
||||
_hasPhysicsBsp = id => _physicsBspBounds(id) is not null;
|
||||
_defaultPose = defaultPose
|
||||
?? throw new ArgumentNullException(nameof(defaultPose));
|
||||
// Campaign OVERHAUL S2 chunk 1b: absent in every existing fixture
|
||||
// call site (both positional-arg tests and production before this
|
||||
// chunk) — resolves to "no render geometry for any part", so
|
||||
// Build() below retains its exact prior Shapes/registration output.
|
||||
_getGfxObj = getGfxObj ?? (_ => null);
|
||||
_getVisualBounds = getVisualBounds ?? (_ => null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -197,6 +229,18 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
if (spawn.ItemType == (uint)ItemType.Creature)
|
||||
flags |= EntityCollisionFlags.IsCreature;
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 1b: retail's WHOLE visual part array —
|
||||
// every Setup part, colliding or not — beside the BSP-exclusive
|
||||
// `shapes` collision dispatch above. A side product only; nothing
|
||||
// consumes it yet.
|
||||
IReadOnlyList<ShadowShape> renderParts = ShadowShapeBuilder.FromSetupRenderParts(
|
||||
setup,
|
||||
scale,
|
||||
effectivePartGfxObjIds,
|
||||
defaultPose,
|
||||
_getGfxObj,
|
||||
_getVisualBounds);
|
||||
|
||||
return new LiveEntityCollisionRegistration(
|
||||
entity.Id,
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
|
|
@ -208,7 +252,8 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
worldOrigin.X,
|
||||
worldOrigin.Y,
|
||||
position.LandblockId,
|
||||
position.LandblockId);
|
||||
position.LandblockId,
|
||||
renderParts);
|
||||
}
|
||||
|
||||
public static void Register(
|
||||
|
|
@ -228,7 +273,8 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
registration.WorldOffsetY,
|
||||
registration.LandblockId,
|
||||
registration.SeedCellId,
|
||||
isStatic: false);
|
||||
isStatic: false,
|
||||
partArray: registration.RenderParts);
|
||||
|
||||
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
|
||||
return;
|
||||
|
|
@ -276,6 +322,7 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
registration.LandblockId,
|
||||
registration.SeedCellId,
|
||||
isStatic: false,
|
||||
suspendIfNew);
|
||||
suspendIfNew,
|
||||
partArray: registration.RenderParts);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue