acdream/tools/RainMeshProbe/Program.cs
Erik 646ccca85e feat(sky): load Setup-backed (0x020xxx) sky objects via SetupMesh.Flatten
Independent code review by an external agent (2026-04-27) flagged
that SkyRenderer.EnsureMeshUploaded only ever called
_dats.Get<GfxObj>(...) — every 0x020xxx Setup ID returned null and
got cached as an empty submesh list, silently dropping every
Setup-backed sky object across the Dereth Region. In Rainy DG3
alone that's 6 dropped SkyObjects (0x02000714, 0x02000BA6 ×2,
0x02000588 ×4, 0x02000589 ×3 across various time-of-day windows).

Verbatim from retail's CelestialPosition struct at acclient.h:35451:

    struct CelestialPosition {
        IDClass<...> gfx_id;
        IDClass<...> pes_id;          // particle scheduler
        float heading; float rotation;
        Vector3 tex_velocity;
        float transparent; float luminosity; float max_bright;
        unsigned int properties;
    };

Per the named retail decomp, CPhysicsObj::InitPartArrayObject (decomp
~280484) dispatches gfx_id by type prefix: type 6 → direct GfxObj,
type 7 → Setup via CPartArray::CreateSetup (decomp ~287490) which
walks Setup.Parts. Mirror that here: detect 0x020xxxxx in
EnsureMeshUploaded, route to a new EnsureSetupUploaded helper that
flattens via SetupMesh.Flatten (existing Phase-2 utility) and bakes
each part's transform into the vertex positions before upload.
Sky setups don't animate in any way that affects the static-mesh
visual we render here.

Probe extension: also added the Diffuse column to RainMeshProbe's
sky-surface audit so the (Type, Translucency, Luminosity, Diffuse)
quadruple is visible on every flag-bit row.

Visual impact at verification launch: not observable. The Setup
objects in Rainy DGs appear to be tiny placeholder meshes existing
mainly to anchor PES emitters. The dynamic "aurora-like" sheen the
user observes in retail comes from the PES particle layer, which
remains unimplemented (issue #28). Keeping this fix because the
geometry path is now decomp-correct and provides foundation for
the eventual PES wiring.

Issue #29 filed for the residual cloud-density gap. 1227 tests pass.
2026-04-27 23:24:09 +02:00

196 lines
9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
// Phase 7c: also dump every sky surface we know to test the LUMINOUS flag.
// Two existing code comments contradict each other about whether Dereth's
// dome/sun/moon meshes carry the LUMINOUS bit. Resolve empirically.
Console.WriteLine();
Console.WriteLine("================ Sky Surface LUMINOUS audit ================");
uint[] skySurfaceIds = {
0x08000048u, 0x08000049u, 0x0800004Au, 0x0800004Bu, // dome 0x010015EE
0x0800004Du, // star sheet 0x010015EF
0x0800004Eu, 0x0800004Fu, 0x08000050u, 0x08000051u, // dome 0x010015F0
0x08000053u, 0x08000054u, 0x08000055u, 0x08000056u, // dome 0x010015F1
0x08000057u, 0x08000058u, 0x08000059u, 0x0800005Au, // dome 0x010015F2
0x080000D1u, // celestial 0x01001348
0x080000D2u, // sun-like 0x01001F67
0x080000D6u, 0x080000D7u, // moon 0x01001F6A
0x080000D4u, // cloud 0x01004C36/37
0x08000023u, // cloud 0x01004C35
0x08000024u, 0x08000025u, // cloud 0x01004C39/3A
0x080000D5u, // dome variant 0x010015B6
0x080000C5u, // RAIN — control row, expected NO Luminous
};
foreach (uint sid in skySurfaceIds) ProbeSkySurface(dats, sid);
return 0;
static void ProbeSkySurface(DatCollection dats, uint sid)
{
if (!dats.TryGet<Surface>(sid, out var s) || s is null)
{ Console.WriteLine($" Surface 0x{sid:X8} (NOT FOUND)"); return; }
uint t = (uint)s.Type;
bool luminous = (t & 0x40u) != 0u;
Console.Write($" Surface 0x{sid:X8} Type=0x{t:X8} Luminous={(luminous ? "YES" : "no ")} Lum={s.Luminosity:F4} Trans={s.Translucency:F4} Diff={s.Diffuse:F4} ");
// Decode bits inline.
var bits = new (uint mask, string n)[] {
(0x01u,"B1Solid"),(0x02u,"B1Image"),(0x04u,"B1ClipMap"),(0x10u,"Translucent"),
(0x20u,"Diffuse"),(0x40u,"Luminous"),(0x100u,"Alpha"),(0x200u,"InvAlpha"),
(0x10000u,"Additive"),(0x20000u,"Detail"),
};
Console.WriteLine(string.Join("|", bits.Where(b => (t & b.mask) != 0).Select(b => b.n)));
}
static void ProbeRain(DatCollection dats, uint gid)
{
Console.WriteLine();
Console.WriteLine($"================ GfxObj 0x{gid:X8} ================");
if (!dats.TryGet<GfxObj>(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<Surface>(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})");
}
}