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

@ -17,15 +17,20 @@ internal readonly record struct HeadlessProcessContentSnapshot(
internal interface IHeadlessProcessContentFactory
{
(IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
HeadlessOpenedProcessContent Open(
HeadlessContentDescriptor descriptor,
Action<string> diagnostic);
}
internal sealed record HeadlessOpenedProcessContent(
IDatReaderWriter Dats,
IPreparedAssetSource Prepared,
MagicCatalog Magic);
internal sealed class ProductionHeadlessProcessContentFactory
: IHeadlessProcessContentFactory
{
public (IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
public HeadlessOpenedProcessContent Open(
HeadlessContentDescriptor descriptor,
Action<string> diagnostic)
{
@ -36,18 +41,30 @@ internal sealed class ProductionHeadlessProcessContentFactory
string preparedAssetPath =
Path.GetFullPath(descriptor.PreparedAssetPath);
IDatReaderWriter? dats = null;
IPreparedAssetSource? prepared = null;
try
{
dats = RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
var prepared = new PakPreparedAssetSource(
prepared = new PakPreparedAssetSource(
preparedAssetPath,
dats,
diagnostic);
return (dats, prepared);
MagicCatalog magic = MagicCatalog.Load(dats);
return new HeadlessOpenedProcessContent(
dats,
prepared,
magic);
}
catch
{
dats?.Dispose();
try
{
prepared?.Dispose();
}
finally
{
dats?.Dispose();
}
throw;
}
}
@ -62,8 +79,10 @@ internal sealed class ProductionHeadlessProcessContentFactory
internal sealed class HeadlessProcessContentOwner : IDisposable
{
private readonly object _gate = new();
private readonly MagicCatalog _magic;
private IDatReaderWriter? _dats;
private IPreparedAssetSource? _prepared;
private SharedPreparedCollisionCache? _collision;
private int _leaseCount;
private bool _disposeRequested;
private bool _disposed;
@ -75,30 +94,41 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
{
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentNullException.ThrowIfNull(diagnostic);
(IDatReaderWriter dats, IPreparedAssetSource prepared) =
HeadlessOpenedProcessContent? opened =
(factory ?? new ProductionHeadlessProcessContentFactory())
.Open(descriptor, diagnostic);
_dats = dats
?? throw new InvalidOperationException(
"The headless content factory returned no DAT owner.");
_prepared = prepared
?? throw new InvalidOperationException(
"The headless content factory returned no prepared source.");
if (prepared is not IPreparedCollisionSource)
if (opened is null)
{
throw new InvalidOperationException(
"The headless content factory returned no content owner.");
}
IDatReaderWriter? dats = opened.Dats;
IPreparedAssetSource? prepared = opened.Prepared;
MagicCatalog? magic = opened.Magic;
bool incomplete =
dats is null || prepared is null || magic is null;
if (incomplete || prepared is not IPreparedCollisionSource)
{
try
{
prepared.Dispose();
prepared?.Dispose();
}
finally
{
dats.Dispose();
dats?.Dispose();
}
_prepared = null;
_dats = null;
throw new NotSupportedException(
"Headless production content must expose prepared collision.");
throw !incomplete
&& prepared is not IPreparedCollisionSource
? new NotSupportedException(
"Headless production content must expose prepared collision.")
: new InvalidOperationException(
"The headless content factory returned an incomplete owner.");
}
_dats = dats!;
_prepared = prepared!;
_magic = magic!;
_collision = new SharedPreparedCollisionCache(
(IPreparedCollisionSource)prepared!);
}
internal HeadlessProcessContentLease AcquireLease(string sessionId)
@ -117,7 +147,9 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
this,
sessionId,
_dats!,
_prepared!);
_prepared!,
_collision!,
_magic);
}
}
@ -163,6 +195,11 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
private void DrainResources()
{
if (_collision is not null)
{
_collision.Dispose();
_collision = null;
}
if (_prepared is not null)
{
_prepared.Dispose();
@ -181,17 +218,23 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
private HeadlessProcessContentOwner? _owner;
private readonly IDatReaderWriter _dats;
private readonly IPreparedAssetSource _prepared;
private readonly IPreparedCollisionSource _collision;
private readonly MagicCatalog _magic;
internal HeadlessProcessContentLease(
HeadlessProcessContentOwner owner,
string sessionId,
IDatReaderWriter dats,
IPreparedAssetSource prepared)
IPreparedAssetSource prepared,
IPreparedCollisionSource collision,
MagicCatalog magic)
{
_owner = owner;
SessionId = sessionId;
_dats = dats;
_prepared = prepared;
_collision = collision;
_magic = magic;
}
internal string SessionId { get; }
@ -214,8 +257,23 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
}
}
internal IPreparedCollisionSource PreparedCollision =>
(IPreparedCollisionSource)PreparedAssets;
internal IPreparedCollisionSource PreparedCollision
{
get
{
ObjectDisposedException.ThrowIf(_owner is null, this);
return _collision;
}
}
internal MagicCatalog MagicCatalog
{
get
{
ObjectDisposedException.ThrowIf(_owner is null, this);
return _magic;
}
}
public void Dispose()
{