using System.Numerics;
using AcDream.Core.Vfx;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Vfx;
///
/// Setup script/profile data resolved once when a logical effect owner is
/// registered. Part transforms are indexed and root-local.
///
public sealed record ScriptActivationInfo(
uint ScriptId,
IReadOnlyList PartTransforms,
EntityEffectProfile? EffectProfile = null,
IReadOnlyList? PartAvailability = null);
///
/// Owns create-time Setup script activation and its symmetric teardown for
/// live and DAT-static entities.
///
///
/// Setup DefaultScript is a direct PES played during Setup
/// initialization. Live registration invokes this class once per logical
/// generation; spatial rebucketing never replays it.
///
public sealed class EntityScriptActivator
{
private readonly PhysicsScriptRunner _scriptRunner;
private readonly ParticleHookSink _particleSink;
private readonly EntityEffectPoseRegistry _poses;
private readonly Func _resolver;
private readonly Action? _registerDatStaticEffectOwner;
private readonly Action? _unregisterDatStaticEffectOwner;
private readonly Dictionary _activeStaticOwners = new();
public EntityScriptActivator(
PhysicsScriptRunner scriptRunner,
ParticleHookSink particleSink,
EntityEffectPoseRegistry poses,
Func resolver,
Action? registerDatStaticEffectOwner = null,
Action? 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);
}
}