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,
};
}