// 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}"); } }