From b8e0857b87c0f3974908ff4ddaa5301222bebcf5 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 27 Apr 2026 08:50:02 +0200 Subject: [PATCH] =?UTF-8?q?tools(probe):=20add=20RainMeshProbe=20=E2=80=94?= =?UTF-8?q?=20dumps=20rain=20mesh=20surface=20+=20polygons=20+=20build=20c?= =?UTF-8?q?ounts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sibling of StarsProbe/WeatherEnumerator. Targets GfxObjs 0x01004C42 and 0x01004C44 (the two rain cylinders). For each: dumps the Surface raw record (Type bits, Translucency, Luminosity, Diffuse, ColorValue, OrigTextureId), every polygon's SidesType + Stippling + hasPos/hasNeg emission flags (mirroring GfxObjMesh.Build's neg-side rule), and the final GfxObjMesh.Build() submesh+index counts. Built per independent code-review §5: "Run one targeted probe... if one cylinder has more than 48 indices per side-equivalent, fix the duplicate-side/cull behavior together with the surface-opacity uniform." Probe results (rain_mesh_probe.log, not committed): Surface 0x080000C5: Type=0x10112 (Base1Image|Translucent|Alpha|Additive), Translucency=0.5000, Luminosity=0.1484, OrigTextureId=0x050016A6. Polygons: all 8 are Stippling=Positive, SidesType=None, hasNeg=False. Build output: 1 submesh, 24 verts, 48 indices = 8 walls × 2 tris × 3. → SINGLE-SIDED (the duplicate-side hypothesis is disconfirmed). Confirmed: the rim brightness excess is purely from Translucency not being plumbed (acdream draws rain at full alpha=1.0 instead of retail's 0.5). Bonus finding: surface.Luminosity=0.1484 is also ignored by the renderer's `effEmissive = (luminosity > 0) ? luminosity : sub.SurfLuminosity` fallback (the local `luminosity` defaults to 1.0 so the fallback never fires) — but that's keyed on the LUMINOUS flag bit (0x40), which the rain surface does NOT have. Filed as follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/RainMeshProbe/Program.cs | 157 +++++++++++++++++++++++ tools/RainMeshProbe/RainMeshProbe.csproj | 15 +++ 2 files changed, 172 insertions(+) create mode 100644 tools/RainMeshProbe/Program.cs create mode 100644 tools/RainMeshProbe/RainMeshProbe.csproj diff --git a/tools/RainMeshProbe/Program.cs b/tools/RainMeshProbe/Program.cs new file mode 100644 index 0000000..37475b1 --- /dev/null +++ b/tools/RainMeshProbe/Program.cs @@ -0,0 +1,157 @@ +// RainMeshProbe — independent code-review recommended probe (Bug A, post-#26). +// +// Per Report 1's §5: "Run one targeted probe for 0x01004C42/0x01004C44: print +// surface raw type/translucency, each polygon's SidesType/Stippling, and +// GfxObjMesh.Build() submesh/index counts. If one cylinder has more than 48 +// indices per side-equivalent, fix the duplicate-side/cull behavior together +// with the surface-opacity uniform." +// +// The cylinder has 8 wall quads. With fan-triangulation each quad → 2 tris → +// 6 indices, total 48 indices per side. If pos-only emission: 48. If pos+neg: +// 96. The threshold tells us whether double-sided drawing is happening. +using System; +using System.IO; +using System.Linq; +using DatReaderWriter; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Options; +using DatReaderWriter.Types; +using AcDream.Core.Meshing; +using SysEnv = System.Environment; + +string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile), + "Documents", "Asheron's Call"); +Console.WriteLine($"datDir = {datDir}"); +using var dats = new DatCollection(datDir, DatAccessType.Read); + +uint[] gfxIds = { 0x01004C42u, 0x01004C44u }; +foreach (uint gid in gfxIds) ProbeRain(dats, gid); +return 0; + +static void ProbeRain(DatCollection dats, uint gid) +{ + Console.WriteLine(); + Console.WriteLine($"================ GfxObj 0x{gid:X8} ================"); + if (!dats.TryGet(gid, out var go) || go is null) + { + Console.WriteLine(" (NOT FOUND)"); + return; + } + + Console.WriteLine($" Flags={go.Flags}"); + Console.WriteLine($" VertexArray.Vertices.Count={go.VertexArray?.Vertices.Count ?? 0}"); + Console.WriteLine($" Polygons.Count={go.Polygons?.Count ?? 0}"); + Console.WriteLine($" Surfaces.Count={go.Surfaces?.Count ?? 0}"); + Console.WriteLine($" PhysicsPolygons.Count={go.PhysicsPolygons?.Count ?? 0}"); + Console.WriteLine($" SortCenter=({go.SortCenter.X:F2},{go.SortCenter.Y:F2},{go.SortCenter.Z:F2})"); + + // ----- Per-Surface dump ----- + Console.WriteLine(); + Console.WriteLine(" --- Surfaces (raw dat record) ---"); + if (go.Surfaces is { Count: > 0 }) + { + for (int i = 0; i < go.Surfaces.Count; i++) + { + uint sid = (uint)go.Surfaces[i]; + Console.WriteLine($" Surface[{i}] = 0x{sid:X8}"); + if (!dats.TryGet(sid, out var surf) || surf is null) + { + Console.WriteLine(" (Surface NOT FOUND)"); + continue; + } + uint typeRaw = (uint)surf.Type; + Console.WriteLine($" Type=0x{typeRaw:X8} ({surf.Type})"); + Console.WriteLine($" decoded bits:"); + DumpFlagBits(typeRaw); + Console.WriteLine($" Translucency={surf.Translucency:F4} (1.0 - x = opacity = {1f - surf.Translucency:F4})"); + Console.WriteLine($" Luminosity={surf.Luminosity:F4}"); + Console.WriteLine($" Diffuse={surf.Diffuse:F4}"); + Console.WriteLine($" ColorValue=" + (surf.ColorValue is null ? "null" : + $"A:{surf.ColorValue.Alpha} R:{surf.ColorValue.Red} G:{surf.ColorValue.Green} B:{surf.ColorValue.Blue}")); + Console.WriteLine($" OrigTextureId=0x{(uint)surf.OrigTextureId:X8}"); + Console.WriteLine($" OrigPaletteId=0x{(uint)surf.OrigPaletteId:X8}"); + } + } + + // ----- Per-Polygon dump ----- + Console.WriteLine(); + Console.WriteLine(" --- Polygons (sides + stippling — checks Report 1 hypothesis) ---"); + if (go.Polygons is { Count: > 0 }) + { + int posCount = 0, negCount = 0; + foreach (var kv in go.Polygons) + { + var p = kv.Value; + // Mirror the GfxObjMesh.Build() emission rule (lines 71-91): + bool hasPos = !p.Stippling.HasFlag(StipplingType.NoPos); + bool hasNeg = + p.Stippling.HasFlag(StipplingType.Negative) || + p.Stippling.HasFlag(StipplingType.Both) || + (!p.Stippling.HasFlag(StipplingType.NoNeg) && p.SidesType == CullMode.Clockwise); + if (hasPos) posCount++; + if (hasNeg) negCount++; + + Console.WriteLine( + $" Poly[{kv.Key,3}] VertexIds={p.VertexIds.Count} " + + $"PosSurface={p.PosSurface} NegSurface={p.NegSurface} " + + $"Stippling={p.Stippling} SidesType={p.SidesType} " + + $"hasPos={hasPos} hasNeg={hasNeg} " + + $"PosUVIdx={p.PosUVIndices.Count} NegUVIdx={p.NegUVIndices.Count}"); + } + Console.WriteLine($" Build emission summary: pos-side polys={posCount} neg-side polys={negCount}"); + } + + // ----- GfxObjMesh.Build() output ----- + Console.WriteLine(); + Console.WriteLine(" --- GfxObjMesh.Build() output ---"); + var subs = GfxObjMesh.Build(go, dats); + Console.WriteLine($" Submesh count: {subs.Count}"); + int totalVerts = 0, totalIndices = 0; + for (int i = 0; i < subs.Count; i++) + { + var s = subs[i]; + totalVerts += s.Vertices.Length; + totalIndices += s.Indices.Length; + Console.WriteLine( + $" Submesh[{i}] SurfaceId=0x{s.SurfaceId:X8} " + + $"Vertices={s.Vertices.Length} Indices={s.Indices.Length} " + + $"Translucency={s.Translucency} Luminosity={s.Luminosity:F2} " + + $"NeedsUvRepeat={s.NeedsUvRepeat}"); + } + Console.WriteLine($" TOTAL: verts={totalVerts} indices={totalIndices}"); + Console.WriteLine(); + Console.WriteLine($" Report 1 threshold check: with 8 wall quads × 2 tris × 3 indices = 48 indices per side."); + Console.WriteLine($" pos-only emission expects ~48 indices total."); + Console.WriteLine($" pos+neg emission expects ~96 indices total."); + Console.WriteLine($" OBSERVED: {totalIndices} indices → " + + (totalIndices > 60 ? "*** DOUBLE-SIDED — duplicate-side rendering active ***" : "single-sided")); +} + +static void DumpFlagBits(uint type) +{ + // From docs/research/named-retail/acclient.h:5820-5836. + // Print every named SurfaceType bit that's set. + var bits = new (uint mask, string name)[] + { + (0x00000001u, "Base1Solid"), + (0x00000002u, "Base1Image"), + (0x00000004u, "Base1ClipMap"), + (0x00000010u, "Translucent"), + (0x00000020u, "Diffuse"), + (0x00000040u, "Luminous"), + (0x00000100u, "Alpha"), + (0x00000200u, "InvAlpha"), + (0x00010000u, "Additive"), + (0x00020000u, "Detail"), + (0x10000000u, "Gouraud"), + (0x40000000u, "Stippled"), + (0x80000000u, "Perspective"), + }; + foreach (var (mask, name) in bits) + { + if ((type & mask) != 0) + Console.WriteLine($" {name} (0x{mask:X8})"); + } +} diff --git a/tools/RainMeshProbe/RainMeshProbe.csproj b/tools/RainMeshProbe/RainMeshProbe.csproj new file mode 100644 index 0000000..7e499da --- /dev/null +++ b/tools/RainMeshProbe/RainMeshProbe.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + RainMeshProbe + + + + + + +