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) <noreply@anthropic.com>
153 lines
6.4 KiB
C#
153 lines
6.4 KiB
C#
// 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<Region>(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<uint>();
|
|
|
|
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<Setup>(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<GfxObj>(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}");
|
|
}
|
|
}
|