using System;
using AcDream.Core.Vfx;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Vfx;
///
/// Fires Setup.DefaultScript through
/// when a server-spawned enters the world, so static
/// objects (portals, chimneys, fireplaces, building details) emit their
/// retail-faithful persistent particle effects automatically. Stops the
/// scripts and live emitters when the entity despawns.
///
///
/// Wires alongside EntitySpawnAdapter in GpuWorldState: the
/// adapter handles meshes + animation state, the activator handles scripts +
/// particles. Both are render-thread-only.
///
///
///
/// Retail oracle: play_script_internal(setup.DefaultScript) is what
/// retail's CPhysicsObj invokes at object load (see Phase C.1 plan §C.1
/// and memory/project_sky_pes_port.md). C.1 already shipped the runner;
/// this class adds the missing fire-on-spawn call site.
///
///
public sealed class EntityScriptActivator
{
private readonly PhysicsScriptRunner _scriptRunner;
private readonly ParticleHookSink _particleSink;
private readonly Func _defaultScriptResolver;
/// Already-shipped runner from C.1. Owns the
/// (scriptId, entityId) instance table and schedules hooks at their
/// StartTime offsets.
/// Already-shipped hook sink from C.1. The
/// activator only calls its
/// to drop any per-entity emitter handles on despawn.
/// Returns
/// entity.SourceGfxObjOrSetupId's Setup.DefaultScript.DataId,
/// or 0 on miss / dat throw / missing field. Production lambda hits
/// ; tests pass a hand-rolled
/// stub.
public EntityScriptActivator(
PhysicsScriptRunner scriptRunner,
ParticleHookSink particleSink,
Func defaultScriptResolver)
{
ArgumentNullException.ThrowIfNull(scriptRunner);
ArgumentNullException.ThrowIfNull(particleSink);
ArgumentNullException.ThrowIfNull(defaultScriptResolver);
_scriptRunner = scriptRunner;
_particleSink = particleSink;
_defaultScriptResolver = defaultScriptResolver;
}
///
/// Resolve the entity's Setup.DefaultScript and fire it through
/// the script runner. No-op if the entity has no DefaultScript
/// (resolver returns 0) or if the entity has no server guid
/// (atlas-tier entities are out of scope for this activator).
///
public void OnCreate(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (entity.ServerGuid == 0) return;
uint scriptId = _defaultScriptResolver(entity);
if (scriptId == 0) return;
// Seed the sink's per-entity rotation so CreateParticleHook.Offset.Origin
// (in entity-local frame) transforms correctly to world space when the
// hook fires. Without this, the sink falls through to Quaternion.Identity
// and the offset gets applied in world axes — visual symptom for portals:
// swirl oriented along world XYZ instead of the portal's facing, partially
// buried because the local-Z lift becomes a world-axis offset.
_particleSink.SetEntityRotation(entity.ServerGuid, entity.Rotation);
_scriptRunner.Play(scriptId, entity.ServerGuid, entity.Position);
}
///
/// Stop every script instance the runner is tracking for this entity, and
/// kill every live emitter the sink has attributed to it. Idempotent for
/// unknown guids (both calls no-op).
///
public void OnRemove(uint serverGuid)
{
if (serverGuid == 0) return;
_scriptRunner.StopAllForEntity(serverGuid);
_particleSink.StopAllForEntity(serverGuid, fadeOut: false);
}
}