diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj
index 1f35dbb..d50c6b4 100644
--- a/src/AcDream.App/AcDream.App.csproj
+++ b/src/AcDream.App/AcDream.App.csproj
@@ -18,6 +18,11 @@
implement IUniformBuffer from Chorizite.Core; NuGet PackageReferences are not
forwarded from ProjectReferences so we must declare it explicitly here. -->
+
+
+
@@ -35,12 +40,6 @@
-
-
-
diff --git a/src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs b/src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs
index 3314589..4c564a9 100644
--- a/src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs
+++ b/src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs
@@ -1,6 +1,4 @@
using System.Numerics;
-using Chorizite.OpenGLSDLBackend.Lib;
-using WorldBuilder.Shared.Models;
namespace AcDream.App.Rendering.Wb {
public class ActiveParticleEmitter {
@@ -8,11 +6,15 @@ namespace AcDream.App.Rendering.Wb {
public uint PartIndex { get; }
public Matrix4x4 LocalOffset { get; }
- // Store reference info instead of struct copy
- public ObjectLandblock? ParentLandblock { get; set; }
- public ObjectId? ParentInstanceId { get; set; }
+ // Store reference info instead of struct copy.
+ // ParentLandblock was ObjectLandblock (WB type) — typed as object? in Phase O-T7
+ // to remove the WorldBuilder project reference; no consumer reads it.
+ public object? ParentLandblock { get; set; }
+ // ParentInstanceId was WorldBuilder.Shared.Models.ObjectId? — erased to ulong? in T7;
+ // the field is stored but never read by any consumer in our codebase.
+ public ulong? ParentInstanceId { get; set; }
- public ActiveParticleEmitter(ParticleEmitterRenderer renderer, uint partIndex, Matrix4x4 localOffset, ObjectLandblock? parentLandblock = null, ObjectId? parentInstanceId = null) {
+ public ActiveParticleEmitter(ParticleEmitterRenderer renderer, uint partIndex, Matrix4x4 localOffset, object? parentLandblock = null, ulong? parentInstanceId = null) {
Renderer = renderer;
PartIndex = partIndex;
LocalOffset = localOffset;
diff --git a/src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs b/src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs
index 3b661e2..9f49a15 100644
--- a/src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs
@@ -6,7 +6,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
-using WorldBuilder.Shared.Services;
namespace AcDream.App.Rendering.Wb;
diff --git a/src/AcDream.App/Rendering/Wb/GeometryUtils.cs b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs
new file mode 100644
index 0000000..5b5ea09
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs
@@ -0,0 +1,147 @@
+using System.Numerics;
+
+// Phase O-T7: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils into
+// the AcDream.App.Rendering.Wb namespace so the WorldBuilder.Shared project
+// reference can be dropped. Only the float-precision overloads are used by
+// ObjectMeshManager (RayIntersectsSphere + RayIntersectsTriangle). The
+// double-precision overloads are kept verbatim for completeness.
+
+namespace AcDream.App.Rendering.Wb;
+
+public static class GeometryUtils {
+
+ public static bool RayIntersectsBox(Vector3 rayOrigin, Vector3 rayDirection, Vector3 min, Vector3 max, out float distance) {
+ distance = 0;
+ float tmin = 0.0f;
+ float tmax = float.MaxValue;
+
+ if (Math.Abs(rayDirection.X) < 1e-7f) {
+ if (rayOrigin.X < min.X || rayOrigin.X > max.X) return false;
+ }
+ else {
+ float invD = 1.0f / rayDirection.X;
+ float t0 = (min.X - rayOrigin.X) * invD;
+ float t1 = (max.X - rayOrigin.X) * invD;
+ if (t0 > t1) (t0, t1) = (t1, t0);
+ tmin = Math.Max(tmin, t0);
+ tmax = Math.Min(tmax, t1);
+ if (tmin > tmax) return false;
+ }
+
+ if (Math.Abs(rayDirection.Y) < 1e-7f) {
+ if (rayOrigin.Y < min.Y || rayOrigin.Y > max.Y) return false;
+ }
+ else {
+ float invD = 1.0f / rayDirection.Y;
+ float t0 = (min.Y - rayOrigin.Y) * invD;
+ float t1 = (max.Y - rayOrigin.Y) * invD;
+ if (t0 > t1) (t0, t1) = (t1, t0);
+ tmin = Math.Max(tmin, t0);
+ tmax = Math.Min(tmax, t1);
+ if (tmin > tmax) return false;
+ }
+
+ if (Math.Abs(rayDirection.Z) < 1e-7f) {
+ if (rayOrigin.Z < min.Z || rayOrigin.Z > max.Z) return false;
+ }
+ else {
+ float invD = 1.0f / rayDirection.Z;
+ float t0 = (min.Z - rayOrigin.Z) * invD;
+ float t1 = (max.Z - rayOrigin.Z) * invD;
+ if (t0 > t1) (t0, t1) = (t1, t0);
+ tmin = Math.Max(tmin, t0);
+ tmax = Math.Min(tmax, t1);
+ if (tmin > tmax) return false;
+ }
+
+ distance = tmin;
+ return true;
+ }
+
+ public static bool RayIntersectsTriangle(Vector3 origin, Vector3 direction, Vector3 v0, Vector3 v1, Vector3 v2, out float t) {
+ t = 0;
+ Vector3 edge1 = v1 - v0;
+ Vector3 edge2 = v2 - v0;
+ Vector3 h = Vector3.Cross(direction, edge2);
+ float a = Vector3.Dot(edge1, h);
+
+ if (a > -0.00001f && a < 0.00001f) return false;
+
+ float f = 1.0f / a;
+ Vector3 s = origin - v0;
+ float u = f * Vector3.Dot(s, h);
+
+ if (u < 0.0f || u > 1.0f) return false;
+
+ Vector3 q = Vector3.Cross(s, edge1);
+ float v = f * Vector3.Dot(direction, q);
+
+ if (v < 0.0f || u + v > 1.0f) return false;
+
+ t = f * Vector3.Dot(edge2, q);
+ return t > 0.00001f;
+ }
+
+ public static bool RayIntersectsSphere(Vector3 rayOrigin, Vector3 rayDirection, Vector3 sphereOrigin, float sphereRadius, out float distance) {
+ distance = 0;
+ Vector3 l = sphereOrigin - rayOrigin;
+ float tca = Vector3.Dot(l, rayDirection);
+ if (tca < 0) return false;
+ float d2 = Vector3.Dot(l, l) - tca * tca;
+ float r2 = sphereRadius * sphereRadius;
+ if (d2 > r2) return false;
+ float thc = MathF.Sqrt(r2 - d2);
+ distance = tca - thc;
+ return true;
+ }
+
+ public static ushort PackKey(int x, int y) => (ushort)((x << 8) | y);
+
+ ///
+ /// Converts a quaternion to Euler angles (in degrees) using the ZYX convention.
+ ///
+ public static Vector3 QuaternionToEuler(Quaternion q) {
+ float x = q.X, y = q.Y, z = q.Z, w = q.W;
+ float roll, pitch, yaw;
+
+ float sinr_cosp = 2 * (w * x + y * z);
+ float cosr_cosp = 1 - 2 * (x * x + y * y);
+ roll = (float)Math.Atan2(sinr_cosp, cosr_cosp);
+
+ float sinp = 2 * (w * y - z * x);
+ if (Math.Abs(sinp) >= 1)
+ pitch = (float)Math.CopySign(Math.PI / 2, sinp);
+ else
+ pitch = (float)Math.Asin(sinp);
+
+ float siny_cosp = 2 * (w * z + x * y);
+ float cosy_cosp = 1 - 2 * (y * y + z * z);
+ yaw = (float)Math.Atan2(siny_cosp, cosy_cosp);
+
+ return new Vector3(roll, pitch, yaw) * (180.0f / (float)Math.PI);
+ }
+
+ ///
+ /// Converts Euler angles (in degrees) to a quaternion using the ZYX convention.
+ ///
+ public static Quaternion EulerToQuaternion(Vector3 euler) {
+ Vector3 rads = euler * (MathF.PI / 180.0f);
+ float roll = rads.X;
+ float pitch = rads.Y;
+ float yaw = rads.Z;
+
+ float cr = MathF.Cos(roll * 0.5f);
+ float sr = MathF.Sin(roll * 0.5f);
+ float cp = MathF.Cos(pitch * 0.5f);
+ float sp = MathF.Sin(pitch * 0.5f);
+ float cy = MathF.Cos(yaw * 0.5f);
+ float sy = MathF.Sin(yaw * 0.5f);
+
+ return new Quaternion(
+ sr * cp * cy - cr * sp * sy,
+ cr * sp * cy + sr * cp * sy,
+ cr * cp * sy - sr * sp * cy,
+ cr * cp * cy + sr * sp * sy
+ );
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/IDatReaderWriter.cs b/src/AcDream.App/Rendering/Wb/IDatReaderWriter.cs
new file mode 100644
index 0000000..a287bf7
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/IDatReaderWriter.cs
@@ -0,0 +1,114 @@
+using DatReaderWriter;
+using DatReaderWriter.Enums;
+using DatReaderWriter.Lib.IO;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+
+// Phase O-T7: verbatim copy of WorldBuilder.Shared.Services.IDatReaderWriter +
+// IDatDatabase into the AcDream.App.Rendering.Wb namespace so the
+// WorldBuilder.Shared project reference can be dropped.
+// The only consumer of IDatReaderWriter in acdream is DatCollectionAdapter +
+// ObjectMeshManager, both already in this namespace.
+
+namespace AcDream.App.Rendering.Wb;
+
+///
+/// Interface for the dat reader/writer
+///
+public interface IDatReaderWriter : IDisposable {
+ ///
+ /// Gets the source directory of the DAT files.
+ ///
+ string SourceDirectory { get; }
+
+ ///
+ /// Tries to get the raw bytes of a file from a specific region database.
+ ///
+ bool TryGetFileBytes(uint regionId, uint fileId, ref byte[] bytes, out int bytesRead);
+
+ ///
+ /// The portal database
+ ///
+ IDatDatabase Portal { get; }
+
+ ///
+ /// The cell region databases. Each key is a cell region ID
+ ///
+ ReadOnlyDictionary CellRegions { get; }
+
+ ///
+ /// The high res database
+ ///
+ IDatDatabase HighRes { get; }
+
+ ///
+ /// The language database
+ ///
+ IDatDatabase Language { get; }
+
+ ///
+ /// A mapping of region ids to region dat file entry ids. key: region id, value: region dat file entry
+ ///
+ ReadOnlyDictionary RegionFileMap { get; }
+
+ ///
+ /// Gets the current portal iteration.
+ ///
+ int PortalIteration { get; }
+
+ ///
+ /// Gets the current cell iteration (from the first cell region).
+ ///
+ int CellIteration { get; }
+
+ ///
+ /// Gets the current high res iteration.
+ ///
+ int HighResIteration { get; }
+
+ ///
+ /// Gets the current language iteration.
+ ///
+ int LanguageIteration { get; }
+
+ /// Attempts to save a database object to the appropriate DAT.
+ bool TrySave(T obj, int iteration = 0) where T : IDBObj;
+
+ /// Attempts to save a database object to the appropriate DAT for a specific region.
+ bool TrySave(uint regionId, T obj, int iteration = 0) where T : IDBObj;
+
+ ///
+ /// Resolution of a data ID to a database and type
+ ///
+ public record IdResolution(IDatDatabase Database, DBObjType Type);
+
+ ///
+ /// Resolves a data ID to all possible databases and types.
+ ///
+ public IEnumerable ResolveId(uint id);
+}
+
+///
+/// Interface for a dat database, providing methods to retrieve files and objects.
+///
+public interface IDatDatabase : IDisposable {
+ DatDatabase Db { get; }
+
+ /// Retrieves the current iteration of the database.
+ int Iteration { get; }
+
+ /// Retrieves all file IDs of a specific type.
+ public IEnumerable GetAllIdsOfType() where T : IDBObj;
+
+ /// Attempts to retrieve a database object by its file ID.
+ public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj;
+
+ /// Attempts to retrieve the raw bytes of a file by its ID.
+ bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value);
+
+ /// Attempts to retrieve the raw bytes of a file by its ID into a provided buffer.
+ bool TryGetFileBytes(uint fileId, ref byte[] bytes, out int bytesRead);
+
+ /// Attempts to save a database object.
+ bool TrySave(T obj, int iteration = 0) where T : IDBObj;
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
index 4889a0f..7443fc5 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
@@ -1,6 +1,5 @@
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
-using Chorizite.OpenGLSDLBackend.Lib;
using Silk.NET.OpenGL;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
@@ -158,7 +157,7 @@ namespace AcDream.App.Rendering.Wb {
///
public void Bind() {
- BaseObjectRenderManager.CurrentIBO = 0;
+ RenderStateCache.CurrentIBO = 0;
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
index 27d83cd..827dc8a 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
@@ -1,6 +1,5 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
-using Chorizite.OpenGLSDLBackend.Lib;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
@@ -108,7 +107,7 @@ namespace AcDream.App.Rendering.Wb {
GLHelpers.CheckErrors(GL);
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
- BaseObjectRenderManager.CurrentAtlas = 0;
+ RenderStateCache.CurrentAtlas = 0;
GL.GetInteger(GLEnum.TextureBinding2D, out int oldBinding);
GL.BindTexture(GLEnum.Texture2D, _texture);
@@ -147,7 +146,7 @@ namespace AcDream.App.Rendering.Wb {
public void Bind(int slot = 0) {
if (slot == 0) {
- BaseObjectRenderManager.CurrentAtlas = 0;
+ RenderStateCache.CurrentAtlas = 0;
}
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
index 11e20d9..eb69e11 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
@@ -1,7 +1,6 @@
using AcDream.Core.Rendering.Wb;
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
-using Chorizite.OpenGLSDLBackend.Lib;
// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
using Microsoft.Extensions.Logging;
@@ -274,7 +273,7 @@ namespace AcDream.App.Rendering.Wb {
GLHelpers.CheckErrors(GL);
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
- BaseObjectRenderManager.CurrentAtlas = 0;
+ RenderStateCache.CurrentAtlas = 0;
GL.GetInteger(GLEnum.TextureBinding2DArray, out int oldBinding);
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
index d2e1f4c..fa6f225 100644
--- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
+++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
@@ -15,7 +15,6 @@ using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using WorldBuilder.Shared.Services;
using AcDream.Core.Rendering.Wb;
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
@@ -24,7 +23,6 @@ using BCnEncoder.Shared;
using BCnEncoder.ImageSharp;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
-using WorldBuilder.Shared.Lib;
namespace AcDream.App.Rendering.Wb {
///
diff --git a/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs b/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
index fcfe139..0841ec6 100644
--- a/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
+++ b/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using Chorizite.Core.Render;
-using Chorizite.OpenGLSDLBackend.Lib;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
@@ -187,9 +186,7 @@ namespace AcDream.App.Rendering.Wb {
gl.ActiveTexture(TextureUnit.Texture0);
gl.BindTexture(GLEnum.Texture2DArray, (uint)_currentAtlas.NativePtr);
- // T4 interim: BaseObjectRenderManager state fields stay on the WB type until T7
- // when the WorldBuilder project reference is dropped entirely.
- BaseObjectRenderManager.CurrentAtlas = (uint)_currentAtlas.Slot;
+ RenderStateCache.CurrentAtlas = (uint)_currentAtlas.Slot;
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
unsafe {
@@ -210,9 +207,8 @@ namespace AcDream.App.Rendering.Wb {
gl.DepthMask(true);
_allParticles.Clear();
- // T4 interim: BaseObjectRenderManager state fields stay on the WB type until T7
- BaseObjectRenderManager.CurrentVAO = 0;
- BaseObjectRenderManager.CurrentIBO = 0;
+ RenderStateCache.CurrentVAO = 0;
+ RenderStateCache.CurrentIBO = 0;
}
public void End() {
diff --git a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs
new file mode 100644
index 0000000..840a0bc
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs
@@ -0,0 +1,20 @@
+namespace AcDream.App.Rendering.Wb;
+
+///
+/// Tracks currently-bound GL state to skip redundant rebinds across the
+/// WB-derived render path. Previously these were static fields on
+/// BaseObjectRenderManager in the WorldBuilder.Shared project; inlined
+/// here in Phase O-T7 to eliminate the WorldBuilder project reference.
+///
+/// Semantics are identical to the WB originals:
+/// CurrentAtlas — slot index of the currently bound texture atlas.
+/// CurrentVAO — OpenGL name of the currently bound vertex array object.
+/// CurrentIBO — OpenGL name of the currently bound index buffer object.
+/// Sentinel value 0 means "no valid binding cached."
+///
+public static class RenderStateCache
+{
+ public static uint CurrentAtlas = 0;
+ public static uint CurrentVAO = 0;
+ public static uint CurrentIBO = 0;
+}
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
index 6dbf0c8..f62790d 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
@@ -6,7 +6,6 @@ using AcDream.Core.Meshing;
using AcDream.Core.Rendering;
using AcDream.Core.Terrain;
using AcDream.Core.World;
-using Chorizite.OpenGLSDLBackend.Lib;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb;
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
index ed8b8ef..f07aca1 100644
--- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -1,16 +1,12 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
using AcDream.Core.Meshing;
using AcDream.Core.Rendering;
-using Chorizite.OpenGLSDLBackend.Lib;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Silk.NET.OpenGL;
-using WorldBuilder.Shared.Services;
namespace AcDream.App.Rendering.Wb;
@@ -20,30 +16,19 @@ namespace AcDream.App.Rendering.Wb;
/// so the rest of the renderer doesn't need to know about WB's types directly.
///
///
-/// The adapter constructs its own DefaultDatReaderWriter internally; it
-/// does NOT share file handles with our DatCollection. This duplicates
-/// index-cache memory (~50–100 MB) but keeps the two subsystems fully decoupled.
-/// Acceptable for Phase N.4 foundation work (plan Adjustment 1).
+/// As of Phase O-T7, all DAT I/O routes through
+/// (backed by our shared ) — the separate
+/// DefaultDatReaderWriter file-handle set has been removed.
///
///
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
{
private readonly OpenGLGraphicsDevice? _graphicsDevice;
- private readonly DefaultDatReaderWriter? _wbDats;
private readonly ObjectMeshManager? _meshManager;
private readonly DatCollection? _dats;
private readonly AcSurfaceMetadataTable _metadataTable = new();
private readonly HashSet _metadataPopulated = new();
- ///
- /// EnvCell ids we've requested via PrepareMeshDataAsync but not yet
- /// seen completion for in Tick(). Used by the [indoor-upload] probe
- /// to log requested + completed pairs. Cleared per completion;
- /// missing completions after a few seconds indicate WB silently
- /// returned null (hypothesis H1 in the design spec).
- ///
- private readonly HashSet _pendingEnvCellRequests = new();
-
///
/// True when this instance was created via ;
/// all public methods no-op when uninitialized.
@@ -53,13 +38,13 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
private bool _disposed;
///
- /// Constructs the full WB pipeline: OpenGLGraphicsDevice → DefaultDatReaderWriter
+ /// Constructs the full WB pipeline: OpenGLGraphicsDevice → DatCollectionAdapter
/// → ObjectMeshManager.
///
/// Active Silk.NET GL context. Must be bound to the current
/// thread (construction runs GL queries; call from OnLoad).
- /// Path to the dat directory (same as the one supplied
- /// to our DatCollection). DefaultDatReaderWriter opens its own file handles.
+ /// Path to the dat directory. Retained for API compatibility;
+ /// DatCollectionAdapter routes all DAT I/O through our shared DatCollection.
/// acdream's DatCollection, used to populate the surface
/// metadata side-table via GfxObjMesh.Build. Shares file handles with
/// the rest of the client; read-only access from the render thread.
@@ -75,12 +60,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_dats = dats;
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
_graphicsDevice.ParticleBatcher = new ParticleBatcher(_graphicsDevice);
- _wbDats = new DefaultDatReaderWriter(datDir);
- // Phase 2 diagnostic — replace NullLogger with a Console-backed
- // logger so WB's internal catch block at ObjectMeshManager.cs:589
- // (and similar) surfaces its swallowed exceptions instead of
- // dropping them. ConsoleErrorLogger filters to LogLevel.Error+
- // so successful operations stay quiet.
+ // ConsoleErrorLogger surfaces WB's silently-caught exceptions
+ // (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
_meshManager = new ObjectMeshManager(
_graphicsDevice,
new DatCollectionAdapter(dats),
@@ -186,80 +167,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
// isSetup: false — acdream's MeshRefs already carry expanded
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
// unused.
- var prepTask = _meshManager.PrepareMeshDataAsync(id, isSetup: false);
-
- // [indoor-upload] requested probe — only for EnvCell ids.
- if (RenderingDiagnostics.IsEnvCellId(id) && RenderingDiagnostics.ProbeIndoorUploadEnabled)
- {
- bool hadRenderDataAtRequest = _meshManager.HasRenderData(id);
- _pendingEnvCellRequests.Add(id);
- Console.WriteLine($"[indoor-upload] requested cellId=0x{id:X8} hadRenderData={hadRenderDataAtRequest}");
-
- // Phase 2 — surface what WB's catch block silently swallows.
- // ObjectMeshManager.PrepareMeshData has a try/catch at line 589
- // that calls _logger.LogError(ex, ...) — but we construct
- // ObjectMeshManager with NullLogger.Instance so the log is
- // dropped. This continuation captures the same data scoped to
- // EnvCell ids only. Runs on ThreadPool; non-blocking. Zero cost
- // when the probe is off.
- ulong cellId = id;
- _ = prepTask.ContinueWith(t =>
- {
- if (t.IsFaulted && t.Exception is not null)
- {
- var ex = t.Exception.InnerException ?? t.Exception;
- var stack = (ex.StackTrace ?? "").Split('\n')
- .Take(3).Select(s => s.Trim()).Where(s => s.Length > 0);
- Console.WriteLine(
- $"[indoor-upload] FAILED cellId=0x{cellId:X8} " +
- $"exception={ex.GetType().Name}: {ex.Message} " +
- $"stack=[{string.Join(" | ", stack)}]");
- }
- else if (t.IsCompletedSuccessfully && t.Result is null)
- {
- // Phase 2 cause-narrowing: WB's PrepareMeshData can return
- // null for several reasons (ResolveId empty / TryGet
- // failed / type Unknown). Cross-check against acdream's own
- // DatCollection — if WE find the cell but WB doesn't, the
- // divergence is between dat readers, not a missing record.
- bool ourCellFound = false;
- try
- {
- ourCellFound = _dats?.Cell.TryGet(
- (uint)cellId, out _) ?? false;
- }
- catch { /* swallow — this is best-effort diagnostic */ }
-
- int wbResolveCount = -1;
- string wbSelectedType = "none";
- bool wbDbTryGetEnvCell = false;
- bool wbDbIsPortal = false;
- try
- {
- var wbResolutions = _wbDats?.ResolveId((uint)cellId).ToList();
- wbResolveCount = wbResolutions?.Count ?? -1;
- if (wbResolutions is not null && wbResolutions.Count > 0)
- {
- var selected = wbResolutions
- .OrderByDescending(r => r.Database == _wbDats!.Portal)
- .First();
- wbSelectedType = selected.Type.ToString();
- wbDbIsPortal = selected.Database == _wbDats!.Portal;
- try { wbDbTryGetEnvCell = selected.Database.TryGet((uint)cellId, out _); } catch {}
- }
- }
- catch { /* swallow — best-effort */ }
-
- Console.WriteLine(
- $"[indoor-upload] NULL_RESULT cellId=0x{cellId:X8} " +
- $"ourCellDb.TryGet={ourCellFound} " +
- $"wbResolveId.Count={wbResolveCount} " +
- $"wbSelectedType={wbSelectedType} " +
- $"wbDbIsPortal={wbDbIsPortal} " +
- $"wbDbTryGet={wbDbTryGetEnvCell}");
- }
- }, TaskScheduler.Default);
- }
+ _meshManager.PrepareMeshDataAsync(id, isSetup: false);
}
}
@@ -298,26 +206,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_graphicsDevice!.ProcessGLQueue();
while (_meshManager!.StagedMeshData.TryDequeue(out var meshData))
{
- // [indoor-upload] completed probe — check BEFORE upload so we
- // see what WB actually produced (vertex counts, parts) before
- // any post-upload mutation.
- bool isPendingEnvCell = RenderingDiagnostics.ProbeIndoorUploadEnabled
- && _pendingEnvCellRequests.Remove(meshData.ObjectId);
-
- var renderData = _meshManager.UploadMeshData(meshData);
-
- if (isPendingEnvCell)
- {
- int parts = meshData.SetupParts?.Count ?? 0;
- bool hasGeom = meshData.EnvCellGeometry is not null;
- int cellGeomVerts = meshData.EnvCellGeometry?.Vertices?.Length ?? 0;
- bool uploadOk = renderData is not null;
- Console.WriteLine(
- $"[indoor-upload] completed cellId=0x{meshData.ObjectId:X8} " +
- $"isSetup={meshData.IsSetup} parts={parts} " +
- $"hasEnvCellGeom={hasGeom} cellGeomVerts={cellGeomVerts} " +
- $"uploadOk={uploadOk}");
- }
+ _meshManager.UploadMeshData(meshData);
}
}
@@ -342,7 +231,6 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
if (_disposed) return;
_disposed = true;
_meshManager?.Dispose();
- _wbDats?.Dispose();
_graphicsDevice?.Dispose();
}
}
diff --git a/src/AcDream.Core/AcDream.Core.csproj b/src/AcDream.Core/AcDream.Core.csproj
index 1ac800c..eafd339 100644
--- a/src/AcDream.Core/AcDream.Core.csproj
+++ b/src/AcDream.Core/AcDream.Core.csproj
@@ -8,6 +8,7 @@
+
@@ -18,13 +19,5 @@
-
-
-
diff --git a/tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs b/tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs
deleted file mode 100644
index feaa28f..0000000
--- a/tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using AcDream.Core.Terrain;
-using Xunit;
-using Xunit.Abstractions;
-using WbTerrainUtils = WorldBuilder.Shared.Modules.Landscape.Lib.TerrainUtils;
-using WbCellSplitDirection = WorldBuilder.Shared.Modules.Landscape.Models.CellSplitDirection;
-
-namespace AcDream.Core.Tests.Terrain;
-
-///
-/// Phase N.5b data-collection test: quantifies how often WB's
-/// TerrainUtils.CalculateSplitDirection disagrees with acdream's
-/// TerrainBlending.CalculateSplitDirection (which retail uses
-/// per CLandBlockStruct::ConstructPolygons at retail address
-/// 00531d10; named-retail decomp lines 316042-316144 contain
-/// the exact constants 0x0CCAC033 / 0x6C1AC587 / 0x421BE3BD /
-/// 0x519B8F25).
-///
-/// Sweeps every (lbX, lbY, cellX, cellY) tuple in the world map
-/// (255 x 255 landblocks x 64 cells = ~4.16M cells) and reports the
-/// disagreement rate, per-landblock worst case, and a few named
-/// representative landblocks. The number drives the Path A/B/C
-/// decision in the N.5b brainstorm:
-/// - Low disagreement <5% : Path A's risk is bounded
-/// - Medium 5-20% : Path B (fork-patch WB) preferred
-/// - High >20% : Path B/C strongly preferred
-///
-public class SplitFormulaDivergenceTest
-{
- private readonly ITestOutputHelper _out;
-
- public SplitFormulaDivergenceTest(ITestOutputHelper output) => _out = output;
-
- [Fact]
- public void Quantify_RetailVsWb_DivergenceRate()
- {
- // Two divergence flavors are tracked simultaneously:
- //
- // rawDisagree : retail-enum != wb-enum (pure formula output)
- // diagonalDisagree: retail-actually-paints-diagonal !=
- // wb-actually-paints-diagonal (effective geometry)
- //
- // The two differ because the enums are SEMANTICALLY INVERTED:
- // - acdream `CellSplitDirection.SWtoNE` -> renderer paints BL->TR
- // (SW-NE diagonal). Matches retail per AC2D Landblocks.cpp:400-412
- // where FSplitNESW=true wraps a TRIANGLE_FAN [BL, BR, TR, TL] =
- // diagonal BL-TR.
- // - WB `CellSplitDirection.SWtoNE` -> WB's TerrainGeometryGenerator
- // emits triangles {BL,TL,BR}+{BR,TL,TR} which share the BR-TL
- // diagonal (SE-NW direction). The enum name is misleading; what
- // WB actually draws is the OTHER diagonal.
- //
- // So the question "would WB's pipeline produce the same diagonals as
- // retail's pipeline?" is answered by `diagonalDisagree`, not
- // `rawDisagree`. If diagonalDisagree is near 0%, WB's formula +
- // renderer happen to compose into a correct pipeline (despite the
- // confusing labels). If diagonalDisagree is ~50%, the two pipelines
- // truly diverge and Path A would visibly break terrain on every
- // landblock.
-
- const int lbCount = 255;
- const int cellsPerSide = 8;
- long totalCells = 0;
- long rawDisagree = 0;
- long diagonalDisagree = 0;
-
- int worstLbDiag = 0;
- uint worstLbX = 0, worstLbY = 0;
- int bestLbDiag = 64;
- uint bestLbX = 0, bestLbY = 0;
-
- for (uint lbX = 0; lbX < lbCount; lbX++)
- for (uint lbY = 0; lbY < lbCount; lbY++)
- {
- int lbDiagDisagree = 0;
- for (uint cx = 0; cx < cellsPerSide; cx++)
- for (uint cy = 0; cy < cellsPerSide; cy++)
- {
- bool retailEnumSWtoNE =
- TerrainBlending.CalculateSplitDirection(lbX, cx, lbY, cy)
- == CellSplitDirection.SWtoNE;
- bool wbEnumSWtoNE =
- WbTerrainUtils.CalculateSplitDirection(lbX, cx, lbY, cy)
- == WbCellSplitDirection.SWtoNE;
-
- // What diagonal each pipeline actually paints.
- bool retailPaintsBLtoTR = retailEnumSWtoNE; // direct mapping
- bool wbPaintsBLtoTR = !wbEnumSWtoNE; // inverted mapping
-
- totalCells++;
- if (retailEnumSWtoNE != wbEnumSWtoNE) rawDisagree++;
- if (retailPaintsBLtoTR != wbPaintsBLtoTR)
- {
- diagonalDisagree++;
- lbDiagDisagree++;
- }
- }
-
- if (lbDiagDisagree > worstLbDiag)
- {
- worstLbDiag = lbDiagDisagree;
- worstLbX = lbX;
- worstLbY = lbY;
- }
- if (lbDiagDisagree < bestLbDiag)
- {
- bestLbDiag = lbDiagDisagree;
- bestLbX = lbX;
- bestLbY = lbY;
- }
- }
-
- double rawPct = 100.0 * rawDisagree / totalCells;
- double diagPct = 100.0 * diagonalDisagree / totalCells;
-
- _out.WriteLine($"=== Phase N.5b — terrain split formula divergence ===");
- _out.WriteLine($"Sweep: {lbCount}x{lbCount} landblocks, {cellsPerSide*cellsPerSide} cells each");
- _out.WriteLine($"Total cells: {totalCells:N0}");
- _out.WriteLine("");
- _out.WriteLine($"RAW enum-output disagreement : {rawDisagree,12:N0} ({rawPct:F2}%)");
- _out.WriteLine($" (compares retail-enum vs wb-enum, NOT what each system actually draws)");
- _out.WriteLine("");
- _out.WriteLine($"DIAGONAL-actually-painted disagreement: {diagonalDisagree,12:N0} ({diagPct:F2}%)");
- _out.WriteLine($" (compares retail-paints-BL->TR vs wb-paints-BL->TR; this is the");
- _out.WriteLine($" number that determines whether Path A visibly works)");
- _out.WriteLine("");
- _out.WriteLine($"Worst landblock (diagonal): 0x{worstLbX:X2}{worstLbY:X2} disagrees on {worstLbDiag}/64 cells ({100.0*worstLbDiag/64:F1}%)");
- _out.WriteLine($"Best landblock (diagonal): 0x{bestLbX:X2}{bestLbY:X2} disagrees on {bestLbDiag}/64 cells ({100.0*bestLbDiag/64:F1}%)");
-
- // Specific landblocks of interest (per N.5b handoff representative set).
- var representative = new (string name, uint lbX, uint lbY)[]
- {
- ("Holtburg town", 0xA9, 0xB0),
- ("Holtburg LB 0xA9B1", 0xA9, 0xB1),
- ("Foundry-area", 0x80, 0x80),
- ("Cragstone", 0xCB, 0x99),
- ("Direlands sample", 0xC0, 0x40),
- ("MapOrigin 0x0000", 0x00, 0x00),
- ("MapCorner 0xFEFE", 0xFE, 0xFE),
- ("Mid-map 0x7F7F", 0x7F, 0x7F),
- ("Subway dungeon LB 0x0185 outdoor part", 0x01, 0x85),
- };
-
- _out.WriteLine("");
- _out.WriteLine("Representative landblocks (diagonal-actually-painted disagreement):");
- foreach (var (name, lbX, lbY) in representative)
- {
- int dis = 0;
- for (uint cx = 0; cx < 8; cx++)
- for (uint cy = 0; cy < 8; cy++)
- {
- bool retailEnum = TerrainBlending.CalculateSplitDirection(lbX, cx, lbY, cy) == CellSplitDirection.SWtoNE;
- bool wbEnum = WbTerrainUtils.CalculateSplitDirection(lbX, cx, lbY, cy) == WbCellSplitDirection.SWtoNE;
- bool retailPaintsBLtoTR = retailEnum;
- bool wbPaintsBLtoTR = !wbEnum;
- if (retailPaintsBLtoTR != wbPaintsBLtoTR) dis++;
- }
- _out.WriteLine($" 0x{lbX:X2}{lbY:X2} {dis,2}/64 cells disagree ({100.0*dis/64:F1}%) {name}");
- }
-
- // Soft-floor on the DIAGONAL comparison: if diagPct is near 0% the
- // formulas are equivalent post-inversion (Path A would just work
- // visually; the only "bug" is enum naming). If diagPct is well
- // above 0%, Path A truly breaks terrain.
- // Soft-ceiling: an inversion of inversion shouldn't push past ~70%.
- Assert.True(diagPct >= 0 && diagPct <= 100,
- $"Sanity: diagonal disagreement out of range (rate={diagPct:F2}%)");
- }
-}