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

@ -264,6 +264,29 @@ public sealed class ParticleHookSinkTests
Assert.Contains($"0x{Owner:X8}", diagnostic, StringComparison.Ordinal);
}
[Fact]
public void MissingEmitterAsset_ReportsOnlyOnceAcrossManyOwners()
{
const uint missingId = 0x32DEAD00u;
const uint secondOwner = Owner + 1u;
var registry = new EmitterDescRegistry();
var system = new ParticleSystem(registry, new Random(42));
var poses = new MutablePoseSource();
poses.Publish(Owner, Matrix4x4.Identity, Matrix4x4.Identity);
poses.Publish(secondOwner, Matrix4x4.Identity, Matrix4x4.Identity);
var sink = new ParticleHookSink(system, poses);
var diagnostics = new List<string>();
sink.DiagnosticSink = diagnostics.Add;
sink.OnHook(Owner, Vector3.Zero, Create(missingId, logicalId: 0u));
sink.OnHook(secondOwner, Vector3.Zero, Create(missingId, logicalId: 0u));
sink.OnHook(Owner, Vector3.Zero, Create(missingId, logicalId: 0u));
Assert.Empty(system.EnumerateEmitters());
Assert.Single(diagnostics);
Assert.Contains($"0x{missingId:X8}", diagnostics[0], StringComparison.Ordinal);
}
[Fact]
public void MissingLivePose_HidesPresentationWithoutEndingEmitterLifetime()
{

View file

@ -1,6 +1,8 @@
using System.Linq;
using System.Numerics;
using AcDream.Core.Content;
using AcDream.Core.Vfx;
using DatReaderWriter.Lib.IO;
using DatReaderWriter.Types;
using Xunit;
@ -267,6 +269,35 @@ public sealed class ParticleSystemTests
Assert.Same(desc, reg.Get(0x32001234u));
}
[Fact]
public void EmitterDescRegistry_NegativeCachesMissingDatAndRegisterClearsFailure()
{
int calls = 0;
var reg = new EmitterDescRegistry(_ =>
{
calls++;
return null;
});
const uint emitterId = 0x3200DEADu;
Assert.False(reg.TryGet(emitterId, out _, out var first));
Assert.False(reg.TryGet(emitterId, out _, out var second));
Assert.Equal(1, calls);
Assert.Equal(
EmitterDescResolutionFailureKind.MissingEmitterInfo,
first.Kind);
Assert.Equal(first, second);
Assert.Equal(1, reg.FailureCount);
var desc = new EmitterDesc { DatId = emitterId };
reg.Register(desc);
Assert.True(reg.TryGet(emitterId, out var resolved, out var failure));
Assert.Same(desc, resolved);
Assert.Equal(EmitterDescResolutionFailureKind.None, failure.Kind);
Assert.Equal(0, reg.FailureCount);
}
[Fact]
public void LocalVelocity_TransformsABySpawnRotation()
{
@ -398,6 +429,31 @@ public sealed class ParticleSystemTests
Assert.Equal(0u, EmitterDescRegistry.GetRetailHardwareGfxObjId(dat));
}
[Fact]
public void EmitterDescRegistry_ClassifiesAndCachesAuthoredHardwarelessEmitter()
{
const uint emitterId = 0x320002D6u;
var dats = new FakeDatObjectSource();
dats.Add(
emitterId,
new DatReaderWriter.DBObjs.ParticleEmitter
{
GfxObjId = 0x010016C9u,
HwGfxObjId = 0u,
});
var reg = new EmitterDescRegistry(dats);
Assert.False(reg.TryGet(emitterId, out _, out var first));
Assert.False(reg.TryGet(emitterId, out _, out var second));
Assert.Equal(
EmitterDescResolutionFailureKind.InvalidHardwareGfxObjId,
first.Kind);
Assert.Equal(first, second);
Assert.Equal(1, dats.GetCallCount(emitterId));
Assert.Equal(1, reg.FailureCount);
}
[Fact]
public void RetailParticleDegradeDistance_MatchesRetailEntrySelection()
{
@ -1100,4 +1156,31 @@ public sealed class ParticleSystemTests
sys.Tick(0.01f);
Assert.Equal(2, sys.ActiveParticleCount);
}
private sealed class FakeDatObjectSource : IDatObjectSource
{
private readonly Dictionary<uint, IDBObj> _objects = new();
private readonly Dictionary<uint, int> _calls = new();
public void Add(uint id, IDBObj value) => _objects[id] = value;
public int GetCallCount(uint id) =>
_calls.TryGetValue(id, out int count) ? count : 0;
public T Get<T>(uint fileId) where T : IDBObj
{
_calls.TryGetValue(fileId, out int count);
_calls[fileId] = count + 1;
return _objects.TryGetValue(fileId, out IDBObj? value)
&& value is T typed
? typed
: default!;
}
public bool TryGet<T>(uint fileId, out T value) where T : IDBObj
{
value = Get<T>(fileId);
return value is not null;
}
}
}