107 lines
4.1 KiB
C#
107 lines
4.1 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Vfx;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Rendering.Vfx;
|
|
|
|
/// <summary>
|
|
/// Setup script/profile data resolved once when a logical effect owner is
|
|
/// registered. Part transforms are indexed and root-local.
|
|
/// </summary>
|
|
public sealed record ScriptActivationInfo(
|
|
uint ScriptId,
|
|
IReadOnlyList<Matrix4x4> PartTransforms,
|
|
EntityEffectProfile? EffectProfile = null,
|
|
IReadOnlyList<bool>? PartAvailability = null);
|
|
|
|
/// <summary>
|
|
/// Owns create-time Setup script activation and its symmetric teardown for
|
|
/// live and DAT-static entities.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Setup <c>DefaultScript</c> is a direct PES played during Setup
|
|
/// initialization. Live registration invokes this class once per logical
|
|
/// generation; spatial rebucketing never replays it.
|
|
/// </remarks>
|
|
public sealed class EntityScriptActivator
|
|
{
|
|
private readonly PhysicsScriptRunner _scriptRunner;
|
|
private readonly ParticleHookSink _particleSink;
|
|
private readonly EntityEffectPoseRegistry _poses;
|
|
private readonly Func<WorldEntity, ScriptActivationInfo?> _resolver;
|
|
private readonly Action<uint, WorldEntity, EntityEffectProfile>? _registerDatStaticEffectOwner;
|
|
private readonly Action<uint>? _unregisterDatStaticEffectOwner;
|
|
private readonly Dictionary<uint, WorldEntity> _activeStaticOwners = new();
|
|
|
|
public EntityScriptActivator(
|
|
PhysicsScriptRunner scriptRunner,
|
|
ParticleHookSink particleSink,
|
|
EntityEffectPoseRegistry poses,
|
|
Func<WorldEntity, ScriptActivationInfo?> resolver,
|
|
Action<uint, WorldEntity, EntityEffectProfile>? registerDatStaticEffectOwner = null,
|
|
Action<uint>? unregisterDatStaticEffectOwner = null)
|
|
{
|
|
_scriptRunner = scriptRunner ?? throw new ArgumentNullException(nameof(scriptRunner));
|
|
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
|
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
|
|
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
|
|
_registerDatStaticEffectOwner = registerDatStaticEffectOwner;
|
|
_unregisterDatStaticEffectOwner = unregisterDatStaticEffectOwner;
|
|
}
|
|
|
|
public void OnCreate(WorldEntity entity)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
uint key = entity.Id;
|
|
if (key == 0)
|
|
return;
|
|
|
|
if (entity.ServerGuid == 0
|
|
&& _activeStaticOwners.TryGetValue(key, out WorldEntity? existing))
|
|
{
|
|
if (ReferenceEquals(existing, entity))
|
|
return;
|
|
throw new InvalidOperationException(
|
|
$"Dat-static WorldEntity id 0x{key:X8} is already active; static allocators must be globally unique.");
|
|
}
|
|
|
|
ScriptActivationInfo? info = _resolver(entity);
|
|
if (info is null)
|
|
return;
|
|
|
|
// Particle::Init (0x0051C930) samples the current root/part frames.
|
|
// Publish before the Setup default script can execute; effects never
|
|
// fall back to the camera or a cached spawn anchor.
|
|
_poses.Publish(entity, info.PartTransforms, info.PartAvailability);
|
|
|
|
if (entity.ServerGuid == 0)
|
|
_activeStaticOwners.Add(key, entity);
|
|
|
|
if (entity.ServerGuid == 0 && info.EffectProfile is { } profile)
|
|
_registerDatStaticEffectOwner?.Invoke(key, entity, profile);
|
|
|
|
if (info.ScriptId != 0)
|
|
_scriptRunner.Play(info.ScriptId, key, entity.Position);
|
|
}
|
|
|
|
public void OnRemove(WorldEntity entity)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entity);
|
|
uint key = entity.Id;
|
|
if (key == 0)
|
|
return;
|
|
|
|
if (entity.ServerGuid == 0
|
|
&& (!_activeStaticOwners.TryGetValue(key, out WorldEntity? existing)
|
|
|| !ReferenceEquals(existing, entity)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_scriptRunner.StopAllForEntity(key);
|
|
_particleSink.StopAllForEntity(key, fadeOut: false);
|
|
_poses.Remove(key);
|
|
if (entity.ServerGuid == 0 && _activeStaticOwners.Remove(key))
|
|
_unregisterDatStaticEffectOwner?.Invoke(key);
|
|
}
|
|
}
|