feat(headless): share immutable gameplay content

This commit is contained in:
Erik 2026-07-27 09:00:48 +02:00
parent 12b500d383
commit 9569dadb57
25 changed files with 1031 additions and 429 deletions

View file

@ -4,6 +4,7 @@ using AcDream.App.Composition;
using AcDream.App.Diagnostics;
using AcDream.App.Rendering;
using AcDream.App.Spells;
using AcDream.Content;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.World;

View file

@ -1,5 +1,6 @@
using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.Content;
using AcDream.Content.Vfx;
using AcDream.Core.World;
using DatReaderWriter;

View file

@ -2,6 +2,7 @@ using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Selection;
using AcDream.App.UI;
using AcDream.Content;
using AcDream.Core.Textures;
using AcDream.Core.Selection;
using DatReaderWriter;

View file

@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.App.Spells;
using AcDream.Content;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Combat;

View file

@ -1,4 +1,5 @@
using AcDream.App.Spells;
using AcDream.Content;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;

View file

@ -0,0 +1,143 @@
using System.Collections.Immutable;
using AcDream.Content.Pak;
using AcDream.Core.Physics;
namespace AcDream.Content.Tests;
public sealed class SharedPreparedCollisionCacheTests
{
[Fact]
public void LoadedPayloadIsSharedByReferenceAndLeastRecentlyUsedIsBounded()
{
var source = new FixtureSource();
using var cache = new SharedPreparedCollisionCache(
source,
capacity: 2);
FlatSetupCollision first =
Assert.IsType<FlatSetupCollision>(
cache.ReadSetupCollision(1u).Data);
Assert.Same(
first,
cache.ReadSetupCollision(1u).Data);
_ = cache.ReadSetupCollision(2u);
Assert.Same(
first,
cache.ReadSetupCollision(1u).Data);
_ = cache.ReadSetupCollision(3u);
SharedPreparedCollisionCacheSnapshot snapshot =
cache.CaptureSnapshot();
Assert.Equal(2, snapshot.Count);
Assert.True(snapshot.IsBounded);
Assert.Equal(2, snapshot.Hits);
Assert.Equal(3, snapshot.Misses);
Assert.Equal(1, source.SetupReads[1u]);
Assert.Equal(1, source.SetupReads[2u]);
_ = cache.ReadSetupCollision(2u);
Assert.Equal(2, source.SetupReads[2u]);
Assert.True(cache.CaptureSnapshot().IsBounded);
}
[Fact]
public void TypeParticipatesInIdentityAndDisposeDoesNotOwnPackage()
{
var source = new FixtureSource();
var cache = new SharedPreparedCollisionCache(source, capacity: 4);
FlatSetupCollision setup =
Assert.IsType<FlatSetupCollision>(
cache.ReadSetupCollision(7u).Data);
FlatGfxObjCollisionAsset gfx =
Assert.IsType<FlatGfxObjCollisionAsset>(
cache.ReadGfxObjCollision(7u).Data);
Assert.NotSame(setup, gfx);
Assert.Equal(1, source.SetupReads[7u]);
Assert.Equal(1, source.GfxReads[7u]);
cache.Dispose();
Assert.False(source.IsDisposed);
Assert.True(cache.CaptureSnapshot().IsDisposed);
Assert.Throws<ObjectDisposedException>(
() => cache.ReadSetupCollision(7u));
}
private sealed class FixtureSource : IPreparedCollisionSource
{
internal Dictionary<uint, int> SetupReads { get; } = new();
internal Dictionary<uint, int> GfxReads { get; } = new();
internal bool IsDisposed { get; private set; }
public PreparedCollisionSourceStats CollisionStats => default;
public PreparedAssetPresence ProbeCollision(
PakAssetType type,
uint sourceFileId) =>
PreparedAssetPresence.Available;
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Increment(GfxReads, sourceFileId);
return PreparedCollisionReadResult<
FlatGfxObjCollisionAsset>.Loaded(
new FlatGfxObjCollisionAsset(
EmptyPhysicsBsp(),
null,
null));
}
public PreparedCollisionReadResult<FlatSetupCollision>
ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Increment(SetupReads, sourceFileId);
return PreparedCollisionReadResult<FlatSetupCollision>.Loaded(
new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
ImmutableArray<FlatCollisionSphere>.Empty,
0f,
0f,
0f,
0f));
}
public PreparedCollisionReadResult<
FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<
FlatCellStructureCollisionAsset>.Missing;
public PreparedCollisionReadResult<FlatEnvCellTopology>
ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatEnvCellTopology>.Missing;
public void Dispose() => IsDisposed = true;
private static FlatPhysicsBsp EmptyPhysicsBsp() => new(
-1,
ImmutableArray<FlatPhysicsBspNode>.Empty,
ImmutableArray<int>.Empty,
FlatPolygonTable.Empty);
private static void Increment(
Dictionary<uint, int> counts,
uint id) =>
counts[id] = counts.TryGetValue(id, out int count)
? count + 1
: 1;
}
}

