using System; using System.Collections.Concurrent; using System.Numerics; using DatReaderWriter; using DatParticleEmitter = DatReaderWriter.DBObjs.ParticleEmitter; using DatEmitterType = DatReaderWriter.Enums.EmitterType; using DatParticleType = DatReaderWriter.Enums.ParticleType; namespace AcDream.Core.Vfx; /// /// Resolves retail ParticleEmitterInfo dat records /// (0x32xxxxxx) into acdream runtime descriptors. /// public sealed class EmitterDescRegistry { private const uint FallbackEmitterId = 0xFFFFFFFFu; private readonly Func? _resolver; private readonly ConcurrentDictionary _byId = new(); public EmitterDescRegistry() : this((Func?)null) { } public EmitterDescRegistry(DatCollection dats) : this(id => SafeGet(dats, id)) { } public EmitterDescRegistry(Func? resolver) { _resolver = resolver; Register(BuildFallback()); } 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 (_resolver is not null) { var dat = _resolver(emitterId); if (dat is not null) { desc = FromDat(emitterId, dat); _byId[emitterId] = desc; return desc; } } if (_byId.TryGetValue(FallbackEmitterId, out var fallback)) return fallback; throw new InvalidOperationException("No default emitter registered in registry."); } public int Count => _byId.Count; public static EmitterDesc FromDat(uint emitterId, DatParticleEmitter dat) { ArgumentNullException.ThrowIfNull(dat); float birthrate = MathF.Max(0f, (float)dat.Birthrate); float lifespan = MathF.Max(0f, (float)dat.Lifespan); float lifespanRand = MathF.Abs((float)dat.LifespanRand); float lifetimeMin = MathF.Max(0f, lifespan - lifespanRand); float lifetimeMax = MathF.Max(lifetimeMin, lifespan + lifespanRand); // ParticleEmitterInfo has no "additive" field; retail derives blend // state from the particle GfxObj surface material. var flags = EmitterFlags.Billboard | EmitterFlags.FaceCamera; if (dat.IsParentLocal) flags |= EmitterFlags.AttachLocal; // ParticleEmitterInfo stores translucency, not opacity. Retail feeds // StartTrans/FinalTrans to PhysicsPart::SetTranslucency; the GL path // uses the complement as source alpha. float startOpacity = 1f - Math.Clamp((float)dat.StartTrans, 0f, 1f); float endOpacity = 1f - Math.Clamp((float)dat.FinalTrans, 0f, 1f); return new EmitterDesc { DatId = emitterId, Type = MapParticleType(dat.ParticleType), EmitterKind = MapEmitterKind(dat.EmitterType), Flags = flags, GfxObjId = dat.GfxObjId.DataId, HwGfxObjId = dat.HwGfxObjId.DataId, Birthrate = birthrate, EmitRate = dat.EmitterType == DatEmitterType.BirthratePerSec && birthrate > 0f ? 1f / birthrate : 0f, MaxParticles = Math.Max(1, dat.MaxParticles), InitialParticles = Math.Max(0, dat.InitialParticles), TotalParticles = Math.Max(0, dat.TotalParticles), TotalDuration = MathF.Max(0f, (float)dat.TotalSeconds), Lifespan = lifespan, LifespanRand = lifespanRand, LifetimeMin = lifetimeMin, LifetimeMax = lifetimeMax, OffsetDir = dat.OffsetDir, MinOffset = dat.MinOffset, MaxOffset = dat.MaxOffset, SpawnDiskRadius = dat.MaxOffset, InitialVelocity = dat.A, Gravity = dat.B, A = dat.A, MinA = dat.MinA, MaxA = dat.MaxA, B = dat.B, MinB = dat.MinB, MaxB = dat.MaxB, C = dat.C, MinC = dat.MinC, MaxC = dat.MaxC, StartSize = dat.StartScale, EndSize = dat.FinalScale, ScaleRand = dat.ScaleRand, StartAlpha = startOpacity, EndAlpha = endOpacity, TransRand = dat.TransRand, }; } private static DatParticleEmitter? SafeGet(DatCollection dats, uint id) { if (dats is null) return null; try { return dats.Get(id); } catch { return null; } } private static EmitterDesc BuildFallback() => new() { DatId = FallbackEmitterId, Type = ParticleType.LocalVelocity, EmitterKind = ParticleEmitterKind.BirthratePerSec, Flags = EmitterFlags.Billboard | EmitterFlags.FaceCamera, Birthrate = 0.1f, EmitRate = 10f, MaxParticles = 32, LifetimeMin = 0.6f, LifetimeMax = 1.2f, Lifespan = 0.9f, LifespanRand = 0.3f, OffsetDir = new Vector3(0, 0, 1), MinOffset = 0f, MaxOffset = 0.1f, SpawnDiskRadius = 0.1f, InitialVelocity = new Vector3(0, 0, 0.5f), VelocityJitter = 0.3f, A = new Vector3(0, 0, 0.5f), MinA = 1f, MaxA = 1f, B = Vector3.Zero, C = Vector3.Zero, StartSize = 0.25f, EndSize = 0.6f, StartAlpha = 0.85f, EndAlpha = 0f, }; private static ParticleEmitterKind MapEmitterKind(DatEmitterType type) => type switch { DatEmitterType.BirthratePerSec => ParticleEmitterKind.BirthratePerSec, DatEmitterType.BirthratePerMeter => ParticleEmitterKind.BirthratePerMeter, _ => ParticleEmitterKind.Unknown, }; private static ParticleType MapParticleType(DatParticleType type) => type switch { DatParticleType.Still => ParticleType.Still, DatParticleType.LocalVelocity => ParticleType.LocalVelocity, DatParticleType.ParabolicLVGA => ParticleType.ParabolicLVGA, DatParticleType.ParabolicLVGAGR => ParticleType.ParabolicLVGAGR, DatParticleType.Swarm => ParticleType.Swarm, DatParticleType.Explode => ParticleType.Explode, DatParticleType.Implode => ParticleType.Implode, DatParticleType.ParabolicLVLA => ParticleType.ParabolicLVLA, DatParticleType.ParabolicLVLALR => ParticleType.ParabolicLVLALR, DatParticleType.ParabolicGVGA => ParticleType.ParabolicGVGA, DatParticleType.ParabolicGVGAGR => ParticleType.ParabolicGVGAGR, DatParticleType.GlobalVelocity => ParticleType.GlobalVelocity, _ => ParticleType.Unknown, }; }