using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Vfx;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Vfx;
///
/// What the activator's resolver returns when an entity's Setup carries
/// a DefaultScript. Bundles the script id with the per-part
/// transforms baked from Setup.PlacementFrames so a single dat
/// lookup yields both pieces of state. The activator pushes the part
/// transforms into
/// before calling , which closes
/// the part-anchor pipeline introduced for issue #56.
///
public sealed record ScriptActivationInfo(
uint ScriptId,
IReadOnlyList PartTransforms);
///
/// Fires Setup.DefaultScript through
/// when a enters the world, so static objects
/// (portals, chimneys, fireplaces, EnvCell decorations, building details)
/// emit their retail-faithful persistent particle effects automatically.
/// Stops the scripts and live emitters when the entity despawns.
///
///
/// Handles both server-spawned and dat-hydrated entities, always keyed by
/// canonical local entity.Id. The C.1.5a guard that early-returned for
/// ServerGuid == 0 was relaxed in C.1.5b so EnvCell static objects
/// (which have no server guid because they come from the dat file, not
/// the network) also fire their DefaultScript.
///
///
///
/// For live objects this is invoked by LiveEntityRuntime logical
/// registration/teardown. GpuWorldState invokes it only for dat-static
/// landblock load, promotion, demotion, and unload paths.
///
///
///
/// 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 _resolver;
private readonly HashSet _legacyPendingOwners = new();
/// 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 pushes per-entity rotation + part transforms here, and
/// calls to drop
/// per-entity emitter handles on despawn.
/// Returns
/// with the entity's
/// Setup.DefaultScript.DataId and per-part transforms (via
/// SetupPartTransforms.Compute), or null on dat miss /
/// throw / missing DefaultScript. Production lambda hits
/// DatCollection; tests pass a hand-rolled stub.
public EntityScriptActivator(
PhysicsScriptRunner scriptRunner,
ParticleHookSink particleSink,
Func resolver)
{
ArgumentNullException.ThrowIfNull(scriptRunner);
ArgumentNullException.ThrowIfNull(particleSink);
ArgumentNullException.ThrowIfNull(resolver);
_scriptRunner = scriptRunner;
_particleSink = particleSink;
_resolver = resolver;
}
///
/// Resolve the entity's Setup.DefaultScript and fire it through
/// the script runner, keyed by canonical local entity.Id.
/// No-op if the entity has no DefaultScript (resolver returns null
/// or zero-script).
///
public void OnCreate(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
uint key = entity.Id;
if (key == 0) return; // malformed entity
var info = _resolver(entity);
if (info is null || info.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. C.1.5a fix: 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.
_particleSink.SetEntityRotation(key, entity.Rotation);
// C.1.5b #56: seed the sink's per-entity part transforms so
// CreateParticleHook.PartIndex routes the hook offset through the
// right mesh part's resting transform. Without this, every emitter
// in a multi-part Setup collapses to the entity root.
_particleSink.SetEntityPartTransforms(key, info.PartTransforms);
_scriptRunner.Play(info.ScriptId, key, entity.Position);
}
///
/// Stop every script instance the runner is tracking for this key, and
/// kill every live emitter the sink has attributed to the canonical
/// local entity id. Idempotent for unknown keys.
///
public void OnRemove(uint key)
{
if (key == 0) return;
_scriptRunner.StopAllForEntity(key);
_particleSink.StopAllForEntity(key, fadeOut: false);
}
///
/// Starts the temporary server-GUID owner used when F754 precedes the
/// target's CreateObject/materialization. Step 4 replaces this with the
/// retail mixed pending-packet FIFO. Tracking is required because no
/// LiveEntityRecord may exist yet for delete/session teardown.
///
public bool PlayLegacyPending(
uint serverGuid,
uint scriptDid,
Vector3 anchorWorldPosition)
{
if (serverGuid == 0)
return false;
bool played = _scriptRunner.Play(scriptDid, serverGuid, anchorWorldPosition);
if (played)
_legacyPendingOwners.Add(serverGuid);
return played;
}
///
/// Cleans the temporary server-GUID owner used when F754 arrives before a
/// live projection has a canonical local ID. Step 4 replaces this alias
/// with the retail mixed pending-packet FIFO; until then teardown must stop
/// the alias as well as the normal local owner so an early effect cannot
/// outlive its object incarnation.
///
public void OnRemoveLegacyOwner(uint serverGuid, uint canonicalLocalId)
{
if (serverGuid == 0 || serverGuid == canonicalLocalId)
return;
if (!_legacyPendingOwners.Remove(serverGuid))
return;
OnRemove(serverGuid);
}
/// Stops every pre-Create F754 alias at session teardown.
public void ClearLegacyPendingOwners()
{
foreach (uint serverGuid in _legacyPendingOwners)
OnRemove(serverGuid);
_legacyPendingOwners.Clear();
}
public int LegacyPendingOwnerCount => _legacyPendingOwners.Count;
}