feat(headless): share immutable process content

This commit is contained in:
Erik 2026-07-27 08:37:24 +02:00
parent fbdb58a962
commit 12b500d383
11 changed files with 617 additions and 18 deletions

View file

@ -117,8 +117,10 @@ public sealed class DatCollectionAdapter : IDatReaderWriter {
public int LanguageIteration => _language.Iteration;
public bool TryGetFileBytes(uint regionId, uint fileId, ref byte[] bytes, out int bytesRead) {
// Route to cell db (the only region we expose)
return _dats.Cell.TryGetFileBytes(fileId, ref bytes, out bytesRead);
// Route through the wrapper's serialized raw-read path. The process
// content owner is shared by headless sessions, while DatDatabase's
// seek/read cursor is not a per-caller resource.
return _cell.TryGetFileBytes(fileId, ref bytes, out bytesRead);
}
/// <summary>

View file

@ -0,0 +1,105 @@
using DatReaderWriter;
using DatReaderWriter.Options;
using DatReaderWriter.Lib.IO;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
namespace AcDream.Content;
/// <summary>
/// Opens the read-only DAT collection owned for the lifetime of one client
/// process.
/// </summary>
/// <remarks>
/// DatReaderWriter's default <see cref="FileCachingStrategy.OnDemand"/> retains
/// every unpacked file entry for the lifetime of the collection. The runtime has
/// bounded decoded-content caches of its own, so retaining that second,
/// unbounded copy makes memory depend on every landblock visited. Keep the small
/// B-tree lookup cache, but read file payloads on demand and let the owning
/// runtime caches decide their residency.
/// </remarks>
public static class RuntimeDatCollectionFactory
{
public static IDatReaderWriter OpenReadOnly(string datDirectory) =>
new RuntimeDatCollection(new DatCollection(CreateReadOnlyOptions(datDirectory)));
public static DatCollectionOptions CreateReadOnlyOptions(string datDirectory)
{
ArgumentException.ThrowIfNullOrWhiteSpace(datDirectory);
return new DatCollectionOptions
{
DatDirectory = datDirectory,
AccessType = DatAccessType.Read,
IndexCachingStrategy = IndexCachingStrategy.OnDemand,
FileCachingStrategy = FileCachingStrategy.Never,
};
}
}
/// <summary>
/// Owns the runtime's one raw DAT handle set and its one shared bounded typed
/// object facade. Disposing this owner closes both layers exactly once.
/// </summary>
public sealed class RuntimeDatCollection : IDatReaderWriter
{
private readonly DatCollection _raw;
private readonly DatCollectionAdapter _bounded;
private bool _disposed;
internal RuntimeDatCollection(DatCollection raw)
{
ArgumentNullException.ThrowIfNull(raw);
_raw = raw;
_bounded = new DatCollectionAdapter(raw);
}
public string SourceDirectory => _bounded.SourceDirectory;
/// <summary>Aggregate DAT-object cache hit/miss/eviction counts (2026-07-24 measurement-tooling review).</summary>
public CacheStats ObjectCacheStats => _bounded.ObjectCacheStats;
public IDatDatabase Portal => _bounded.Portal;
public IDatDatabase Cell => _bounded.Cell;
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions => _bounded.CellRegions;
public IDatDatabase HighRes => _bounded.HighRes;
public IDatDatabase Language => _bounded.Language;
public IDatDatabase Local => _bounded.Local;
public ReadOnlyDictionary<uint, uint> RegionFileMap => _bounded.RegionFileMap;
public int PortalIteration => _bounded.PortalIteration;
public int CellIteration => _bounded.CellIteration;
public int HighResIteration => _bounded.HighResIteration;
public int LanguageIteration => _bounded.LanguageIteration;
[return: MaybeNull]
public T Get<T>(uint fileId) where T : IDBObj => _bounded.Get<T>(fileId);
public bool TryGet<T>(uint fileId, [MaybeNullWhen(false)] out T value)
where T : IDBObj =>
_bounded.TryGet(fileId, out value);
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
_bounded.GetAllIdsOfType<T>();
public bool TryGetFileBytes(uint regionId, uint fileId, ref byte[] bytes, out int bytesRead) =>
_bounded.TryGetFileBytes(regionId, fileId, ref bytes, out bytesRead);
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
_bounded.ResolveId(id);
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
_bounded.TrySave(obj, iteration);
public bool TrySave<T>(uint regionId, T obj, int iteration = 0) where T : IDBObj =>
_bounded.TrySave(regionId, obj, iteration);
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_bounded.Dispose();
_raw.Dispose();
}
}