322 lines
9.4 KiB
C#
322 lines
9.4 KiB
C#
using System.Collections.Immutable;
|
|
using AcDream.Content;
|
|
using AcDream.Headless.Configuration;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.Headless.Hosting;
|
|
|
|
internal readonly record struct HeadlessProcessContentSnapshot(
|
|
int LeaseCount,
|
|
bool IsDisposeRequested,
|
|
bool IsDisposed,
|
|
long MappedVirtualBytes)
|
|
{
|
|
internal bool IsConverged =>
|
|
IsDisposed
|
|
&& LeaseCount == 0
|
|
&& MappedVirtualBytes == 0L;
|
|
}
|
|
|
|
internal interface IHeadlessProcessContentFactory
|
|
{
|
|
HeadlessOpenedProcessContent Open(
|
|
HeadlessContentDescriptor descriptor,
|
|
Action<string> diagnostic);
|
|
}
|
|
|
|
internal sealed record HeadlessOpenedProcessContent(
|
|
IDatReaderWriter Dats,
|
|
IPreparedAssetSource Prepared,
|
|
MagicCatalog Magic,
|
|
ImmutableArray<float> HeightTable);
|
|
|
|
internal sealed class ProductionHeadlessProcessContentFactory
|
|
: IHeadlessProcessContentFactory
|
|
{
|
|
public HeadlessOpenedProcessContent Open(
|
|
HeadlessContentDescriptor descriptor,
|
|
Action<string> diagnostic)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(descriptor);
|
|
ArgumentNullException.ThrowIfNull(diagnostic);
|
|
|
|
string datDirectory = Path.GetFullPath(descriptor.DatDirectory);
|
|
string preparedAssetPath =
|
|
Path.GetFullPath(descriptor.PreparedAssetPath);
|
|
IDatReaderWriter? dats = null;
|
|
IPreparedAssetSource? prepared = null;
|
|
try
|
|
{
|
|
dats = RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
|
prepared = new PakPreparedAssetSource(
|
|
preparedAssetPath,
|
|
dats,
|
|
diagnostic);
|
|
MagicCatalog magic = MagicCatalog.Load(dats);
|
|
Region region = dats.Get<Region>(0x13000000u)
|
|
?? throw new InvalidOperationException(
|
|
"Region dat id 0x13000000 is missing.");
|
|
float[]? sourceHeightTable =
|
|
region.LandDefs.LandHeightTable;
|
|
if (sourceHeightTable is null
|
|
|| sourceHeightTable.Length < 256)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Region.LandDefs.LandHeightTable is missing or truncated.");
|
|
}
|
|
ImmutableArray<float> heightTable =
|
|
ImmutableArray.CreateRange(sourceHeightTable);
|
|
return new HeadlessOpenedProcessContent(
|
|
dats,
|
|
prepared,
|
|
magic,
|
|
heightTable);
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
prepared?.Dispose();
|
|
}
|
|
finally
|
|
{
|
|
dats?.Dispose();
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process lifetime root for immutable/read-only content shared by all
|
|
/// headless sessions. A lease carries no mutable session state. Disposal is
|
|
/// requested once and closes the package/DAT handles only after the final
|
|
/// session releases its lease.
|
|
/// </summary>
|
|
internal sealed class HeadlessProcessContentOwner : IDisposable
|
|
{
|
|
private readonly object _gate = new();
|
|
private readonly MagicCatalog _magic;
|
|
private readonly ImmutableArray<float> _heightTable;
|
|
private IDatReaderWriter? _dats;
|
|
private IPreparedAssetSource? _prepared;
|
|
private SharedPreparedCollisionCache? _collision;
|
|
private int _leaseCount;
|
|
private bool _disposeRequested;
|
|
private bool _disposed;
|
|
|
|
internal HeadlessProcessContentOwner(
|
|
HeadlessContentDescriptor descriptor,
|
|
Action<string> diagnostic,
|
|
IHeadlessProcessContentFactory? factory = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(descriptor);
|
|
ArgumentNullException.ThrowIfNull(diagnostic);
|
|
HeadlessOpenedProcessContent? opened =
|
|
(factory ?? new ProductionHeadlessProcessContentFactory())
|
|
.Open(descriptor, diagnostic);
|
|
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;
|
|
ImmutableArray<float> heightTable = opened.HeightTable;
|
|
bool incomplete =
|
|
dats is null
|
|
|| prepared is null
|
|
|| magic is null
|
|
|| heightTable.IsDefaultOrEmpty
|
|
|| heightTable.Length < 256;
|
|
if (incomplete || prepared is not IPreparedCollisionSource)
|
|
{
|
|
try
|
|
{
|
|
prepared?.Dispose();
|
|
}
|
|
finally
|
|
{
|
|
dats?.Dispose();
|
|
}
|
|
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!;
|
|
_heightTable = heightTable;
|
|
_collision = new SharedPreparedCollisionCache(
|
|
(IPreparedCollisionSource)prepared!);
|
|
}
|
|
|
|
internal HeadlessProcessContentLease AcquireLease(string sessionId)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
|
lock (_gate)
|
|
{
|
|
ObjectDisposedException.ThrowIf(
|
|
_disposeRequested || _disposed,
|
|
this);
|
|
checked
|
|
{
|
|
_leaseCount++;
|
|
}
|
|
return new HeadlessProcessContentLease(
|
|
this,
|
|
sessionId,
|
|
_dats!,
|
|
_prepared!,
|
|
_collision!,
|
|
_magic,
|
|
_heightTable);
|
|
}
|
|
}
|
|
|
|
internal HeadlessProcessContentSnapshot CaptureSnapshot()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return new HeadlessProcessContentSnapshot(
|
|
_leaseCount,
|
|
_disposeRequested,
|
|
_disposed,
|
|
_prepared?.MappedVirtualBytes ?? 0L);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
_disposeRequested = true;
|
|
if (_leaseCount != 0)
|
|
return;
|
|
DrainResources();
|
|
}
|
|
}
|
|
|
|
private void Release()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_leaseCount <= 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Headless content lease count underflow.");
|
|
}
|
|
_leaseCount--;
|
|
if (_leaseCount == 0 && _disposeRequested)
|
|
DrainResources();
|
|
}
|
|
}
|
|
|
|
private void DrainResources()
|
|
{
|
|
if (_collision is not null)
|
|
{
|
|
_collision.Dispose();
|
|
_collision = null;
|
|
}
|
|
if (_prepared is not null)
|
|
{
|
|
_prepared.Dispose();
|
|
_prepared = null;
|
|
}
|
|
if (_dats is not null)
|
|
{
|
|
_dats.Dispose();
|
|
_dats = null;
|
|
}
|
|
_disposed = true;
|
|
}
|
|
|
|
internal sealed class HeadlessProcessContentLease : IDisposable
|
|
{
|
|
private HeadlessProcessContentOwner? _owner;
|
|
private readonly IDatReaderWriter _dats;
|
|
private readonly IPreparedAssetSource _prepared;
|
|
private readonly IPreparedCollisionSource _collision;
|
|
private readonly MagicCatalog _magic;
|
|
private readonly ImmutableArray<float> _heightTable;
|
|
|
|
internal HeadlessProcessContentLease(
|
|
HeadlessProcessContentOwner owner,
|
|
string sessionId,
|
|
IDatReaderWriter dats,
|
|
IPreparedAssetSource prepared,
|
|
IPreparedCollisionSource collision,
|
|
MagicCatalog magic,
|
|
ImmutableArray<float> heightTable)
|
|
{
|
|
_owner = owner;
|
|
SessionId = sessionId;
|
|
_dats = dats;
|
|
_prepared = prepared;
|
|
_collision = collision;
|
|
_magic = magic;
|
|
_heightTable = heightTable;
|
|
}
|
|
|
|
internal string SessionId { get; }
|
|
|
|
internal IDatReaderWriter Dats
|
|
{
|
|
get
|
|
{
|
|
ObjectDisposedException.ThrowIf(_owner is null, this);
|
|
return _dats;
|
|
}
|
|
}
|
|
|
|
internal IPreparedAssetSource PreparedAssets
|
|
{
|
|
get
|
|
{
|
|
ObjectDisposedException.ThrowIf(_owner is null, this);
|
|
return _prepared;
|
|
}
|
|
}
|
|
|
|
internal IPreparedCollisionSource PreparedCollision
|
|
{
|
|
get
|
|
{
|
|
ObjectDisposedException.ThrowIf(_owner is null, this);
|
|
return _collision;
|
|
}
|
|
}
|
|
|
|
internal MagicCatalog MagicCatalog
|
|
{
|
|
get
|
|
{
|
|
ObjectDisposedException.ThrowIf(_owner is null, this);
|
|
return _magic;
|
|
}
|
|
}
|
|
|
|
internal ImmutableArray<float> HeightTable
|
|
{
|
|
get
|
|
{
|
|
ObjectDisposedException.ThrowIf(_owner is null, this);
|
|
return _heightTable;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
HeadlessProcessContentOwner? owner =
|
|
Interlocked.Exchange(ref _owner, null);
|
|
owner?.Release();
|
|
}
|
|
}
|
|
}
|