Move appearance rebinding, collision construction, default-pose resolution, local shadow ownership, and exact leave-world presentation into focused owners. Preserve retail parent ordering with staged validation, committed recovery, recursive attached-subtree withdrawal, and retryable exact teardown across parent, pickup, position, unwield, and pose-loss edges. Co-Authored-By: OpenAI Codex <codex@openai.com>
231 lines
8.4 KiB
C#
231 lines
8.4 KiB
C#
using System.Numerics;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.App.Physics;
|
|
|
|
/// <summary>
|
|
/// Immutable input to the canonical shadow registry for one live object.
|
|
/// Building is DAT/physics-only and therefore testable without a window or GL
|
|
/// context; registration remains one explicit commit.
|
|
/// </summary>
|
|
internal sealed record LiveEntityCollisionRegistration(
|
|
uint EntityId,
|
|
uint SourceId,
|
|
Vector3 EntityWorldPosition,
|
|
Quaternion EntityWorldRotation,
|
|
IReadOnlyList<ShadowShape> Shapes,
|
|
uint State,
|
|
EntityCollisionFlags Flags,
|
|
float WorldOffsetX,
|
|
float WorldOffsetY,
|
|
uint LandblockId,
|
|
uint SeedCellId);
|
|
|
|
/// <summary>
|
|
/// Ports the live-object collision-shape policy used by
|
|
/// <c>CPartArray::FindObjCollisions</c>: CylSpheres before Spheres, every
|
|
/// physics-BSP part, and the established Setup-radius fallback for the
|
|
/// remaining ACE prop data.
|
|
/// </summary>
|
|
internal sealed class LiveEntityCollisionBuilder
|
|
{
|
|
private readonly Func<uint, bool> _hasPhysicsBsp;
|
|
private readonly Func<uint, float?> _physicsBspRadius;
|
|
private readonly LiveEntityDefaultPoseResolver _defaultPose;
|
|
|
|
public LiveEntityCollisionBuilder(
|
|
PhysicsDataCache physicsData,
|
|
LiveEntityDefaultPoseResolver defaultPose)
|
|
: this(
|
|
id => physicsData.GetGfxObj(id)?.BSP?.Root is not null,
|
|
id => physicsData.GetGfxObj(id)?.BoundingSphere?.Radius,
|
|
defaultPose)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(physicsData);
|
|
}
|
|
|
|
internal LiveEntityCollisionBuilder(
|
|
Func<uint, bool> hasPhysicsBsp,
|
|
Func<uint, float?> physicsBspRadius,
|
|
LiveEntityDefaultPoseResolver defaultPose)
|
|
{
|
|
_hasPhysicsBsp = hasPhysicsBsp
|
|
?? throw new ArgumentNullException(nameof(hasPhysicsBsp));
|
|
_physicsBspRadius = physicsBspRadius
|
|
?? throw new ArgumentNullException(nameof(physicsBspRadius));
|
|
_defaultPose = defaultPose
|
|
?? throw new ArgumentNullException(nameof(defaultPose));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves collision slot zero for every post-AnimPartChanged identity.
|
|
/// This intentionally has no visual-LOD option or Setup-type input.
|
|
/// </summary>
|
|
public static uint[] ResolveEffectivePartIdentities(
|
|
IReadOnlyList<uint> postAnimPartGfxObjIds,
|
|
Func<uint, uint> resolveSlotZero)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(postAnimPartGfxObjIds);
|
|
ArgumentNullException.ThrowIfNull(resolveSlotZero);
|
|
var result = new uint[postAnimPartGfxObjIds.Count];
|
|
for (int i = 0; i < result.Length; i++)
|
|
result[i] = resolveSlotZero(postAnimPartGfxObjIds[i]);
|
|
return result;
|
|
}
|
|
|
|
public LiveEntityCollisionRegistration? Build(
|
|
WorldEntity entity,
|
|
Setup setup,
|
|
IReadOnlyList<uint> effectivePartGfxObjIds,
|
|
WorldSession.EntitySpawn spawn,
|
|
LiveEntityRecord exactRecord,
|
|
Vector3 worldOrigin,
|
|
bool retainEmptyPayload = false)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
ArgumentNullException.ThrowIfNull(setup);
|
|
ArgumentNullException.ThrowIfNull(effectivePartGfxObjIds);
|
|
ArgumentNullException.ThrowIfNull(exactRecord);
|
|
if (spawn.Position is not { } position)
|
|
return null;
|
|
if (spawn.Guid != exactRecord.ServerGuid
|
|
|| spawn.InstanceSequence != exactRecord.Generation
|
|
|| !ReferenceEquals(exactRecord.WorldEntity, entity))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Live collision construction requires the exact materialized record.");
|
|
}
|
|
|
|
float scale = spawn.ObjScale ?? 1f;
|
|
IReadOnlyList<Frame>? defaultPose = _defaultPose.Resolve(
|
|
spawn.MotionTableId ?? 0u,
|
|
setup.Parts.Count);
|
|
IReadOnlyList<ShadowShape> raw = ShadowShapeBuilder.FromSetup(
|
|
setup,
|
|
scale,
|
|
_hasPhysicsBsp,
|
|
partPoseOverride: defaultPose,
|
|
effectivePartGfxObjIds: effectivePartGfxObjIds);
|
|
var shapes = new List<ShadowShape>(raw.Count);
|
|
foreach (ShadowShape shape in raw)
|
|
{
|
|
if (shape.CollisionType == ShadowCollisionType.BSP)
|
|
{
|
|
float radius = (_physicsBspRadius(shape.GfxObjId) ?? 2f) * scale;
|
|
shapes.Add(shape with { Radius = radius });
|
|
}
|
|
else
|
|
{
|
|
shapes.Add(shape);
|
|
}
|
|
}
|
|
|
|
if (shapes.Count == 0 && setup.Radius > 0.0001f)
|
|
{
|
|
shapes.Add(new ShadowShape(
|
|
GfxObjId: 0u,
|
|
LocalPosition: Vector3.Zero,
|
|
LocalRotation: Quaternion.Identity,
|
|
Scale: scale,
|
|
CollisionType: ShadowCollisionType.Cylinder,
|
|
Radius: setup.Radius * scale,
|
|
CylHeight: (setup.Height > 0f ? setup.Height : setup.Radius * 2f) * scale));
|
|
}
|
|
|
|
if (shapes.Count == 0 && !retainEmptyPayload)
|
|
return null;
|
|
|
|
EntityCollisionFlags flags = EntityCollisionFlags.HasWeenie;
|
|
if (spawn.ObjectDescriptionFlags is { } descriptionFlags)
|
|
flags |= EntityCollisionFlagsExt.FromPwdBitfield(descriptionFlags);
|
|
if (spawn.ItemType == (uint)ItemType.Creature)
|
|
flags |= EntityCollisionFlags.IsCreature;
|
|
|
|
return new LiveEntityCollisionRegistration(
|
|
entity.Id,
|
|
entity.SourceGfxObjOrSetupId,
|
|
entity.Position,
|
|
entity.Rotation,
|
|
shapes,
|
|
(uint)exactRecord.FinalPhysicsState,
|
|
flags,
|
|
worldOrigin.X,
|
|
worldOrigin.Y,
|
|
position.LandblockId,
|
|
position.LandblockId);
|
|
}
|
|
|
|
public static void Register(
|
|
ShadowObjectRegistry registry,
|
|
LiveEntityCollisionRegistration registration)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(registry);
|
|
ArgumentNullException.ThrowIfNull(registration);
|
|
registry.RegisterMultiPart(
|
|
registration.EntityId,
|
|
registration.EntityWorldPosition,
|
|
registration.EntityWorldRotation,
|
|
registration.Shapes,
|
|
registration.State,
|
|
registration.Flags,
|
|
registration.WorldOffsetX,
|
|
registration.WorldOffsetY,
|
|
registration.LandblockId,
|
|
registration.SeedCellId,
|
|
isStatic: false);
|
|
|
|
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
|
|
return;
|
|
|
|
int cylinders = 0;
|
|
int bsps = 0;
|
|
foreach (ShadowShape shape in registration.Shapes)
|
|
{
|
|
if (shape.CollisionType == ShadowCollisionType.Cylinder)
|
|
cylinders++;
|
|
else
|
|
bsps++;
|
|
}
|
|
Console.WriteLine(FormattableString.Invariant(
|
|
$"[entity-source] id=0x{registration.EntityId:X8} entityId=0x{registration.EntityId:X8} src=0x{registration.SourceId:X8} gfxObj=0x{registration.SourceId:X8} lb=0x{registration.LandblockId:X8} shapes=cyl{cylinders}+bsp{bsps} note=server-spawn-root state=0x{registration.State:X8} flags={registration.Flags}"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits a retail ObjDesc part-array replacement. A shapeless result
|
|
/// removes the prior payload; a shaped result replaces it without making
|
|
/// a Hidden, parented, or cell-less object collide in world cells.
|
|
/// </summary>
|
|
public static void ReconcileAppearance(
|
|
ShadowObjectRegistry registry,
|
|
uint entityId,
|
|
LiveEntityCollisionRegistration? registration,
|
|
bool suspendIfNew)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(registry);
|
|
if (registration is null)
|
|
return;
|
|
|
|
if (registration.EntityId != entityId)
|
|
throw new InvalidOperationException("Collision replacement belongs to a different live entity.");
|
|
|
|
registry.ReplaceMultiPartPayload(
|
|
registration.EntityId,
|
|
registration.EntityWorldPosition,
|
|
registration.EntityWorldRotation,
|
|
registration.Shapes,
|
|
registration.State,
|
|
registration.Flags,
|
|
registration.WorldOffsetX,
|
|
registration.WorldOffsetY,
|
|
registration.LandblockId,
|
|
registration.SeedCellId,
|
|
isStatic: false,
|
|
suspendIfNew);
|
|
}
|
|
}
|