fix(vfx): classify hardwareless particle emitters once
This commit is contained in:
parent
921712f412
commit
5b3fb17775
8 changed files with 374 additions and 18 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
private readonly HashSet<uint> _examinationOwners = [];
|
||||
private readonly HashSet<uint> _hiddenPresentationOwners = [];
|
||||
private readonly HashSet<uint> _stoppingOwners = [];
|
||||
private readonly HashSet<uint> _reportedEmitterResolutionFailures = [];
|
||||
|
||||
// Production pose registries publish actual root/part/cell changes. Only
|
||||
// those owners are recomposed; static world emitters do no refresh work.
|
||||
|
|
@ -417,11 +418,16 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
partIndex,
|
||||
renderPass,
|
||||
visibilityPolicy,
|
||||
out int handle))
|
||||
out int handle,
|
||||
out EmitterDescResolutionFailure failure))
|
||||
{
|
||||
DiagnosticSink?.Invoke(
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} for owner " +
|
||||
$"0x{ownerLocalId:X8} was not found; no fallback was created.");
|
||||
if (_reportedEmitterResolutionFailures.Add(emitterInfoId))
|
||||
{
|
||||
DiagnosticSink?.Invoke(FormatEmitterResolutionDiagnostic(
|
||||
emitterInfoId,
|
||||
ownerLocalId,
|
||||
failure));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_system.UpdateEmitterOwnerPosition(handle, ownerPosition);
|
||||
|
|
@ -534,6 +540,26 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
return true;
|
||||
}
|
||||
|
||||
private static string FormatEmitterResolutionDiagnostic(
|
||||
uint emitterInfoId,
|
||||
uint ownerLocalId,
|
||||
EmitterDescResolutionFailure failure) =>
|
||||
failure.Kind switch
|
||||
{
|
||||
EmitterDescResolutionFailureKind.InvalidHardwareGfxObjId =>
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} has no retail " +
|
||||
$"hardware GfxObj; ParticleEmitter::SetInfo skipped it " +
|
||||
$"(first owner 0x{ownerLocalId:X8}).",
|
||||
EmitterDescResolutionFailureKind.MissingHardwareGfxObj =>
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} references missing " +
|
||||
$"retail hardware GfxObj 0x{failure.RelatedDatId:X8}; " +
|
||||
$"ParticleEmitter::SetInfo skipped it " +
|
||||
$"(first owner 0x{ownerLocalId:X8}).",
|
||||
_ =>
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} for owner " +
|
||||
$"0x{ownerLocalId:X8} was not found; no fallback was created.",
|
||||
};
|
||||
|
||||
private readonly record struct EmitterBinding(
|
||||
uint OwnerLocalId,
|
||||
int PartIndex,
|
||||
|
|
|
|||
|
|
@ -176,8 +176,29 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
ParticleRenderPass renderPass,
|
||||
ParticleVisibilityPolicy visibilityPolicy,
|
||||
out int handle)
|
||||
=> TrySpawnEmitterById(
|
||||
emitterId,
|
||||
anchor,
|
||||
rot,
|
||||
attachedObjectId,
|
||||
attachedPartIndex,
|
||||
renderPass,
|
||||
visibilityPolicy,
|
||||
out handle,
|
||||
out _);
|
||||
|
||||
public bool TrySpawnEmitterById(
|
||||
uint emitterId,
|
||||
Vector3 anchor,
|
||||
Quaternion? rot,
|
||||
uint attachedObjectId,
|
||||
int attachedPartIndex,
|
||||
ParticleRenderPass renderPass,
|
||||
ParticleVisibilityPolicy visibilityPolicy,
|
||||
out int handle,
|
||||
out EmitterDescResolutionFailure failure)
|
||||
{
|
||||
if (!_registry.TryGet(emitterId, out EmitterDesc? desc))
|
||||
if (!_registry.TryGet(emitterId, out EmitterDesc? desc, out failure))
|
||||
{
|
||||
handle = 0;
|
||||
return false;
|
||||
|
|
@ -191,6 +212,7 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
attachedPartIndex,
|
||||
renderPass,
|
||||
visibilityPolicy);
|
||||
failure = EmitterDescResolutionFailure.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue