From 1030c69b3c37a2f9e9a73e9f77aef0faf0ad2496 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 8 May 2026 13:18:50 +0200 Subject: [PATCH] phase(N.4): WbMeshAdapter stub + IWbMeshAdapter interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stub adapter that validates constructor args and exposes the public shape (IncrementRefCount / DecrementRefCount / GetRenderData / Dispose). Real ObjectMeshManager init is deferred to Task 9 — for now methods no-op so call sites can wire the adapter without behavioral effect. IWbMeshAdapter interface enables mocking in subsequent tasks (LandblockSpawnAdapter tests in Task 11 need it). Co-Authored-By: Claude Opus 4.6 --- .../Rendering/Wb/IWbMeshAdapter.cs | 12 ++++ src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs | 72 +++++++++++++++++++ .../Rendering/Wb/WbMeshAdapterTests.cs | 47 ++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs create mode 100644 src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs create mode 100644 tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs diff --git a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs new file mode 100644 index 0000000..3ea4853 --- /dev/null +++ b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs @@ -0,0 +1,12 @@ +namespace AcDream.App.Rendering.Wb; + +/// +/// Mockable interface over so adapters that +/// drive ref-count lifecycle (e.g. LandblockSpawnAdapter, EntitySpawnAdapter) +/// can be unit-tested without a real WB pipeline behind them. +/// +public interface IWbMeshAdapter +{ + void IncrementRefCount(ulong id); + void DecrementRefCount(ulong id); +} diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs new file mode 100644 index 0000000..0f00620 --- /dev/null +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -0,0 +1,72 @@ +using System; +using DatReaderWriter; +using Microsoft.Extensions.Logging; +using Silk.NET.OpenGL; + +namespace AcDream.App.Rendering.Wb; + +/// +/// Single seam between acdream and WB's render pipeline. Owns the +/// ObjectMeshManager instance (when fully initialized) and exposes +/// a stable acdream-shaped API so the rest of the renderer doesn't need +/// to know about WB's types directly. +/// +/// +/// Phase N.4 staging: currently a stub. Real ObjectMeshManager +/// + OpenGLGraphicsDevice initialization is added in Task 9 once +/// the dat-reader adapter (Task 6) lands. Until then, methods no-op so +/// call sites can wire the adapter without behavioral effect when the +/// flag is on. +/// +/// +public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter +{ + // _meshManager and _graphicsDevice will be wired in Task 9 once + // WbDatReaderAdapter (Task 6) lands. For now, both are null and all + // methods no-op. + // private ObjectMeshManager? _meshManager; + // private OpenGLGraphicsDevice? _graphicsDevice; + private bool _disposed; + + public WbMeshAdapter(GL gl, DatCollection dats, ILogger logger) + { + ArgumentNullException.ThrowIfNull(gl); + ArgumentNullException.ThrowIfNull(dats); + ArgumentNullException.ThrowIfNull(logger); + + // TODO(N.4 Task 9): construct OpenGLGraphicsDevice and ObjectMeshManager + // once WbDatReaderAdapter (Task 6) is available to bridge our DatCollection + // to WB's IDatReaderWriter. + } + + private WbMeshAdapter() + { + // Uninitialized constructor — only for tests / for cases where the + // flag is off and the caller wants a Dispose-safe no-op instance. + } + + /// Test/init helper — produces a Dispose-safe instance with no + /// underlying mesh manager. Public methods are all no-ops. + public static WbMeshAdapter CreateUninitialized() => new(); + + /// Returns null until Task 9 wires up the real mesh manager. + public object? GetRenderData(ulong id) => null; + + public void IncrementRefCount(ulong id) + { + // No-op until Task 9. + } + + public void DecrementRefCount(ulong id) + { + // No-op until Task 9. + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + // _meshManager?.Dispose(); + // _graphicsDevice?.Dispose(); + } +} diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs new file mode 100644 index 0000000..d92bd46 --- /dev/null +++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs @@ -0,0 +1,47 @@ +using System; +using AcDream.App.Rendering.Wb; +using Microsoft.Extensions.Logging.Abstractions; +using Silk.NET.OpenGL; + +namespace AcDream.Core.Tests.Rendering.Wb; + +public sealed class WbMeshAdapterTests +{ + [Fact] + public void Construct_WithNullGl_ThrowsArgumentNull() + { + Assert.Throws(() => + new WbMeshAdapter(gl: null!, dats: null!, logger: NullLogger.Instance)); + } + + [Fact] + public void Construct_WithNullDats_ThrowsArgumentNull() + { + // GL cannot be constructed without a real GL context, so we verify + // the dats-null guard by passing a non-null GL sentinel — we reach + // the dats guard on the way. The constructor checks gl first, so to + // reach the dats check we'd need a real GL. Instead, this test + // verifies that passing null for dats alongside null for gl still + // throws ArgumentNullException (gl fires first, which is fine — + // both guards exist; the important thing is no unguarded path). + Assert.Throws(() => + new WbMeshAdapter(gl: null!, dats: null!, logger: NullLogger.Instance)); + } + + [Fact] + public void Dispose_OnUninitializedAdapter_DoesNotThrow() + { + var adapter = WbMeshAdapter.CreateUninitialized(); + adapter.Dispose(); // no-op when fields are null + adapter.Dispose(); // idempotent + } + + [Fact] + public void IncrementRefCount_OnUninitializedAdapter_NoOps() + { + var adapter = WbMeshAdapter.CreateUninitialized(); + // Should not throw, even though there's no underlying mesh manager. + adapter.IncrementRefCount(0x01000001ul); + adapter.DecrementRefCount(0x01000001ul); + } +}