using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Walk; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using AcDream.Core.Meshing; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using Microsoft.Extensions.Logging.Abstractions; namespace AcDream.App.Tests.Rendering.Walk; /// /// Campaign FW stage FW2: /// (pure CPU merge-run legality) and, from Campaign FW stage FW3.4a, /// + /// (the same legality proven /// through actual recorded RHI calls against , /// replacing the single SubmitOrderedStream call those two now split). /// public sealed class OrderPreservingSubmitterTests { private static OrderedDrawCommand MakeCommand( int index, WalkDrawStage stage = WalkDrawStage.Terrain, TranslucencyKind translucency = TranslucencyKind.Opaque, CullMode cullMode = CullMode.CounterClockwise, uint detailCategory = 0) => new( Key: new GroupKey( FirstIndex: (uint)index * 3, BaseVertex: index * 4, IndexCount: 3, TextureSlot: new GpuTextureSlot((uint)index), TextureLayer: 0, Translucency: translucency, FoliageFlags: 0, CullMode: cullMode), Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3), Stage: stage, CellId: 0x8C040100u + (uint)index, ClipSlot: 0, Lights: WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 0, Alpha: 1f, SelectionLighting: Vector2.Zero, DetailCategory: detailCategory); private static OrderedDrawStream StreamOf(params OrderedDrawCommand[] commands) { var stream = new OrderedDrawStream(); foreach (OrderedDrawCommand command in commands) stream.Append(command); return stream; } // ── Pure BuildOrderedMergeRuns — no GPU device ───────────────────────── [Fact] public void BuildOrderedMergeRuns_MergesThreeAdjacentSameStateCommandsIntoOneRun() { OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1), MakeCommand(2)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); WbDrawDispatcher.OrderedMergeRun run = Assert.Single(runs); Assert.Equal(0, run.FirstCommand); Assert.Equal(3, run.CommandCount); } [Fact] public void BuildOrderedMergeRuns_SplitsOnAPipelineBucketChange() { OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.Opaque), MakeCommand(2, translucency: TranslucencyKind.AlphaBlend)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 2), new WbDrawDispatcher.OrderedMergeRun(2, 1), ], runs); } [Fact] public void BuildOrderedMergeRuns_SplitsOnACullModeChange() { OrderedDrawStream stream = StreamOf( MakeCommand(0, cullMode: CullMode.None), MakeCommand(1, cullMode: CullMode.None), MakeCommand(2, cullMode: CullMode.Clockwise)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 2), new WbDrawDispatcher.OrderedMergeRun(2, 1), ], runs); } /// /// The load-bearing new assertion: two commands whose material state /// (bucket, cull mode, detail category) is IDENTICAL still split into two /// runs when their differs. Nothing about the /// deferred-alpha template this submitter borrows from ever had to /// consider stage — walk order introduces it. /// [Fact] public void BuildOrderedMergeRuns_SplitsOnAStageChangeEvenWithIdenticalMaterialState() { OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.CellStatic)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 1), new WbDrawDispatcher.OrderedMergeRun(1, 1), ], runs); } [Fact] public void BuildOrderedMergeRuns_ADetailCategoryCommandIsAlwaysSolo() { OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1, detailCategory: 1), MakeCommand(2)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 1), new WbDrawDispatcher.OrderedMergeRun(1, 1), new WbDrawDispatcher.OrderedMergeRun(2, 1), ], runs); } [Fact] public void BuildOrderedMergeRuns_ThrowsNotSupportedForAPortalPunchCommand() { OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.PortalPunch)); Assert.Throws( () => WbDrawDispatcher.BuildOrderedMergeRuns(stream)); } [Fact] public void BuildOrderedMergeRuns_EveryCommandBelongsToExactlyOneRunInOrderWithNoGaps() { OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.Opaque, cullMode: CullMode.None), MakeCommand(1, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.Opaque, cullMode: CullMode.None), MakeCommand(2, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.None), MakeCommand(3, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise), MakeCommand(4, stage: WalkDrawStage.CellStatic, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise), MakeCommand(5, stage: WalkDrawStage.CellStatic, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise, detailCategory: 1), MakeCommand(6, stage: WalkDrawStage.CellStatic, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); int coveredThrough = 0; int totalCommands = 0; foreach (WbDrawDispatcher.OrderedMergeRun run in runs) { Assert.Equal(coveredThrough, run.FirstCommand); Assert.True(run.CommandCount > 0); coveredThrough = run.FirstCommand + run.CommandCount; totalCommands += run.CommandCount; } Assert.Equal(stream.Count, coveredThrough); Assert.Equal(stream.Count, totalCommands); } // ── PrepareOrderedStream + DrawOrderedRange — recorded RHI calls against // RecordingGpuDevice. Campaign FW3.4a replaced the single // SubmitOrderedStream call with this pair (prepare the whole stream once, // draw it via one or more ranges) — every test below that used to call // SubmitOrderedStream now calls Prepare once and Draw the WHOLE stream as // ONE range, which is exactly SubmitOrderedStream's old behavior; the // "several ranges" and "bind once" shapes get their own tests further // down since they have no FW2 analogue. ──────────────────────────────── private static void PrepareAndDrawWhole(WbDrawDispatcher dispatcher, DrawScope draw, OrderedDrawStream stream) { dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); if (stream.Count > 0) dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count); } [Fact] public void PrepareThenDraw_AlternatingStateCommandsRecordOneDrawEachInOrder() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.AlphaBlend), MakeCommand(2, translucency: TranslucencyKind.Opaque), MakeCommand(3, translucency: TranslucencyKind.AlphaBlend)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device); Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)], ranges); } [Fact] public void PrepareThenDraw_MergesAdjacentSameStateCommandsIntoOneMultiDrawIndirect() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1), MakeCommand(2)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device); Assert.Equal([(0, 3)], ranges); } [Fact] public void PrepareThenDraw_CullModeChangeRecordsSeparateCullCallsAndSplitsTheDraw() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, cullMode: CullMode.None), MakeCommand(1, cullMode: CullMode.None), MakeCommand(2, cullMode: CullMode.Clockwise)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); Assert.Equal([(0, 2), (2, 1)], DecodeDrawRanges(fx.Device)); List cullCalls = [.. fx.Device.Calls.OfType().Select(c => c.CullMode)]; // ApplyCullModeRhi: CullMode.None -> GpuCullMode.None, CullMode.Clockwise -> GpuCullMode.Front. Assert.Equal([GpuCullMode.None, GpuCullMode.Front], cullCalls); } [Fact] public void PrepareThenDraw_StageChangeSplitsTheDrawEvenWithIdenticalMaterialState() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.CellStatic)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device)); } [Fact] public void PrepareThenDraw_ADetailCategoryCommandRecordsItsOwnSoloDraw() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1, detailCategory: 1), MakeCommand(2)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); Assert.Equal([(0, 1), (1, 1), (2, 1)], DecodeDrawRanges(fx.Device)); } [Fact] public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.AlphaBlend)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device); Assert.Equal(2, runs.Count); Assert.Equal(0, runs[0].Constants.RenderPass); Assert.Equal(1, runs[1].Constants.RenderPass); } [Fact] public void PrepareOrderedStream_ThrowsNotSupportedForAPortalPunchCommandBeforeAnyDraw() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.PortalPunch)); Assert.Throws( () => fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity)); Assert.Empty(fx.Device.Calls.OfType()); Assert.Empty(fx.Device.Calls.OfType()); } [Fact] public void PrepareOrderedStream_EmptyStreamRecordsNoDraws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); fx.Dispatcher.PrepareOrderedStream(draw.Frame, new OrderedDrawStream(), Matrix4x4.Identity); Assert.Empty(fx.Device.Calls.OfType()); } // ── Campaign FW3.4a — the shapes with no FW2 analogue: drawing the SAME // prepared stream as several ranges, the bind-once optimization, the // fail-loud range check, and the assert-don't-slice straddle guard. ─── /// /// The whole point of the split: drawing the SAME stream as TWO ranges /// (with the boundary between them supplied to Prepare, exactly as /// WalkFrameDriver.Replay supplies its recorded mark positions) produces /// the identical total recorded draw/cull/push-constant calls as drawing /// it as one range — the range split changes nothing about what reaches /// the GPU, only how many DrawOrderedRange calls got there. /// [Fact] public void DrawOrderedRange_AsTwoRangesAtASegmentBoundary_MatchesOneRangeOverTheWholeStream() { OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.Opaque), MakeCommand(2, translucency: TranslucencyKind.Opaque), MakeCommand(3, translucency: TranslucencyKind.Opaque)); using var wholeFx = new DispatcherFixture(); using (DrawScope draw = wholeFx.BeginDraw()) { wholeFx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); wholeFx.Dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count); } List<(int Start, int Count)> wholeRanges = DecodeDrawRanges(wholeFx.Device); using var splitFx = new DispatcherFixture(); using (DrawScope draw = splitFx.BeginDraw()) { // Command 2 is a segment boundary (mirrors a mark WalkFrameDriver // would record there, e.g. a cell shell between two same-state // segments) — without it, all four commands would merge into ONE // run; the boundary forces two. splitFx.Dispatcher.PrepareOrderedStream( draw.Frame, stream, Matrix4x4.Identity, forcedBreaksAscending: [2]); splitFx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2); splitFx.Dispatcher.DrawOrderedRange(draw.Pass, 2, 2); } List<(int Start, int Count)> splitRanges = DecodeDrawRanges(splitFx.Device); // The split path draws two runs where the whole-range path drew one // (the forced boundary is the only difference) — but every command // reaches the GPU exactly once, in order, with identical coverage. Assert.Equal([(0, 4)], wholeRanges); Assert.Equal([(0, 2), (2, 2)], splitRanges); } /// /// The FW3.4a perf shape itself: the nine per-instance storage binds plus /// the warm-up pipeline bind happen on the FIRST DrawOrderedRange call in /// a frame only — a second call over the same prepared payload issues no /// further StorageBind calls, which is the whole reason this stage exists /// (the old SubmitOrderedStream rebound everything on every call). /// [Fact] public void DrawOrderedRange_EveryCallRebindsTheStorageSections() { // The corrected FW3.4a contract (the dense-Arwic device-lost fix): // between ordered ranges the walk's leaf draws and RetailAlphaQueue // flushes rebind the SAME set-0 slots to THEIR sections, so every // DrawOrderedRange call must re-bind its own — a latched skip draws // the next range against foreign buffers. Only the ring WRITES are // once-per-frame (PrepareOrderedStream); binds repeat per range, // exactly like DrawPreparedAlphaBatchRhi. using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.CellStatic)); fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 1); int boundAfterFirst = fx.Device.Calls.OfType().Count(); Assert.True(boundAfterFirst > 0); fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1); int boundAfterSecond = fx.Device.Calls.OfType().Count(); Assert.Equal(boundAfterFirst * 2, boundAfterSecond); // Both commands drew; the rebinds changed nothing about coverage. Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device)); } /// /// Fail-loud range check (mirrors DrawPreparedAlphaBatchRhi's): a range /// outside what PrepareOrderedStream uploaded throws rather than drawing /// garbage or silently clamping — including a draw attempted before ANY /// Prepare call this frame. /// [Fact] public void DrawOrderedRange_RangeExceedingThePreparedPayload_Throws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf(MakeCommand(0)); fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2)); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1)); } [Fact] public void DrawOrderedRange_BeforeAnyPrepareCallThisFrame_Throws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 1)); } /// /// The plan's "assert it" rule: a range that does not align with a merge /// run boundary throws rather than silently slicing the run — proven /// directly here (skipping the boundary a real WalkFrameDriver mark would /// supply) since production code always supplies the boundary and would /// never exercise this path. /// [Fact] public void DrawOrderedRange_RangeStraddlingAMergeRun_Throws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); // All four commands share stage/bucket/cull — ONE merge run [0, 4) — // and no forced break is supplied, so a [0, 2) range straddles it. OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1), MakeCommand(2), MakeCommand(3)); fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2)); } /// /// Fail-loud invariant: whatever the state pattern, the recorded /// MultiDrawIndirect calls' DrawCounts always sum to the stream's Count — /// no command is ever silently skipped, and none is drawn twice. /// [Fact] public void PrepareThenDraw_TotalRecordedDrawCountAlwaysEqualsTheStreamCount() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); var stream = new OrderedDrawStream(); var stages = new[] { WalkDrawStage.Terrain, WalkDrawStage.CellStatic, WalkDrawStage.BuildingShell }; var blends = new[] { TranslucencyKind.Opaque, TranslucencyKind.AlphaBlend, TranslucencyKind.Additive, TranslucencyKind.InvAlpha, }; var culls = new[] { CullMode.None, CullMode.Clockwise, CullMode.CounterClockwise }; const int commandCount = 11; for (int i = 0; i < commandCount; i++) { stream.Append(MakeCommand( i, stage: stages[i % stages.Length], translucency: blends[i % blends.Length], cullMode: culls[i % culls.Length], detailCategory: i == 5 ? 1u : 0u)); } PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device); int sum = ranges.Sum(r => r.Count); Assert.Equal(commandCount, sum); int coveredThrough = 0; foreach ((int start, int count) in ranges) { Assert.Equal(coveredThrough, start); coveredThrough += count; } Assert.Equal(commandCount, coveredThrough); } // ── Decode helpers ────────────────────────────────────────────────────── private static List<(int Start, int Count)> DecodeDrawRanges(RecordingGpuDevice device) => [.. DecodeRuns(device).Select(r => (r.Start, r.Count))]; private static List<(GpuPushConstants Constants, int Start, int Count)> DecodeRuns( RecordingGpuDevice device) { GpuPushConstants? lastConstants = null; uint? commandBase = null; var result = new List<(GpuPushConstants, int, int)>(); foreach (var call in device.Calls) { if (call is GpuRecordedPushConstants pc) { lastConstants = pc.Constants; } else if (call is GpuRecordedMultiDrawIndirect mdi) { Assert.Equal((uint)WbDrawDispatcher.DrawCommandStride, mdi.StrideBytes); commandBase ??= mdi.OffsetBytes; int start = (int)((mdi.OffsetBytes - commandBase.Value) / mdi.StrideBytes); Assert.NotNull(lastConstants); result.Add((lastConstants!.Value, start, (int)mdi.DrawCount)); } } return result; } // ── Fixture: a real WbDrawDispatcher against RecordingGpuDevice ───────── private readonly struct DrawScope : IDisposable { private readonly IDisposable _publication; private readonly IGpuPassEncoder _pass; public DrawScope(IGpuFrame frame, IGpuPassEncoder pass, IDisposable publication) { Frame = frame; _pass = pass; _publication = publication; } public IGpuFrame Frame { get; } public IGpuPassEncoder Pass => _pass; public void Dispose() { _publication.Dispose(); _pass.Dispose(); } } private sealed class DispatcherFixture : IDisposable { private readonly WbMeshAdapter _meshAdapter; private readonly TextureCache _textures; public DispatcherFixture() { Device = new RecordingGpuDevice(); FrameLifetime = new GpuDeviceFrameLifetime(Device); Scope = new VulkanWorldPassScope(sampleCount: 1); _textures = new TextureCache(Device, new NoopDatReaderWriter()); _meshAdapter = new WbMeshAdapter( Device, new NoopDatReaderWriter(), new NullPreparedAssetSource(), NullLogger.Instance, Device.Retirement); var entitySpawnAdapter = new EntitySpawnAdapter( _textures, _ => throw new NotSupportedException( "Not exercised by SubmitOrderedStream tests.")); Dispatcher = new WbDrawDispatcher( Device, FrameLifetime, Scope, _textures, _meshAdapter, entitySpawnAdapter, new EntityClassificationCache(), new AcDream.Core.Rendering.TranslucencyFadeManager()); } public RecordingGpuDevice Device { get; } public GpuDeviceFrameLifetime FrameLifetime { get; } public VulkanWorldPassScope Scope { get; } public WbDrawDispatcher Dispatcher { get; } /// Opens a frame and a backbuffer pass, publishes it on /// , then clears the recorded calls so a test only /// sees what its own SubmitOrderedStream call produced. public DrawScope BeginDraw() { FrameLifetime.BeginFrame(); IGpuFrame frame = FrameLifetime.CurrentFrame!; IGpuPassEncoder pass = frame.BeginPass( GpuPassDescription.BackbufferClear( "fw2-ordered-stream-test", Vector4.Zero, sampleCount: 1)); IDisposable publication = Scope.Publish(pass); Device.Clear(); return new DrawScope(frame, pass, publication); } public void Dispose() { Dispatcher.Dispose(); _meshAdapter.Dispose(); _textures.Dispose(); Device.Dispose(); } } private sealed class NullPreparedAssetSource : IPreparedAssetSource { public PreparedAssetSourceStats Stats => default; public CacheStats DecodedTextureCacheStats => default; public PreparedAssetPresence Probe( AcDream.Content.Pak.PakAssetType type, uint sourceFileId) => PreparedAssetPresence.Missing; public PreparedAssetReadResult Read( in PreparedAssetRequest request, CancellationToken cancellationToken = default) => PreparedAssetReadResult.Missing; public void Dispose() { } } private sealed class NoopDatReaderWriter : IDatReaderWriter { private readonly StubDatabase _portal = new(); private readonly StubDatabase _highRes = new(); private readonly StubDatabase _language = new(); private readonly StubDatabase _cell = new(); public string SourceDirectory => string.Empty; public IDatDatabase Portal => _portal; public IDatDatabase Cell => _cell; public ReadOnlyDictionary CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes => _highRes; public IDatDatabase Language => _language; public IDatDatabase Local => _language; public ReadOnlyDictionary RegionFileMap { get; } = new(new Dictionary()); 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) { bytesRead = 0; return false; } public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public IEnumerable ResolveId(uint id) => Array.Empty(); public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public bool TrySave( uint regionId, T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); [return: MaybeNull] public T Get(uint fileId) where T : IDBObj => default; public bool TryGet( uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public void Dispose() { } private sealed class StubDatabase : IDatDatabase { public DatDatabase Db => throw new NotSupportedException(); public int Iteration => 0; public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public bool TryGet( uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public bool TryGetFileBytes( uint fileId, [MaybeNullWhen(false)] out byte[] value) { value = null; return false; } public bool TryGetFileBytes( uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public void Dispose() { } } } }