refactor(world): extract live projection mechanics
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>
This commit is contained in:
parent
c87bac31a0
commit
69a2ca0c6d
26 changed files with 4172 additions and 430 deletions
231
src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs
Normal file
231
src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue