using DatReaderWriter; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; namespace AcDream.App.Rendering.Wb; /// /// Adapts acdream's to WB's interface. /// /// O-D7 fallback path: taken because ObjectMeshManager has 26 _dats.X call sites (threshold is 20), /// making a full refactor to DatCollection larger than spec permits in a single task. /// This adapter lets ObjectMeshManager stay byte-identical to the WB original while /// routing all DAT I/O through our single DatCollection. The adapter is dropped in T7 /// when the WorldBuilder project reference is removed entirely. /// internal sealed class DatCollectionAdapter : IDatReaderWriter { private readonly DatCollection _dats; private readonly DatDatabaseWrapper _portal; private readonly DatDatabaseWrapper _cell; private readonly DatDatabaseWrapper _highRes; private readonly DatDatabaseWrapper _language; private readonly ReadOnlyDictionary _cellRegions; public DatCollectionAdapter(DatCollection dats) { ArgumentNullException.ThrowIfNull(dats); _dats = dats; _portal = new DatDatabaseWrapper(dats.Portal); _cell = new DatDatabaseWrapper(dats.Cell); _highRes = new DatDatabaseWrapper(dats.HighRes); _language = new DatDatabaseWrapper(dats.Local); // DatCollection has a single Cell, not multiple cell regions. // Expose it as region 0 to satisfy callers that iterate CellRegions. var regions = new Dictionary { [0u] = _cell }; _cellRegions = new ReadOnlyDictionary(regions); } /// Source directory of the underlying DatCollection. public string SourceDirectory => _dats.Options.DatDirectory ?? string.Empty; public IDatDatabase Portal => _portal; public ReadOnlyDictionary CellRegions => _cellRegions; public IDatDatabase HighRes => _highRes; public IDatDatabase Language => _language; // RegionFileMap is used by some WB internals but not by ObjectMeshManager. public ReadOnlyDictionary RegionFileMap => new ReadOnlyDictionary(new Dictionary()); // Iteration properties — not used by ObjectMeshManager, so delegate to 0. public int PortalIteration => 0; public int CellIteration => 0; public int HighResIteration => 0; public int LanguageIteration => 0; 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); } /// /// Resolves a DAT id to all databases that contain it, along with the DBObjType. /// Mirrors DefaultDatReaderWriter.ResolveId — checks each underlying DatDatabase /// via DatDatabase.TypeFromId (which reads the type range tables). /// public IEnumerable ResolveId(uint id) { var results = new List(); void CheckDb(DatDatabaseWrapper wrapper) { var rawDb = wrapper.RawDatabase; if (rawDb.Tree.TryGetFile(id, out _)) { var type = rawDb.TypeFromId(id); if (type != DBObjType.Unknown) results.Add(new IDatReaderWriter.IdResolution(wrapper, type)); } } // Match DefaultDatReaderWriter ordering: HighRes → Portal → Language → Cell CheckDb(_highRes); CheckDb(_portal); CheckDb(_language); CheckDb(_cell); return results; } public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException("DatCollectionAdapter is read-only."); public bool TrySave(uint regionId, T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException("DatCollectionAdapter is read-only."); public void Dispose() { // The underlying DatCollection is owned by the caller — do not dispose it here. // Individual wrapper objects hold no unmanaged resources. } } /// /// Wraps a as . /// Mirrors WorldBuilder.Shared.Services.DefaultDatDatabase but lives in our namespace /// so the WorldBuilder project reference can be dropped in T7. /// internal sealed class DatDatabaseWrapper : IDatDatabase { private readonly DatDatabase _db; private readonly ConcurrentDictionary<(Type, uint), IDBObj> _cache = new(); private readonly object _lock = new(); public DatDatabaseWrapper(DatDatabase db) { ArgumentNullException.ThrowIfNull(db); _db = db; } /// Exposes the raw DatDatabase for ResolveId's Tree.TryGetFile + TypeFromId calls. internal DatDatabase RawDatabase => _db; public DatDatabase Db => _db; public int Iteration => _db.Iteration?.CurrentIteration ?? 0; public IEnumerable GetAllIdsOfType() where T : IDBObj => _db.GetAllIdsOfType(); public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { if (_cache.TryGetValue((typeof(T), fileId), out var cached)) { value = (T)cached; return true; } lock (_lock) { if (_db.TryGet(fileId, out value)) { _cache.TryAdd((typeof(T), fileId), value); return true; } } return false; } public bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value) { lock (_lock) { return _db.TryGetFileBytes(fileId, out value); } } public bool TryGetFileBytes(uint fileId, ref byte[] bytes, out int bytesRead) { lock (_lock) { return _db.TryGetFileBytes(fileId, ref bytes, out bytesRead); } } public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException("DatDatabaseWrapper is read-only."); public void Dispose() { // The underlying DatDatabase is owned by DatCollection — do not dispose here. } }