View file

@ -1,4 +1,4 @@
using AcDream.App.Spells;
using AcDream.Content;
using AcDream.Core.Spells;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
@ -9,7 +9,7 @@ using DatMagicSchool = DatReaderWriter.Enums.MagicSchool;
using DatSpellCategory = DatReaderWriter.Enums.SpellCategory;
using CoreSpellTable = AcDream.Core.Spells.SpellTable;
namespace AcDream.App.Tests.Spells;
namespace AcDream.Content.Tests.Spells;
public sealed class RetailSpellMetadataProjectorTests
{
@ -79,7 +79,8 @@ public sealed class RetailSpellMetadataProjectorTests
string? csvPath = ResolveRepoFile("docs", "research", "data", "spells.csv");
if (datDir is null || csvPath is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
using var rawDats = new DatCollection(datDir, DatAccessType.Read);
using var dats = new DatCollectionAdapter(rawDats);
MagicCatalog catalog = MagicCatalog.Load(dats);
CoreSpellTable legacy = CoreSpellTable.LoadFromCsv(csvPath);
@ -106,7 +107,8 @@ public sealed class RetailSpellMetadataProjectorTests
string? csvPath = ResolveRepoFile("docs", "research", "data", "spells.csv");
if (datDir is null || csvPath is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
using var rawDats = new DatCollection(datDir, DatAccessType.Read);
using var dats = new DatCollectionAdapter(rawDats);
MagicCatalog catalog = MagicCatalog.Load(dats);
CoreSpellTable legacy = CoreSpellTable.LoadFromCsv(csvPath);
var mismatches = new List<string>();

View file

@ -1,8 +1,7 @@
using AcDream.App.Spells;
using AcDream.Core.Items;
using AcDream.Core.Spells;
namespace AcDream.App.Tests.Spells;
namespace AcDream.Core.Tests.Spells;
public sealed class SpellComponentRequirementServiceTests
{

View file

@ -111,6 +111,9 @@ public sealed class HeadlessProcessContentOwnerTests
Assert.Equal(1, factory.OpenCount);
Assert.Equal(sessionCount, host.Sessions.Count);
Assert.Equal(sessionCount, host.Content?.LeaseCount);
IPreparedCollisionSource sharedCollision =
Assert.IsAssignableFrom<IPreparedCollisionSource>(
host.Sessions[0].Content?.PreparedCollision);
Assert.All(
host.Sessions,
session =>
@ -121,6 +124,12 @@ public sealed class HeadlessProcessContentOwnerTests
Assert.Same(
factory.PreparedResource,
session.Content?.PreparedAssets);
Assert.Same(
MagicCatalog.Empty,
session.Content?.MagicCatalog);
Assert.Same(
sharedCollision,
session.Content?.PreparedCollision);
});
Assert.Equal(
sessionCount,
@ -206,12 +215,15 @@ public sealed class HeadlessProcessContentOwnerTests
internal TestResourceProxy Dats { get; }
internal TestResourceProxy Prepared { get; }
public (IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
public HeadlessOpenedProcessContent Open(
HeadlessContentDescriptor descriptor,
Action<string> diagnostic)
{
OpenCount++;
return (DatsResource, PreparedResource);
return new HeadlessOpenedProcessContent(
DatsResource,
PreparedResource,
MagicCatalog.Empty);
}
}
}