feat(vfx): port retail effect scheduling and delivery

This commit is contained in:
Erik 2026-07-14 09:25:44 +02:00
parent 363e046112
commit 96ddfdf175
28 changed files with 2473 additions and 691 deletions

View file

@ -18,9 +18,15 @@ public sealed class RetailDatLoaderTests
private sealed class RawDatabase : IDatDatabase
{
private readonly Dictionary<uint, byte[]> _entries = new();
private int _activeReads;
private int _maxConcurrentReads;
private int _totalReads;
public DatDatabase Db => null!;
public int Iteration => 0;
public int ReadDelayMilliseconds { get; set; }
public int MaxConcurrentReads => Volatile.Read(ref _maxConcurrentReads);
public int TotalReads => Volatile.Read(ref _totalReads);
public void Add(uint id, byte[] bytes) => _entries[id] = bytes;
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj => _entries.Keys;
public bool TryGet<T>(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj
@ -28,8 +34,28 @@ public sealed class RetailDatLoaderTests
value = default;
return false;
}
public bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value) =>
_entries.TryGetValue(fileId, out value);
public bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value)
{
Interlocked.Increment(ref _totalReads);
int active = Interlocked.Increment(ref _activeReads);
int observed;
do
{
observed = Volatile.Read(ref _maxConcurrentReads);
}
while (active > observed
&& Interlocked.CompareExchange(ref _maxConcurrentReads, active, observed) != observed);
try
{
if (ReadDelayMilliseconds > 0)
Thread.Sleep(ReadDelayMilliseconds);
return _entries.TryGetValue(fileId, out value);
}
finally
{
Interlocked.Decrement(ref _activeReads);
}
}
public bool TryGetFileBytes(uint fileId, ref byte[] bytes, out int bytesRead)
{
if (!_entries.TryGetValue(fileId, out byte[]? value))
@ -176,6 +202,47 @@ public sealed class RetailDatLoaderTests
Assert.Throws<InvalidDataException>(() => animations.LoadAnimation(wrongAnimationKey));
}
[Fact]
public async Task PhysicsScriptLoader_AllowsConcurrentFirstReads()
{
const uint firstDid = 0x33010010u;
const uint secondDid = 0x33010011u;
var portal = new RawDatabase { ReadDelayMilliseconds = 40 };
byte[] first = ProjectileVfxDatFixtures.OrdinaryPhysicsScript.ToArray();
byte[] second = ProjectileVfxDatFixtures.OrdinaryPhysicsScript.ToArray();
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(first, firstDid);
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(second, secondDid);
portal.Add(firstDid, first);
portal.Add(secondDid, second);
var loader = new RetailPhysicsScriptLoader(portal);
await Task.WhenAll(
Task.Run(() => loader.LoadPhysicsScript(firstDid)),
Task.Run(() => loader.LoadPhysicsScript(secondDid)));
Assert.Equal(2, portal.MaxConcurrentReads);
Assert.Equal(2, portal.TotalReads);
}
[Fact]
public async Task PhysicsScriptLoader_CoalescesConcurrentReadsForTheSameDid()
{
const uint scriptDid = 0x33010012u;
var portal = new RawDatabase { ReadDelayMilliseconds = 40 };
byte[] bytes = ProjectileVfxDatFixtures.OrdinaryPhysicsScript.ToArray();
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(bytes, scriptDid);
portal.Add(scriptDid, bytes);
var loader = new RetailPhysicsScriptLoader(portal);
DatPhysicsScript?[] loaded = await Task.WhenAll(
Task.Run(() => loader.LoadPhysicsScript(scriptDid)),
Task.Run(() => loader.LoadPhysicsScript(scriptDid)));
Assert.All(loaded, Assert.NotNull);
Assert.Same(loaded[0], loaded[1]);
Assert.Equal(1, portal.TotalReads);
}
[Fact]
public void Parsers_RejectTrailingOrTruncatedEntries()
{
@ -192,6 +259,17 @@ public sealed class RetailDatLoaderTests
RetailAnimationLoader.Parse(animation.Concat(new byte[] { 0 }).ToArray()));
}
[Fact]
public void PhysicsScriptParserRejectsNonFiniteHookTime()
{
byte[] script = ProjectileVfxDatFixtures.OrdinaryPhysicsScript.ToArray();
System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(
script.AsSpan(8, 8),
BitConverter.DoubleToInt64Bits(double.NaN));
Assert.Throws<InvalidDataException>(() => RetailPhysicsScriptLoader.Parse(script));
}
[Fact]
public void Parsers_RejectUnsupportedHookTypesWithoutReturningPartialObjects()
{