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.
196 lines
9 KiB
C#
196 lines
9 KiB
C#
// 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})");
|
||
}
|
||
}
|