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); } }