fix(vfx): classify hardwareless particle emitters once

This commit is contained in:
Erik 2026-07-27 00:03:31 +02:00
parent 921712f412
commit 5b3fb17775
8 changed files with 374 additions and 18 deletions

View file

@ -13,6 +13,21 @@ using DatGfxObjFlags = DatReaderWriter.Enums.GfxObjFlags;
namespace AcDream.Core.Vfx;
public enum EmitterDescResolutionFailureKind
{
None,
MissingEmitterInfo,
InvalidHardwareGfxObjId,
MissingHardwareGfxObj,
}
public readonly record struct EmitterDescResolutionFailure(
EmitterDescResolutionFailureKind Kind,
uint RelatedDatId = 0u)
{
public static EmitterDescResolutionFailure None => default;
}
/// <summary>
/// Resolves retail <c>ParticleEmitterInfo</c> dat records
/// (<c>0x32xxxxxx</c>) into acdream runtime descriptors.
@ -20,8 +35,11 @@ namespace AcDream.Core.Vfx;
public sealed class EmitterDescRegistry
{
private readonly Func<uint, DatParticleEmitter?>? _resolver;
private readonly Func<DatParticleEmitter, float?>? _degradeDistanceResolver;
private readonly Func<DatParticleEmitter, EmitterDatResolution>?
_degradeDistanceResolver;
private readonly ConcurrentDictionary<uint, EmitterDesc> _byId = new();
private readonly ConcurrentDictionary<uint, EmitterDescResolutionFailure>
_failures = new();
public EmitterDescRegistry()
: this((Func<uint, DatParticleEmitter?>?)null)
@ -44,6 +62,7 @@ public sealed class EmitterDescRegistry
{
ArgumentNullException.ThrowIfNull(desc);
_byId[desc.DatId] = desc;
_failures.TryRemove(desc.DatId, out _);
}
public EmitterDesc Get(uint emitterId)
@ -55,35 +74,59 @@ public sealed class EmitterDescRegistry
}
public bool TryGet(uint emitterId, out EmitterDesc desc)
=> TryGet(emitterId, out desc, out _);
public bool TryGet(
uint emitterId,
out EmitterDesc desc,
out EmitterDescResolutionFailure failure)
{
if (_byId.TryGetValue(emitterId, out desc!))
{
failure = EmitterDescResolutionFailure.None;
return true;
}
if (_failures.TryGetValue(emitterId, out failure))
{
desc = null!;
return false;
}
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)
EmitterDatResolution resolved = _degradeDistanceResolver?.Invoke(dat)
?? EmitterDatResolution.Success(
RetailParticleDegradeDistance.DefaultDistance);
if (!resolved.Succeeded)
{
// ParticleEmitter::SetInfo (0x0051CE90) fails when the
// authored hardware GfxObj cannot be loaded. It never
// substitutes the software GfxObj.
failure = resolved.Failure;
_failures.TryAdd(emitterId, failure);
desc = null!;
return false;
}
float maxDegradeDistance = resolvedDistance
?? RetailParticleDegradeDistance.DefaultDistance;
desc = FromDat(emitterId, dat, maxDegradeDistance);
desc = FromDat(emitterId, dat, resolved.MaxDegradeDistance);
_byId[emitterId] = desc;
failure = EmitterDescResolutionFailure.None;
return true;
}
}
failure = new EmitterDescResolutionFailure(
EmitterDescResolutionFailureKind.MissingEmitterInfo,
emitterId);
_failures.TryAdd(emitterId, failure);
desc = null!;
return false;
}
public int Count => _byId.Count;
public int FailureCount => _failures.Count;
public static EmitterDesc FromDat(
uint emitterId,
@ -155,25 +198,34 @@ public sealed class EmitterDescRegistry
};
}
private static float? ResolveMaxDegradeDistance(
private static EmitterDatResolution ResolveMaxDegradeDistance(
IDatObjectSource dats,
DatParticleEmitter emitter)
{
uint gfxObjId = GetRetailHardwareGfxObjId(emitter);
if (gfxObjId == 0)
return null;
{
return EmitterDatResolution.Fail(
EmitterDescResolutionFailureKind.InvalidHardwareGfxObjId);
}
DatGfxObj? gfx = SafeGet<DatGfxObj>(dats, gfxObjId);
if (gfx is null)
return null;
{
return EmitterDatResolution.Fail(
EmitterDescResolutionFailureKind.MissingHardwareGfxObj,
gfxObjId);
}
if (!gfx.Flags.HasFlag(DatGfxObjFlags.HasDIDDegrade)
|| gfx.DIDDegrade == 0)
{
return RetailParticleDegradeDistance.DefaultDistance;
return EmitterDatResolution.Success(
RetailParticleDegradeDistance.DefaultDistance);
}
DatGfxObjDegradeInfo? degrade = SafeGet<DatGfxObjDegradeInfo>(dats, gfx.DIDDegrade);
return RetailParticleDegradeDistance.FromEntries(degrade?.Degrades);
return EmitterDatResolution.Success(
RetailParticleDegradeDistance.FromEntries(degrade?.Degrades));
}
internal static uint GetRetailHardwareGfxObjId(DatParticleEmitter emitter)
@ -216,6 +268,26 @@ public sealed class EmitterDescRegistry
DatParticleType.GlobalVelocity => ParticleType.GlobalVelocity,
_ => ParticleType.Unknown,
};
private readonly record struct EmitterDatResolution(
bool Succeeded,
float MaxDegradeDistance,
EmitterDescResolutionFailure Failure)
{
public static EmitterDatResolution Success(float maxDegradeDistance) =>
new(
true,
maxDegradeDistance,
EmitterDescResolutionFailure.None);
public static EmitterDatResolution Fail(
EmitterDescResolutionFailureKind kind,
uint relatedDatId = 0u) =>
new(
false,
0f,
new EmitterDescResolutionFailure(kind, relatedDatId));
}
}
/// <summary>