using System; using System.Collections.Concurrent; using System.Numerics; namespace AcDream.Core.Vfx; /// /// Resolves instances by their retail emitter /// dat id (0x32xxxxxx range). The current build of /// Chorizite.DatReaderWriter (v2.1.7) doesn't yet ship a /// ParticleEmitterInfo DBObj class, so we maintain a small /// registry of synthesized descriptors for the handful of emitters /// acdream actually needs (portal swirl, chimney smoke, fireplace /// flames, footstep dust, spell auras, weapon trails) and fall back to /// a generic "puff" for unknown ids. When a future DRW release adds /// the dat-type, this class will additionally load + cache from dats. /// /// /// Field mapping once the dat-type arrives (docs/research/deepdives/ /// r04-vfx-particles.md §1 + references/DatReaderWriter's own generated /// ParticleEmitterInfo.generated.cs): /// /// /// Birthrate1 / EmitRate (retail stores the avg /// time between spawns, not the rate). /// /// /// Lifespan ± LifespanRandLifetimeMin / LifetimeMax /// range. /// /// /// A, MinA, MaxA → primary initial velocity with magnitude /// jitter; B / C are secondary spread components. /// /// /// StartScale, FinalScale / StartTrans, FinalTrans /// interpolate linearly over life. /// /// /// /// public sealed class EmitterDescRegistry { private readonly ConcurrentDictionary _byId = new(); public EmitterDescRegistry() { // Seed with a handful of well-known AC emitter ids plus a // fallback. Ids here come from empirical ACViewer dat dumps — // see r04 §5.2 for the more complete inventory. Register(new EmitterDesc { DatId = 0xFFFFFFFFu, // "default" sentinel Type = ParticleType.LocalVelocity, Flags = EmitterFlags.Billboard | EmitterFlags.FaceCamera, EmitRate = 10f, MaxParticles = 32, LifetimeMin = 0.6f, LifetimeMax = 1.2f, OffsetDir = new Vector3(0, 0, 1), MinOffset = 0f, MaxOffset = 0.1f, SpawnDiskRadius = 0.1f, InitialVelocity = new Vector3(0, 0, 0.5f), VelocityJitter = 0.3f, StartSize = 0.25f, EndSize = 0.6f, StartAlpha = 0.85f, EndAlpha = 0f, }); } public void Register(EmitterDesc desc) { ArgumentNullException.ThrowIfNull(desc); _byId[desc.DatId] = desc; } public EmitterDesc Get(uint emitterId) { if (_byId.TryGetValue(emitterId, out var desc)) return desc; if (_byId.TryGetValue(0xFFFFFFFFu, out var fallback)) return fallback; throw new InvalidOperationException("No default emitter registered in registry."); } public int Count => _byId.Count; }