using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using AcDream.Core.Meshing;
using AcDream.Core.World;
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 FW3.2a: the walk→draw population layer's data-
/// equivalence referee. Covers
/// (the shared per-entity classify seam),
/// (opaque → , translucent → the alpha queue,
/// selection publish), and the FW3.2a own-cull-scratch fix to
/// SubmitOrderedStream.
///
public sealed class WalkStaticStreamPopulatorTests
{
// ── Test doubles ────────────────────────────────────────────────────────
private sealed class RecordingSelectionSink : IRetailSelectionRenderSink
{
public readonly List<(uint ServerGuid, uint LocalEntityId, int PartIndex, uint GfxObjId, Matrix4x4 LocalToWorld)>
Calls = new();
public void AddVisiblePart(
uint serverGuid, uint localEntityId, int partIndex, uint gfxObjId, Matrix4x4 partWorld) =>
Calls.Add((serverGuid, localEntityId, partIndex, gfxObjId, partWorld));
}
// ── Synthetic RenderProjectionRecord construction ──────────────────────
private static RenderProjectionRecord MakeRecord(
uint localEntityId,
uint serverGuid,
Vector3 position,
IReadOnlyList meshRefs,
bool isBuildingShell = false,
uint parentCellId = 0u) =>
new(
Id: RenderProjectionId.FromRaw(localEntityId),
ProjectionClass: RenderProjectionClass.OutdoorStatic,
OwnerIncarnation: RenderOwnerIncarnation.FromRaw(1),
Transform: new RenderTransform(Matrix4x4.CreateTranslation(position)),
PreviousTransform: default,
MeshSet: default,
Material: default,
Residency: default,
Bounds: default,
Flags: RenderProjectionFlags.Draw,
DegradeState: default,
SortKey: new RenderSortKey(0),
DirtyMask: default,
Source: new RenderSourceMetadata(
LocalEntityId: localEntityId,
ServerGuid: serverGuid,
SourceId: 0,
ParentCellId: parentCellId,
EffectCellId: 0,
BuildingShellAnchorCellId: 0,
TransformFingerprint: default,
GeometryFingerprint: default,
AppearanceFingerprint: default),
EntityPayload: new RenderEntityPayload(
MeshRefs: meshRefs,
PaletteOverride: null,
IsBuildingShell: isBuildingShell));
private static ObjectRenderBatch MakeBatch(
uint surfaceId,
TranslucencyKind translucency,
uint firstIndex,
int baseVertex,
int indexCount,
uint textureSlotIndex,
uint textureLayer = 0,
CullMode cullMode = CullMode.CounterClockwise) =>
new()
{
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
Translucency = translucency,
FirstIndex = firstIndex,
BaseVertex = (uint)baseVertex,
IndexCount = indexCount,
TextureSlot = new GpuTextureSlot(textureSlotIndex),
TextureIndex = (int)textureLayer,
};
private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) =>
new() { Batches = new List(batches) };
// ── Reflection seam: ObjectMeshManager owns no test-injection API, and
// driving real GPU/GfxObj upload for a unit test is out of this stage's
// scope — ObjectRenderData/ObjectRenderBatch are plain settable classes,
// so this seeds the manager's private cache directly. ──────────────────
private static void InjectRenderData(ObjectMeshManager manager, ulong id, ObjectRenderData data)
{
FieldInfo field = typeof(ObjectMeshManager).GetField(
"_renderData", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException(
"ObjectMeshManager._renderData field not found — test relies on this exact name.");
var dict = (ConcurrentDictionary)field.GetValue(manager)!;
dict[id] = data;
}
// ── Deliverable 1: ClassifyEntityForWalk data equivalence ─────────────
[Fact]
public void ClassifyEntityForWalk_OneOpaqueAndOneTranslucentPart_YieldsBatchesInRecordOrderWithCorrectIsOpaque()
{
using var fx = new DispatcherFixture();
const ulong opaqueGfxObj = 0x0100_0001UL;
const ulong alphaGfxObj = 0x0100_0002UL;
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2)));
var meshRefs = new[]
{
new MeshRef((uint)opaqueGfxObj, Matrix4x4.CreateTranslation(1, 0, 0)),
new MeshRef((uint)alphaGfxObj, Matrix4x4.CreateTranslation(0, 1, 0)),
};
RenderProjectionRecord record = MakeRecord(
localEntityId: 100, serverGuid: 0, position: new Vector3(5, 6, 7), meshRefs);
var batches = new List();
var selectionParts = new List();
fx.Dispatcher.ClassifyEntityForWalk(in record, tupleLandblockId: 0x8C04u, batches, selectionParts);
Assert.Equal(2, batches.Count);
WbDrawDispatcher.WalkClassifiedBatch opaque = batches[0];
Assert.True(opaque.IsOpaque);
Assert.Equal(TranslucencyKind.Opaque, opaque.Key.Translucency);
Assert.Equal(0u, opaque.Key.FirstIndex);
Assert.Equal(3, opaque.Key.IndexCount);
Assert.Equal(1u, opaque.Key.TextureSlot.Index);
Assert.Equal(1f, opaque.Alpha);
Assert.Equal(meshRefs[0].PartTransform * record.Transform.LocalToWorld, opaque.Transform);
WbDrawDispatcher.WalkClassifiedBatch translucent = batches[1];
Assert.False(translucent.IsOpaque);
Assert.Equal(TranslucencyKind.AlphaBlend, translucent.Key.Translucency);
Assert.Equal(3u, translucent.Key.FirstIndex);
Assert.Equal(6, translucent.Key.IndexCount);
Assert.Equal(2u, translucent.Key.TextureSlot.Index);
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
Assert.Equal(2, selectionParts.Count);
Assert.Equal(100u, selectionParts[0].LocalEntityId);
Assert.Equal(0, selectionParts[0].PartIndex);
Assert.Equal((uint)opaqueGfxObj, selectionParts[0].GfxObjId);
Assert.Equal(opaque.Transform, selectionParts[0].LocalToWorld);
Assert.Equal(1, selectionParts[1].PartIndex);
Assert.Equal((uint)alphaGfxObj, selectionParts[1].GfxObjId);
}
[Fact]
public void ClassifyEntityForWalk_SetupComposite_EncodesPartAndSetupPartIndexLikePackedRoute()
{
using var fx = new DispatcherFixture();
const ulong setupGfxObj = 0x1000_0010UL;
const ulong trunkGfxObj = 0x0100_0011UL;
const ulong leavesGfxObj = 0x0100_0012UL;
InjectRenderData(fx.Manager, trunkGfxObj, MakeFlatMesh(
MakeBatch(0x08000011u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
InjectRenderData(fx.Manager, leavesGfxObj, MakeFlatMesh(
MakeBatch(0x08000012u, TranslucencyKind.ClipMap, 3, 4, 6, 2)));
InjectRenderData(fx.Manager, setupGfxObj, new ObjectRenderData
{
IsSetup = true,
SetupParts = new List<(ulong GfxObjId, Matrix4x4 Transform)>
{
(trunkGfxObj, Matrix4x4.CreateTranslation(0, 0, 1)),
(leavesGfxObj, Matrix4x4.CreateTranslation(0, 0, 2)),
},
});
var meshRefs = new[] { new MeshRef((uint)setupGfxObj, Matrix4x4.Identity) };
RenderProjectionRecord record = MakeRecord(200, 0, Vector3.Zero, meshRefs);
var batches = new List();
var selectionParts = new List();
fx.Dispatcher.ClassifyEntityForWalk(in record, 0x8C04u, batches, selectionParts);
Assert.Equal(2, batches.Count);
Assert.Equal(2, selectionParts.Count);
// partIndex=0 (the entity's single top-level MeshRef) << 16 | setupPartIndex.
Assert.Equal(0, selectionParts[0].PartIndex);
Assert.Equal(1, selectionParts[1].PartIndex);
Assert.Equal((uint)trunkGfxObj, selectionParts[0].GfxObjId);
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
}
// ── Deliverable 2: WalkStaticStreamPopulator routing ───────────────────
[Fact]
public void PopulateCell_OpaqueBatchAppendsOrderedDrawCommandInRecordOrderWithStageAndCellProvenance()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0100_0003UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08000003u, TranslucencyKind.Opaque, 10, 2, 12, 5)));
var record = MakeRecord(300, 0, new Vector3(1, 2, 3), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
var stream = new OrderedDrawStream();
populator.PopulateCell(
stream, WalkDrawStage.CellStatic, cellId: 0x8C040100u,
new[] { record }, tupleLandblockId: 0x8C04u,
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
Assert.Equal(1, stream.Count);
Assert.Equal(WalkDrawStage.CellStatic, stream.Stages[0]);
Assert.Equal(0x8C040100u, stream.CellIds[0]);
Assert.Equal(10u, stream.Keys[0].FirstIndex);
Assert.Equal(12, stream.Keys[0].IndexCount);
Assert.Equal(1f, stream.Alphas[0]);
Assert.Equal(record.Transform.LocalToWorld, stream.Transforms[0]);
}
[Fact]
public void PopulateOutdoorStatics_UsesTheOutdoorStaticStage()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0100_0004UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08000004u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
var record = MakeRecord(400, 0, Vector3.Zero, new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
var stream = new OrderedDrawStream();
populator.PopulateOutdoorStatics(
stream, cellId: 0x8C040000u, new[] { record }, tupleLandblockId: 0x8C04u,
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
Assert.Equal(1, stream.Count);
Assert.Equal(WalkDrawStage.OutdoorStatic, stream.Stages[0]);
}
[Fact]
public void PopulateCell_TranslucentBatchDoesNotAppendToTheStreamAndReachesTheAlphaQueue()
{
using var fx = new DispatcherFixture(withAlphaQueue: true);
const ulong gfxObj = 0x0100_0005UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08000005u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
var record = MakeRecord(500, 0, new Vector3(0, 0, 10), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
var stream = new OrderedDrawStream();
fx.AlphaQueue!.BeginFrame();
populator.PopulateCell(
stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u,
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
Assert.Equal(0, stream.Count);
Assert.Equal(1, fx.AlphaQueue.PendingCount);
fx.AlphaQueue.AbortFrame();
}
[Fact]
public void PopulateCell_PublishesSelectionPartsForEveryClassifiedEntity()
{
var sink = new RecordingSelectionSink();
using var fx = new DispatcherFixture(selectionSink: sink);
const ulong gfxObj = 0x0100_0006UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08000006u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
var record = MakeRecord(600, serverGuid: 0x8000_0060u, new Vector3(1, 1, 1),
new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
var stream = new OrderedDrawStream();
populator.PopulateCell(
stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u,
Vector3.Zero, Matrix4x4.Identity);
var call = Assert.Single(sink.Calls);
Assert.Equal(0x8000_0060u, call.ServerGuid);
Assert.Equal(600u, call.LocalEntityId);
Assert.Equal(0, call.PartIndex);
Assert.Equal((uint)gfxObj, call.GfxObjId);
Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld);
}
// ── SubmitWalkAlphaInstance: same viewer distance + per-instance data as
// DeferTransparentGroups, through the REAL RetailAlphaQueue. ───────────
[Fact]
public void SubmitWalkAlphaInstance_SubmitsTheSameViewerDistanceComputeViewerDistanceWouldProduce()
{
using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
Vector3 localSortCenter = new(1, 2, 3);
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
var cameraWorldPosition = Vector3.Zero;
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
key, model, ClipSlot: 7, WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 1,
Alpha: 0.5f, SelectionLighting: new Vector2(0.25f, 0.75f), DetailCategory: 1,
IsOpaque: false, LocalSortCenter: localSortCenter);
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, cameraWorldPosition, Matrix4x4.Identity);
Assert.Equal(1, fx.AlphaQueue.PendingCount);
// Same call DeferTransparentGroups makes per instance — independently
// computed here so the assertion cannot pass by construction.
float expectedDistance = RetailAlphaOrdering.ComputeViewerDistance(
localSortCenter, model, cameraWorldPosition);
FieldInfo submissionsField = typeof(RetailAlphaQueue).GetField(
"_submissions", BindingFlags.NonPublic | BindingFlags.Instance)!;
var submissions = (List)submissionsField.GetValue(fx.AlphaQueue)!;
RetailAlphaSubmission submission = Assert.Single(submissions);
Assert.Equal(expectedDistance, submission.ViewerDistance, precision: 4);
Assert.Equal(0, submission.Token);
fx.AlphaQueue.AbortFrame();
}
[Fact]
public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope()
{
using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Vector3.Zero, Matrix4x4.Identity);
Assert.Throws(() =>
fx.Dispatcher.SubmitWalkAlphaInstance(
in batch, Vector3.Zero, Matrix4x4.CreateTranslation(1, 0, 0)));
fx.AlphaQueue.AbortFrame();
}
// ── Deliverable 3: SubmitOrderedStream's own cull scratch ──────────────
[Fact]
public void SubmitOrderedStream_DoesNotReadOrCorruptTheSharedAlphaCullScratch()
{
using var fx = new DispatcherFixture();
using DrawScope draw = fx.BeginDraw();
// Poison the SHARED _drawCullModes scratch the alpha path owns — the
// exact array SubmitOrderedStream used to write into before FW3.2a.
// Under the OLD shared-scratch behavior this test's second assertion
// fails: SubmitOrderedStream's own command overwrites index 0 with
// its own cull mode (Clockwise), destroying the alpha path's poison.
FieldInfo field = typeof(WbDrawDispatcher).GetField(
"_drawCullModes", BindingFlags.NonPublic | BindingFlags.Instance)!;
var poisonModes = (CullMode[])field.GetValue(fx.Dispatcher)!;
poisonModes[0] = CullMode.None;
var stream = new OrderedDrawStream();
stream.Append(new OrderedDrawCommand(
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
// (1) The ordered submission's OWN recorded cull call reflects the
// STREAM's cull mode (Clockwise -> GpuCullMode.Front), not the
// poisoned shared array's (None).
List cullCalls = [.. fx.Device.Calls.OfType().Select(c => c.CullMode)];
Assert.Equal([GpuCullMode.Front], cullCalls);
// (2) The shared _drawCullModes scratch is UNTOUCHED — the ordered
// path never wrote through it.
var afterModes = (CullMode[])field.GetValue(fx.Dispatcher)!;
Assert.Equal(CullMode.None, afterModes[0]);
}
// ── Fixture ─────────────────────────────────────────────────────────────
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(
bool withAlphaQueue = false,
IRetailSelectionRenderSink? selectionSink = null)
{
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 these tests."));
AlphaQueue = withAlphaQueue ? new RetailAlphaQueue() : null;
Dispatcher = new WbDrawDispatcher(
Device,
FrameLifetime,
Scope,
_textures,
_meshAdapter,
entitySpawnAdapter,
new EntityClassificationCache(),
new AcDream.Core.Rendering.TranslucencyFadeManager(),
selectionSink: selectionSink,
alphaQueue: AlphaQueue);
}
public RecordingGpuDevice Device { get; }
public GpuDeviceFrameLifetime FrameLifetime { get; }
public VulkanWorldPassScope Scope { get; }
public WbDrawDispatcher Dispatcher { get; }
public RetailAlphaQueue? AlphaQueue { get; }
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
public DrawScope BeginDraw()
{
FrameLifetime.BeginFrame();
IGpuFrame frame = FrameLifetime.CurrentFrame!;
IGpuPassEncoder pass = frame.BeginPass(
GpuPassDescription.BackbufferClear(
"fw3-2a-walk-populator-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()
{
}
}
}
}