From 991fb9a222440d08a2074a2845135298fc2edaa5 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Apr 2026 22:19:28 +0200 Subject: [PATCH] tools(probe): add StarsProbe to dump every SkyObject's geometry + UVs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sibling of WeatherEnumerator/PesChainAudit. Walks every DayGroup in the Dereth Region (0x13000000), prints each SkyObject (Properties bits, TexVelocity, BeginTime/EndTime, gfx/pes ids), then dumps the underlying GfxObj's vertices, UV ranges, and surfaces. The crucial diagnostic is the per-GfxObj "UV range outside [0,1]" flag. Built for Bug B (sky-investigation-handoff §"Bug B"): stars rendering as a square in one corner of the sky. Smoking gun on first run: GfxObj 0x010015EF (OI-1 in every DayGroup, TexVelocity = 0) has UVs in [0.398, 4.602] — meaning the texture tiles ~4× across each face, but SkyRenderer's "CLAMP_TO_EDGE unless TexVelocity != 0" heuristic forces clamp on it, so the whole inner dome samples edge texels except the tiny region where UVs happen to fall in [0,1]. That tiny region is the "square in one corner" the user observed. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/StarsProbe/Program.cs | 153 +++++++++++++++++++++++++++++ tools/StarsProbe/StarsProbe.csproj | 15 +++ 2 files changed, 168 insertions(+) create mode 100644 tools/StarsProbe/Program.cs create mode 100644 tools/StarsProbe/StarsProbe.csproj diff --git a/tools/StarsProbe/Program.cs b/tools/StarsProbe/Program.cs new file mode 100644 index 0000000..2902a0a --- /dev/null +++ b/tools/StarsProbe/Program.cs @@ -0,0 +1,153 @@ +// StarsProbe — Bug B (sky-investigation-handoff §"Bug B"): dump every +// SkyObject's geometry + UVs to identify the star object and verify +// whether its UV range matches what GL_CLAMP_TO_EDGE supports. +// +// Sibling of WeatherEnumerator/SetupProbe/etc under tools/. Walks all +// DayGroups in the Dereth Region (0x13000000), prints every SkyObject +// (Properties bits, TexVelocity, BeginTime/EndTime), then dumps the +// underlying GfxObj's vertices, UV ranges, and surfaces. The crucial +// diagnostic is the per-GfxObj "UV range outside [0,1]" flag — when +// that's set on a static (non-scrolling) sky object, our SkyRenderer's +// CLAMP_TO_EDGE heuristic mis-samples and the texture appears as a +// "square in one corner" of the geometry. +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using DatReaderWriter; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Options; +using DatReaderWriter.Types; +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); + +if (!dats.TryGet(0x13000000u, out var region) || region is null) +{ + Console.Error.WriteLine("ERROR: Cannot read Region 0x13000000"); + return 1; +} +var dayGroups = region.SkyInfo?.DayGroups; +if (dayGroups is null) { Console.Error.WriteLine("No DayGroups"); return 1; } + +Console.WriteLine($"Region loaded. {dayGroups.Count} DayGroups."); +Console.WriteLine(); + +var seenGfx = new HashSet(); + +for (int dg = 0; dg < dayGroups.Count; dg++) +{ + var group = dayGroups[dg]; + string name = group.DayName?.Value ?? "(null)"; + Console.WriteLine($"=== DayGroup[{dg}] \"{name}\" Chance={group.ChanceOfOccur:F3} SkyObjects={group.SkyObjects.Count} ==="); + + for (int oi = 0; oi < group.SkyObjects.Count; oi++) + { + var so = group.SkyObjects[oi]; + uint gfx = (uint)so.DefaultGfxObjectId; + uint pes = (uint)so.DefaultPesObjectId; + bool wrapsMidnight = so.BeginTime > so.EndTime; + Console.WriteLine( + $" OI={oi,2} Begin={so.BeginTime:F3} End={so.EndTime:F3} {(wrapsMidnight ? "(wraps midnight — night candidate)" : "")}"); + Console.WriteLine( + $" BeginAng={so.BeginAngle:F1} EndAng={so.EndAngle:F1} TexVel=({so.TexVelocityX:F4},{so.TexVelocityY:F4})"); + Console.WriteLine( + $" Gfx=0x{gfx:X8} Pes=0x{pes:X8} Props=0x{so.Properties:X8} (bin={Convert.ToString(so.Properties, 2).PadLeft(8, '0')})"); + if (gfx != 0) seenGfx.Add(gfx); + } + + // SkyTime replaces (some sky objects swap GfxObj at specific times). + foreach (var st in group.SkyTime) + foreach (var r in st.SkyObjReplace) + { + uint gfx = (uint)r.GfxObjId; + if (gfx != 0 && seenGfx.Add(gfx)) + Console.WriteLine($" REPLACE SkyTime.Begin={st.Begin:F3} OI={r.ObjectIndex} Gfx=0x{gfx:X8}"); + } + + Console.WriteLine(); +} + +Console.WriteLine($"Unique GfxObjIds across all DayGroups: {seenGfx.Count}"); +Console.WriteLine(); +Console.WriteLine("=== Per-GfxObj geometry + UV summary ==="); + +foreach (uint gid in seenGfx.OrderBy(x => x)) + DumpGeoAndUVs(dats, gid); + +return 0; + +static void DumpGeoAndUVs(DatCollection dats, uint gid) +{ + if (gid >= 0x02000000u) + { + if (!dats.TryGet(gid, out var setup) || setup is null) + { Console.WriteLine($"0x{gid:X8} | (Setup not found)"); return; } + Console.WriteLine($"0x{gid:X8} | Setup with {setup.Parts.Count} part(s):"); + foreach (var p in setup.Parts) DumpGfx(dats, (uint)p, indent: " "); + return; + } + DumpGfx(dats, gid, indent: ""); +} + +static void DumpGfx(DatCollection dats, uint gid, string indent) +{ + if (!dats.TryGet(gid, out var go) || go is null) + { Console.WriteLine($"{indent}0x{gid:X8} | (GfxObj not found)"); return; } + var verts = go.VertexArray?.Vertices; + if (verts is null || verts.Count == 0) + { Console.WriteLine($"{indent}0x{gid:X8} | 0 verts"); return; } + + Vector3 mn = new(float.MaxValue), mx = new(float.MinValue); + float uMin = float.MaxValue, uMax = float.MinValue; + float vMin = float.MaxValue, vMax = float.MinValue; + int uvLayerMax = 0; + foreach (var kv in verts) + { + var v = kv.Value; + var p = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z); + mn = Vector3.Min(mn, p); mx = Vector3.Max(mx, p); + if (v.UVs is { Count: > 0 } uvs) + { + uvLayerMax = Math.Max(uvLayerMax, uvs.Count); + foreach (var uv in uvs) + { + uMin = Math.Min(uMin, uv.U); uMax = Math.Max(uMax, uv.U); + vMin = Math.Min(vMin, uv.V); vMax = Math.Max(vMax, uv.V); + } + } + } + var size = mx - mn; + int polyCount = go.Polygons?.Count ?? 0; + int surfCount = go.Surfaces?.Count ?? 0; + bool uvOutsideUnit = uvLayerMax > 0 + && (uMin < 0f || uMax > 1f || vMin < 0f || vMax > 1f); + + Console.WriteLine($"{indent}0x{gid:X8} | verts={verts.Count} polys={polyCount} surfaces={surfCount} uvLayers={uvLayerMax}"); + Console.WriteLine($"{indent} bbox min=({mn.X:F2},{mn.Y:F2},{mn.Z:F2}) max=({mx.X:F2},{mx.Y:F2},{mx.Z:F2}) size=({size.X:F2},{size.Y:F2},{size.Z:F2})"); + if (uvLayerMax > 0) + Console.WriteLine($"{indent} UV range U=[{uMin:F3}, {uMax:F3}] V=[{vMin:F3}, {vMax:F3}] {(uvOutsideUnit ? "*** OUTSIDE [0,1] — needs REPEAT wrap ***" : "in [0,1]")}"); + else + Console.WriteLine($"{indent} UV range (no UVs on any vertex)"); + + if (go.Surfaces is { Count: > 0 }) + for (int i = 0; i < go.Surfaces.Count; i++) + Console.WriteLine($"{indent} Surface[{i}]=0x{(uint)go.Surfaces[i]:X8}"); + + // Verbose per-vertex dump (capped at 64 verts to keep output bounded). + int dumpN = Math.Min(verts.Count, 64); + int shown = 0; + foreach (var kv in verts) + { + if (shown++ >= dumpN) { Console.WriteLine($"{indent} ...({verts.Count - dumpN} more verts)"); break; } + var v = kv.Value; + string uvStr = v.UVs is null || v.UVs.Count == 0 ? "(none)" : string.Join(" ", v.UVs.Select(u => $"({u.U:F3},{u.V:F3})")); + Console.WriteLine($"{indent} v[{kv.Key,3}] pos=({v.Origin.X,7:F2},{v.Origin.Y,7:F2},{v.Origin.Z,7:F2}) uv={uvStr}"); + } +} diff --git a/tools/StarsProbe/StarsProbe.csproj b/tools/StarsProbe/StarsProbe.csproj new file mode 100644 index 0000000..a70fd01 --- /dev/null +++ b/tools/StarsProbe/StarsProbe.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + StarsProbe + + + + + + +