Own render, physics, DAT-static presentation, and retained static-resource rebinds behind retryable receipts so a failed publication resumes its exact unfinished suffix without replaying retail create-time defaults. Co-authored-by: Codex <noreply@openai.com>
627 lines
24 KiB
C#
627 lines
24 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Ports retail's separate <c>CPhysics::static_animating_objects</c> workset
|
|
/// (<c>CPhysics::UseTime</c> 0x00509950 and
|
|
/// <c>CPhysicsObj::animate_static_object</c> 0x00513DF0). Membership is
|
|
/// Setup/state-driven: any physics-Static object with Setup.DefaultAnimation
|
|
/// enters, whether DAT-hydrated or server-created, and leaves with its logical
|
|
/// resource lifetime. Setup default installation itself is unconditional and
|
|
/// belongs to PartArray construction; this class owns only the Static workset.
|
|
/// </summary>
|
|
internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFrameSource
|
|
{
|
|
private const double FrameEpsilon = 0.000199999995;
|
|
private const float MaximumElapsed = 2f;
|
|
|
|
private sealed class Owner
|
|
{
|
|
public required WorldEntity Entity;
|
|
public required Setup Setup;
|
|
public required AnimationSequencer? Sequencer;
|
|
public required uint[] PartGfxIds;
|
|
public required IReadOnlyDictionary<uint, uint>?[] SurfaceOverrides;
|
|
public required bool[] PartAvailable;
|
|
public PhysicsBody? Body;
|
|
public AnimationSequencer? PendingProcessHooks;
|
|
public ulong PendingResidencyVersion;
|
|
public readonly List<PartTransform> PreparedLivePartFrames = new();
|
|
public bool HasPreparedLivePartFrames;
|
|
public AnimationSequencer? PreparedFrameSequencer;
|
|
public ulong PreparedPresentationRevision;
|
|
public LiveEntityAnimationState? LiveAnimation;
|
|
public double ElapsedSinceUpdate;
|
|
public readonly Frame RootFrameScratch = new();
|
|
public readonly List<MeshRef> MeshRefs = new();
|
|
public readonly List<Matrix4x4> PartPoses = new();
|
|
public readonly List<Matrix4x4> VisualPartPoses = new();
|
|
}
|
|
|
|
private readonly IAnimationLoader _animationLoader;
|
|
private readonly Action<uint, AnimationSequencer> _captureHooks;
|
|
private readonly Action<WorldEntity, IReadOnlyList<Matrix4x4>, IReadOnlyList<bool>>
|
|
_publishPartPoses;
|
|
private readonly Func<WorldEntity, bool> _isResident;
|
|
private readonly Action<WorldEntity, PhysicsBody> _commitLiveRoot;
|
|
private readonly Func<WorldEntity, ulong> _residencyVersion;
|
|
private readonly Dictionary<uint, Owner> _owners = new();
|
|
private readonly List<Owner> _snapshot = new();
|
|
private readonly List<Owner> _hookSnapshot = new();
|
|
|
|
public RetailStaticAnimatingObjectScheduler(
|
|
IAnimationLoader animationLoader,
|
|
Action<uint, AnimationSequencer> captureHooks,
|
|
Action<WorldEntity, IReadOnlyList<Matrix4x4>, IReadOnlyList<bool>> publishPartPoses,
|
|
Func<WorldEntity, bool>? isResident = null,
|
|
Action<WorldEntity, PhysicsBody>? commitLiveRoot = null,
|
|
Func<WorldEntity, ulong>? residencyVersion = null)
|
|
{
|
|
_animationLoader = animationLoader
|
|
?? throw new ArgumentNullException(nameof(animationLoader));
|
|
_captureHooks = captureHooks
|
|
?? throw new ArgumentNullException(nameof(captureHooks));
|
|
_publishPartPoses = publishPartPoses
|
|
?? throw new ArgumentNullException(nameof(publishPartPoses));
|
|
_isResident = isResident ?? (_ => true);
|
|
_commitLiveRoot = commitLiveRoot ?? ((_, _) => { });
|
|
_residencyVersion = residencyVersion ?? (_ => 0UL);
|
|
}
|
|
|
|
internal int Count => _owners.Count;
|
|
|
|
public bool Register(WorldEntity entity, ScriptActivationInfo info)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
ArgumentNullException.ThrowIfNull(info);
|
|
if (!info.UsesStaticAnimationWorkset
|
|
|| info.Setup is not { } setup
|
|
|| info.DefaultAnimationId == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_owners.TryGetValue(entity.Id, out Owner? retained))
|
|
{
|
|
if (ReferenceEquals(retained.Entity, entity))
|
|
return true;
|
|
throw new InvalidOperationException(
|
|
$"DAT-static animation owner 0x{entity.Id:X8} is already registered.");
|
|
}
|
|
|
|
// A live CPhysicsObj already owns its canonical PartArray through
|
|
// AnimatedEntity. Resource registration occurs before that App owner
|
|
// is constructed, so retain a pending workset member and bind the
|
|
// exact sequencer later. DAT-only statics have no live owner and may
|
|
// construct their PartArray here.
|
|
AnimationSequencer? sequencer = null;
|
|
if (entity.ServerGuid == 0)
|
|
{
|
|
sequencer = new AnimationSequencer(
|
|
setup,
|
|
new MotionTable(),
|
|
_animationLoader);
|
|
if (!sequencer.HasCurrentNode)
|
|
return false;
|
|
}
|
|
|
|
int partCount = setup.Parts.Count;
|
|
uint[] gfxIds = BuildPartGfxIds(setup, entity);
|
|
bool[] available = new bool[partCount];
|
|
if (info.PartAvailability is { } supplied)
|
|
{
|
|
for (int i = 0; i < partCount && i < supplied.Count; i++)
|
|
available[i] = supplied[i];
|
|
}
|
|
var surfaces = MatchSurfaceOverrides(entity, gfxIds, available);
|
|
_owners.Add(entity.Id, new Owner
|
|
{
|
|
Entity = entity,
|
|
Setup = setup,
|
|
Sequencer = sequencer,
|
|
PartGfxIds = gfxIds,
|
|
SurfaceOverrides = surfaces,
|
|
PartAvailable = available,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rebinds the retained retail static-animation workset member to a fresh
|
|
/// landblock snapshot without reconstructing its PartArray sequencer or
|
|
/// restarting Setup.DefaultAnimation.
|
|
/// </summary>
|
|
public void Rebind(WorldEntity entity, ScriptActivationInfo info)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
ArgumentNullException.ThrowIfNull(info);
|
|
if (!_owners.TryGetValue(entity.Id, out Owner? owner))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"DAT-static animation owner 0x{entity.Id:X8} is not registered.");
|
|
}
|
|
if (ReferenceEquals(owner.Entity, entity))
|
|
return;
|
|
if (entity.ServerGuid != 0
|
|
|| !info.UsesStaticAnimationWorkset
|
|
|| info.Setup is null
|
|
|| info.DefaultAnimationId == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"DAT-static animation owner 0x{entity.Id:X8} cannot be rebound to incompatible Setup data.");
|
|
}
|
|
|
|
int partCount = owner.Setup.Parts.Count;
|
|
uint[] gfxIds = BuildPartGfxIds(owner.Setup, entity);
|
|
bool[] available = new bool[partCount];
|
|
if (info.PartAvailability is { } supplied)
|
|
{
|
|
for (int i = 0; i < partCount && i < supplied.Count; i++)
|
|
available[i] = supplied[i];
|
|
}
|
|
|
|
// Compose publishes the owner's retained lists directly. Detach the
|
|
// displaced immutable snapshot before those lists continue advancing
|
|
// under the replacement entity.
|
|
WorldEntity displaced = owner.Entity;
|
|
if (ReferenceEquals(displaced.MeshRefs, owner.MeshRefs))
|
|
displaced.MeshRefs = owner.MeshRefs.ToArray();
|
|
if (ReferenceEquals(displaced.IndexedPartTransforms, owner.PartPoses)
|
|
|| ReferenceEquals(displaced.IndexedPartAvailable, owner.PartAvailable))
|
|
{
|
|
displaced.SetIndexedPartPoses(
|
|
owner.PartPoses.ToArray(),
|
|
owner.PartAvailable.ToArray());
|
|
}
|
|
|
|
owner.Entity = entity;
|
|
owner.PartGfxIds = gfxIds;
|
|
owner.SurfaceOverrides = MatchSurfaceOverrides(entity, gfxIds, available);
|
|
owner.PartAvailable = available;
|
|
owner.HasPreparedLivePartFrames = false;
|
|
owner.PreparedLivePartFrames.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes a live Physics-Static registration with the same sequencer
|
|
/// and body owned by the canonical live entity.
|
|
/// Retail's static workset stores a <c>CPhysicsObj*</c>; it never clones a
|
|
/// second PartArray for this alternate scheduling path.
|
|
/// </summary>
|
|
public bool BindLiveOwner(
|
|
WorldEntity entity,
|
|
LiveEntityAnimationState animation,
|
|
PhysicsBody body)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
ArgumentNullException.ThrowIfNull(animation);
|
|
ArgumentNullException.ThrowIfNull(body);
|
|
if (!_owners.TryGetValue(entity.Id, out Owner? owner))
|
|
return false;
|
|
if (!ReferenceEquals(owner.Entity, entity) || entity.ServerGuid == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Static animation owner 0x{entity.Id:X8} does not match this live incarnation.");
|
|
}
|
|
if (animation.Sequencer is not { HasCurrentNode: true } sequencer)
|
|
return false;
|
|
|
|
InvalidatePending(owner);
|
|
owner.Sequencer = sequencer;
|
|
owner.LiveAnimation = animation;
|
|
owner.Body = body;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transfers the current live PartArray result to GameWindow's canonical
|
|
/// appearance composer. The scheduler owns timing; AnimatedEntity remains
|
|
/// the single owner of mesh identity, overrides, and appearance rebinding.
|
|
/// </summary>
|
|
public bool TryTakeLivePartFrames(
|
|
LiveEntityRecord record,
|
|
WorldEntity entity,
|
|
LiveEntityAnimationState animation,
|
|
ulong objectClockEpoch,
|
|
ulong projectionMutationVersion,
|
|
ulong presentationRevision,
|
|
out IReadOnlyList<PartTransform> frames)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
ArgumentNullException.ThrowIfNull(animation);
|
|
uint ownerId = entity.Id;
|
|
if (!_owners.TryGetValue(ownerId, out Owner? owner))
|
|
{
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
|
|
bool exactOwner = ReferenceEquals(record.WorldEntity, entity)
|
|
&& ReferenceEquals(record.AnimationRuntime, animation)
|
|
&& record.ObjectClockEpoch == objectClockEpoch
|
|
&& record.ProjectionMutationVersion == projectionMutationVersion
|
|
&& animation.PresentationRevision == presentationRevision
|
|
&& ReferenceEquals(owner.Entity, entity)
|
|
&& ReferenceEquals(owner.LiveAnimation, animation)
|
|
&& ReferenceEquals(owner.Sequencer, animation.Sequencer)
|
|
&& owner.Entity.ServerGuid != 0;
|
|
if (!exactOwner)
|
|
{
|
|
InvalidatePending(owner);
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
|
|
// An already-consumed frame is a normal same-phase recursive query;
|
|
// process_hooks still belongs to the outer presentation tail.
|
|
if (!owner.HasPreparedLivePartFrames)
|
|
{
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
|
|
if (!ReferenceEquals(owner.PreparedFrameSequencer, animation.Sequencer)
|
|
|| !IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
|
{
|
|
InvalidatePending(owner);
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
|
|
if (owner.PreparedPresentationRevision != presentationRevision)
|
|
{
|
|
// Appearance rebinding invalidates only the prepared visual pose.
|
|
// The exact live owner and sequencer still own process_hooks from
|
|
// this quantum, including MotionDone, so retain that semantic tail.
|
|
InvalidatePreparedFrame(owner);
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
|
|
owner.HasPreparedLivePartFrames = false;
|
|
frames = owner.PreparedLivePartFrames;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scheduler-only test seam for DAT/static timing tests which deliberately
|
|
/// do not construct a <see cref="LiveEntityRuntime"/>. Runtime integration
|
|
/// must use the exact-incarnation overload above.
|
|
/// </summary>
|
|
internal bool TryTakePreparedFramesForTest(
|
|
uint ownerId,
|
|
out IReadOnlyList<PartTransform> frames)
|
|
{
|
|
if (!_owners.TryGetValue(ownerId, out Owner? owner))
|
|
{
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
if (!owner.HasPreparedLivePartFrames)
|
|
{
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
if (!IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
|
{
|
|
InvalidatePending(owner);
|
|
frames = Array.Empty<PartTransform>();
|
|
return false;
|
|
}
|
|
owner.HasPreparedLivePartFrames = false;
|
|
frames = owner.PreparedLivePartFrames;
|
|
return true;
|
|
}
|
|
|
|
public void Unregister(uint ownerId) => _owners.Remove(ownerId);
|
|
|
|
public void CopyAnimatedEntityIdsTo(HashSet<uint> destination)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
foreach ((uint ownerId, Owner owner) in _owners)
|
|
{
|
|
if (owner.Sequencer is not null)
|
|
destination.Add(ownerId);
|
|
}
|
|
}
|
|
|
|
public void Tick(float elapsedSeconds)
|
|
{
|
|
if (!float.IsFinite(elapsedSeconds)
|
|
|| elapsedSeconds <= 0f
|
|
|| _owners.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_snapshot.Clear();
|
|
_snapshot.AddRange(_owners.Values);
|
|
foreach (Owner owner in _snapshot)
|
|
{
|
|
if (!_owners.TryGetValue(owner.Entity.Id, out Owner? current)
|
|
|| !ReferenceEquals(current, owner)
|
|
|| owner.Sequencer is not { } sequencer)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
owner.ElapsedSinceUpdate += elapsedSeconds;
|
|
if (!_isResident(owner.Entity))
|
|
{
|
|
InvalidatePending(owner);
|
|
// animate_static_object returns before touching update_time
|
|
// while cell == null. A short leave-world interval therefore
|
|
// catches up on re-entry; a long one is discarded by the
|
|
// retail two-second guard below.
|
|
continue;
|
|
}
|
|
ulong residencyVersion = _residencyVersion(owner.Entity);
|
|
|
|
double ownerElapsed = owner.ElapsedSinceUpdate;
|
|
owner.ElapsedSinceUpdate = 0d;
|
|
if (ownerElapsed <= FrameEpsilon
|
|
|| ownerElapsed > MaximumElapsed)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
IReadOnlyList<PartTransform> frames = sequencer.Advance((float)ownerElapsed);
|
|
owner.PreparedLivePartFrames.Clear();
|
|
for (int i = 0; i < frames.Count; i++)
|
|
owner.PreparedLivePartFrames.Add(frames[i]);
|
|
|
|
if (owner.Body is { } body)
|
|
{
|
|
// CPhysicsObj::animate_static_object 0x00513DF0 passes the
|
|
// stored omega vector directly to Frame::grotate after the
|
|
// PartArray update. Unlike UpdatePhysicsInternal, this odd
|
|
// static branch does not multiply omega by elapsed time.
|
|
Quaternion previousOrientation = body.Orientation;
|
|
owner.RootFrameScratch.Origin = body.Position;
|
|
owner.RootFrameScratch.Orientation = previousOrientation;
|
|
FrameOps.GRotate(owner.RootFrameScratch, body.Omega);
|
|
body.SetFrameInCurrentCell(
|
|
body.Position,
|
|
owner.RootFrameScratch.Orientation);
|
|
owner.Entity.Rotation = body.Orientation;
|
|
// animate_static_object rotates the root before
|
|
// UpdatePartsInternal/UpdateChildrenInternal. Commit every
|
|
// root consumer (collision shadows and effect anchors) at
|
|
// that same boundary; a rendered-only rotation leaves the
|
|
// canonical CPhysicsObj split across two orientations.
|
|
// The common case is a zero omega. Do not turn that no-op
|
|
// into a per-frame ShadowObjectRegistry reflood; only a real
|
|
// root change has canonical consumers to commit.
|
|
if (body.Orientation != previousOrientation)
|
|
{
|
|
_commitLiveRoot(owner.Entity, body);
|
|
if (!_owners.TryGetValue(owner.Entity.Id, out current)
|
|
|| !ReferenceEquals(current, owner)
|
|
|| !IsResidentAtVersion(owner, residencyVersion))
|
|
{
|
|
InvalidatePending(owner);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!IsResidentAtVersion(owner, residencyVersion))
|
|
{
|
|
InvalidatePending(owner);
|
|
continue;
|
|
}
|
|
|
|
if (owner.Entity.ServerGuid != 0)
|
|
{
|
|
owner.HasPreparedLivePartFrames = true;
|
|
owner.PreparedFrameSequencer = sequencer;
|
|
owner.PreparedPresentationRevision =
|
|
owner.LiveAnimation?.PresentationRevision ?? 0UL;
|
|
}
|
|
else
|
|
{
|
|
Compose(owner, owner.PreparedLivePartFrames);
|
|
_publishPartPoses(owner.Entity, owner.PartPoses, owner.PartAvailable);
|
|
if (!_owners.TryGetValue(owner.Entity.Id, out current)
|
|
|| !ReferenceEquals(current, owner)
|
|
|| !IsResidentAtVersion(owner, residencyVersion))
|
|
{
|
|
InvalidatePending(owner);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// animate_static_object runs process_hooks only after the root,
|
|
// parts, and attached children have their final transforms. Live
|
|
// part/child publication occurs outside this owner, so retain the
|
|
// exact PartArray and let GameWindow call ProcessHooks at that
|
|
// later boundary instead of completing AnimationDone here.
|
|
owner.PendingProcessHooks = sequencer;
|
|
owner.PendingResidencyVersion = residencyVersion;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs the static workset's retail <c>process_hooks</c> tail after final
|
|
/// root, part, and child pose publication. Presentation hooks remain in
|
|
/// the shared frame queue until its normal drain.
|
|
/// </summary>
|
|
public void ProcessHooks()
|
|
{
|
|
_hookSnapshot.Clear();
|
|
foreach (Owner owner in _owners.Values)
|
|
{
|
|
if (owner.PendingProcessHooks is not null)
|
|
_hookSnapshot.Add(owner);
|
|
}
|
|
|
|
foreach (Owner owner in _hookSnapshot)
|
|
{
|
|
if (!_owners.TryGetValue(owner.Entity.Id, out Owner? current)
|
|
|| !ReferenceEquals(current, owner)
|
|
|| owner.PendingProcessHooks is not { } sequencer
|
|
|| !ReferenceEquals(owner.Sequencer, sequencer)
|
|
|| !IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
|
{
|
|
InvalidatePending(owner);
|
|
continue;
|
|
}
|
|
|
|
// Clear before the callback: hook delivery may unregister or
|
|
// replace the owner, and a nested caller must not replay this tail.
|
|
owner.PendingProcessHooks = null;
|
|
_captureHooks(owner.Entity.Id, sequencer);
|
|
}
|
|
}
|
|
|
|
private bool IsResidentAtVersion(Owner owner, ulong version) =>
|
|
_isResident(owner.Entity)
|
|
&& _residencyVersion(owner.Entity) == version;
|
|
|
|
private static void InvalidatePending(Owner owner)
|
|
{
|
|
InvalidatePreparedFrame(owner);
|
|
owner.PendingProcessHooks = null;
|
|
owner.PendingResidencyVersion = 0UL;
|
|
}
|
|
|
|
private static void InvalidatePreparedFrame(Owner owner)
|
|
{
|
|
owner.PreparedLivePartFrames.Clear();
|
|
owner.HasPreparedLivePartFrames = false;
|
|
owner.PreparedFrameSequencer = null;
|
|
owner.PreparedPresentationRevision = 0UL;
|
|
}
|
|
|
|
private static void Compose(Owner owner, IReadOnlyList<PartTransform> frames)
|
|
{
|
|
EnsureRetainedPoses(owner);
|
|
owner.MeshRefs.Clear();
|
|
Matrix4x4 objectScale = owner.Entity.Scale == 1f
|
|
? Matrix4x4.Identity
|
|
: Matrix4x4.CreateScale(owner.Entity.Scale);
|
|
|
|
int partCount = owner.Setup.Parts.Count;
|
|
for (int i = 0; i < partCount; i++)
|
|
{
|
|
if (i < frames.Count)
|
|
{
|
|
Vector3 origin = frames[i].Origin;
|
|
Quaternion orientation = frames[i].Orientation;
|
|
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
|
|
? owner.Setup.DefaultScale[i]
|
|
: Vector3.One;
|
|
|
|
Matrix4x4 visual = Matrix4x4.CreateScale(defaultScale)
|
|
* Matrix4x4.CreateFromQuaternion(orientation)
|
|
* Matrix4x4.CreateTranslation(origin);
|
|
if (owner.Entity.Scale != 1f)
|
|
visual *= objectScale;
|
|
owner.VisualPartPoses[i] = visual;
|
|
owner.PartPoses[i] = Matrix4x4.CreateFromQuaternion(orientation)
|
|
* Matrix4x4.CreateTranslation(origin * owner.Entity.Scale);
|
|
}
|
|
if (!owner.PartAvailable[i])
|
|
continue;
|
|
|
|
owner.MeshRefs.Add(new MeshRef(owner.PartGfxIds[i], owner.VisualPartPoses[i])
|
|
{
|
|
SurfaceOverrides = owner.SurfaceOverrides[i],
|
|
});
|
|
}
|
|
|
|
owner.Entity.MeshRefs = owner.MeshRefs;
|
|
owner.Entity.SetIndexedPartPoses(owner.PartPoses, owner.PartAvailable);
|
|
}
|
|
|
|
private static void EnsureRetainedPoses(Owner owner)
|
|
{
|
|
int partCount = owner.Setup.Parts.Count;
|
|
if (owner.VisualPartPoses.Count == partCount
|
|
&& owner.PartPoses.Count == partCount)
|
|
{
|
|
return;
|
|
}
|
|
|
|
owner.VisualPartPoses.Clear();
|
|
owner.PartPoses.Clear();
|
|
bool[] consumed = new bool[owner.Entity.MeshRefs.Count];
|
|
for (int i = 0; i < partCount; i++)
|
|
{
|
|
Matrix4x4 rigid = i < owner.Entity.IndexedPartTransforms.Count
|
|
? owner.Entity.IndexedPartTransforms[i]
|
|
: Matrix4x4.Identity;
|
|
owner.PartPoses.Add(rigid);
|
|
|
|
Matrix4x4 visual = default;
|
|
bool found = false;
|
|
for (int meshIndex = 0; meshIndex < owner.Entity.MeshRefs.Count; meshIndex++)
|
|
{
|
|
MeshRef mesh = owner.Entity.MeshRefs[meshIndex];
|
|
if (consumed[meshIndex] || mesh.GfxObjId != owner.PartGfxIds[i])
|
|
continue;
|
|
consumed[meshIndex] = true;
|
|
visual = mesh.PartTransform;
|
|
found = true;
|
|
break;
|
|
}
|
|
if (!found)
|
|
{
|
|
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
|
|
? owner.Setup.DefaultScale[i]
|
|
: Vector3.One;
|
|
visual = Matrix4x4.CreateScale(defaultScale * owner.Entity.Scale)
|
|
* rigid;
|
|
}
|
|
owner.VisualPartPoses.Add(visual);
|
|
}
|
|
}
|
|
|
|
private static uint[] BuildPartGfxIds(Setup setup, WorldEntity entity)
|
|
{
|
|
var result = new uint[setup.Parts.Count];
|
|
for (int i = 0; i < result.Length; i++)
|
|
result[i] = (uint)setup.Parts[i];
|
|
foreach (PartOverride replacement in entity.PartOverrides)
|
|
{
|
|
if (replacement.PartIndex < result.Length)
|
|
result[replacement.PartIndex] = replacement.GfxObjId;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static IReadOnlyDictionary<uint, uint>?[] MatchSurfaceOverrides(
|
|
WorldEntity entity,
|
|
uint[] gfxIds,
|
|
bool[] available)
|
|
{
|
|
var result = new IReadOnlyDictionary<uint, uint>?[gfxIds.Length];
|
|
var consumed = new bool[entity.MeshRefs.Count];
|
|
for (int partIndex = 0; partIndex < gfxIds.Length; partIndex++)
|
|
{
|
|
for (int meshIndex = 0; meshIndex < entity.MeshRefs.Count; meshIndex++)
|
|
{
|
|
if (consumed[meshIndex]
|
|
|| entity.MeshRefs[meshIndex].GfxObjId != gfxIds[partIndex])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
consumed[meshIndex] = true;
|
|
available[partIndex] = true;
|
|
result[partIndex] = entity.MeshRefs[meshIndex].SurfaceOverrides;
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|