using System; using System.Collections.Concurrent; using System.Numerics; using DatReaderWriter; using DatReaderWriter.Lib.IO; using AcDream.Core.Content; using DatParticleEmitter = DatReaderWriter.DBObjs.ParticleEmitter; using DatGfxObj = DatReaderWriter.DBObjs.GfxObj; using DatGfxObjDegradeInfo = DatReaderWriter.DBObjs.GfxObjDegradeInfo; using DatEmitterType = DatReaderWriter.Enums.EmitterType; using DatParticleType = DatReaderWriter.Enums.ParticleType; using DatGfxObjFlags = DatReaderWriter.Enums.GfxObjFlags; namespace AcDream.Core.Vfx; /// /// Resolves retail ParticleEmitterInfo dat records /// (0x32xxxxxx) into acdream runtime descriptors. /// public sealed class EmitterDescRegistry { private readonly Func? _resolver; private readonly Func? _degradeDistanceResolver; private readonly ConcurrentDictionary _byId = new(); public EmitterDescRegistry() : this((Func?)null) { } public EmitterDescRegistry(IDatObjectSource dats) { ArgumentNullException.ThrowIfNull(dats); _resolver = id => SafeGet(dats, id); _degradeDistanceResolver = emitter => ResolveMaxDegradeDistance(dats, emitter); } public EmitterDescRegistry(Func? resolver) { _resolver = resolver; } public void Register(EmitterDesc desc) { ArgumentNullException.ThrowIfNull(desc); _byId[desc.DatId] = desc; } public EmitterDesc Get(uint emitterId) { if (TryGet(emitterId, out EmitterDesc? desc)) return desc; throw new KeyNotFoundException( $"ParticleEmitterInfo 0x{emitterId:X8} was not found."); } public bool TryGet(uint emitterId, out EmitterDesc desc) { if (_byId.TryGetValue(emitterId, out desc!)) return true; if (_resolver is not null) { var dat = _resolver(emitterId); if (dat is not null) { float? resolvedDistance = _degradeDistanceResolver?.Invoke(dat); if (_degradeDistanceResolver is not null && !resolvedDistance.HasValue) { // ParticleEmitter::SetInfo (0x0051CE90) fails when the // authored hardware GfxObj cannot be loaded. It never // substitutes the software GfxObj. desc = null!; return false; } float maxDegradeDistance = resolvedDistance ?? RetailParticleDegradeDistance.DefaultDistance; desc = FromDat(emitterId, dat, maxDegradeDistance); _byId[emitterId] = desc; return true; } } desc = null!; return false; } public int Count => _byId.Count; public static EmitterDesc FromDat( uint emitterId, DatParticleEmitter dat, float maxDegradeDistance = RetailParticleDegradeDistance.DefaultDistance) { 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 { MaxDegradeDistance = maxDegradeDistance, 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 float? ResolveMaxDegradeDistance( IDatObjectSource dats, DatParticleEmitter emitter) { uint gfxObjId = GetRetailHardwareGfxObjId(emitter); if (gfxObjId == 0) return null; DatGfxObj? gfx = SafeGet(dats, gfxObjId); if (gfx is null) return null; if (!gfx.Flags.HasFlag(DatGfxObjFlags.HasDIDDegrade) || gfx.DIDDegrade == 0) { return RetailParticleDegradeDistance.DefaultDistance; } DatGfxObjDegradeInfo? degrade = SafeGet(dats, gfx.DIDDegrade); return RetailParticleDegradeDistance.FromEntries(degrade?.Degrades); } internal static uint GetRetailHardwareGfxObjId(DatParticleEmitter emitter) => emitter.HwGfxObjId.DataId; private static T? SafeGet(IDatObjectSource dats, uint id) where T : class, IDBObj { if (dats is null) return null; try { return dats.Get(id); } catch { return null; } } 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, }; } /// /// Exact retail maximum-distance selection for a particle GfxObj. /// CPhysicsPart::GetMaxDegradeDistance (0x0050D510) supplies the /// 100-unit default; GfxObjDegradeInfo::get_max_degrade_distance /// (0x0051E2D0) selects entry zero for one/two-entry tables and the /// second-to-last entry for larger tables. /// public static class RetailParticleDegradeDistance { public const float DefaultDistance = 100f; public static float FromEntries(IReadOnlyList? entries) { if (entries is null || entries.Count == 0) return DefaultDistance; int index = entries.Count <= 2 ? 0 : entries.Count - 2; return entries[index].MaxDist; } }