docs(research): sky/weather investigation handoff + diagnostic tools
Captures everything learned from a long worktree iteration on the foreground-rain bug (ISSUES.md #1 / #26) plus a new star-rendering bug observed in the same area. The code work from that worktree (WeatherDispatcher, EmitterDescLoader.LoadFromDat, WeatherCellRenderer, GameWindow integration) was reverted because it didn't visibly fix the rain bug — but the research findings + diagnostic tools are durable and should not have to be rediscovered. What's added: - docs/research/2026-04-26-sky-investigation-handoff.md Comprehensive seed prompt for the next session. Covers: * Bug A: foreground rain (#26) — what's open, what's confirmed, what's been tried * Bug B: stars rendering as square in corner (NEW, user-observed) * 40-agent decomp scan findings — retail rain is NOT camera- particles, NOT server-driven, NOT screen-space; the mesh IS a hollow octagonal tube; only 5 weather GfxObjs in Dereth * Things ruled out by trial (envelope, scaling, unlit, depth- always alone, Setup loading) * Things to try next (depth+zfar combined, full render-state audit, frame ordering, star UV bug as easier first target) * Acceptance criteria for "done" - docs/research/2026-04-26-chorizite-pr-draft.md Upstream PR draft for Chorizite/DatReaderWriter. Five generated DBObj source files reference nonexistent enum values and are silently excluded from the NuGet build: ParticleEmitterInfo, Clothing, PaletteSet, DataIdMapper, DualDataIdMapper. Fix: delete the duplicates. Independent of the rain work — benefits the AC modding ecosystem broadly. - docs/research/2026-04-26-datreaderwriter-reference.md Developer reference for our DatReaderWriter usage. Version, types we consume, known broken types, thread-safety caveats, upgrade procedure, NuGet-vs-vendored decision matrix. - tools/PesChainAudit/ Recursive PES walker — given a 0x33xxxxxx script id, walks all CallPES references and dumps every hook + every referenced ParticleEmitter's parameters. Used to prove no weather PES emits rain particles. - tools/TextureDump/ Dumps texture pixel statistics (alpha histogram, brightness, max) and saves as PNG for visual inspection. - tools/WeatherEnumerator/ Enumerates every DayGroup in a Region, lists weather SkyObjects (Properties & 0x04), dumps GfxObj bounding boxes. - tools/WeatherSetupProbe/ Loads a Setup id, dumps each part's GfxObj + frame + scale + surface. Used to prove weather Setups are 5cm dummy carriers. Worktree feature/sky-fixes is being deleted in a follow-up step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a060f4fc98
commit
8db7a9ec28
11 changed files with 1085 additions and 0 deletions
139
tools/WeatherEnumerator/Program.cs
Normal file
139
tools/WeatherEnumerator/Program.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// WeatherEnumerator — Issue #26 probe.
|
||||
// Iterates ALL DayGroups in Dereth (Region 0x13000000), dumps every SkyObject
|
||||
// flagged with the weather bit (Properties & 0x04), and records the bounding
|
||||
// box of every unique GfxObj used as a weather visual.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Options;
|
||||
using DatReaderWriter.Types;
|
||||
using SysEnv = System.Environment;
|
||||
|
||||
const uint WEATHER_BIT = 0x04;
|
||||
|
||||
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();
|
||||
|
||||
// Collect unique weather GfxObjIds across all DayGroups (incl. SkyTime replaces).
|
||||
var weatherGfxIds = new HashSet<uint>();
|
||||
int totalWeatherSkyObjs = 0;
|
||||
|
||||
for (int dg = 0; dg < dayGroups.Count; dg++)
|
||||
{
|
||||
var group = dayGroups[dg];
|
||||
string name = group.DayName?.Value ?? "(null)";
|
||||
int weatherCount = group.SkyObjects.Count(so => (so.Properties & WEATHER_BIT) != 0);
|
||||
Console.WriteLine($"=== DayGroup[{dg}] \"{name}\" Chance={group.ChanceOfOccur:F3} SkyObjects={group.SkyObjects.Count} WeatherSkyObjs={weatherCount} ===");
|
||||
|
||||
for (int oi = 0; oi < group.SkyObjects.Count; oi++)
|
||||
{
|
||||
var so = group.SkyObjects[oi];
|
||||
if ((so.Properties & WEATHER_BIT) == 0) continue;
|
||||
totalWeatherSkyObjs++;
|
||||
uint gfx = (uint)so.DefaultGfxObjectId;
|
||||
uint pes = (uint)so.DefaultPesObjectId;
|
||||
Console.WriteLine($" WEATHER OI={oi} Begin={so.BeginTime:F3} End={so.EndTime:F3} BeginAng={so.BeginAngle:F1} EndAng={so.EndAngle:F1}");
|
||||
Console.WriteLine($" TexVel=({so.TexVelocityX:F4},{so.TexVelocityY:F4}) Gfx=0x{gfx:X8} Pes=0x{pes:X8} Props=0x{so.Properties:X8}");
|
||||
if (gfx != 0) weatherGfxIds.Add(gfx);
|
||||
}
|
||||
|
||||
// Also scan SkyObjReplace entries for weather slots (replaces never carry
|
||||
// their own Properties, but the same OI's weather bit means the replace
|
||||
// is also a weather visual — record the gfx).
|
||||
var weatherIndices = new HashSet<int>(
|
||||
Enumerable.Range(0, group.SkyObjects.Count)
|
||||
.Where(i => (group.SkyObjects[i].Properties & WEATHER_BIT) != 0));
|
||||
foreach (var st in group.SkyTime)
|
||||
{
|
||||
foreach (var r in st.SkyObjReplace)
|
||||
{
|
||||
if (!weatherIndices.Contains((int)r.ObjectIndex)) continue;
|
||||
uint gfx = (uint)r.GfxObjId;
|
||||
if (gfx != 0 && weatherGfxIds.Add(gfx))
|
||||
Console.WriteLine($" WEATHER REPLACE SkyTime.Begin={st.Begin:F3} OI={r.ObjectIndex} Gfx=0x{gfx:X8}");
|
||||
}
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
Console.WriteLine($"Total weather SkyObjects across all DayGroups: {totalWeatherSkyObjs}");
|
||||
Console.WriteLine($"Unique weather GfxObjIds: {weatherGfxIds.Count}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== Bounding boxes of unique weather GfxObjs ===");
|
||||
Console.WriteLine("GfxObjId | Verts | minX minY minZ | maxX maxY maxZ | sizeX sizeY sizeZ | radius");
|
||||
Console.WriteLine(new string('-', 130));
|
||||
|
||||
foreach (uint gid in weatherGfxIds.OrderBy(x => x))
|
||||
{
|
||||
DumpBounds(dats, gid);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
static void DumpBounds(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) DumpGfxBounds(dats, (uint)p, indent: " ");
|
||||
return;
|
||||
}
|
||||
DumpGfxBounds(dats, gid, indent: "");
|
||||
}
|
||||
|
||||
static void DumpGfxBounds(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 (no geometry)");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 mn = new(float.MaxValue), mx = new(float.MinValue);
|
||||
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);
|
||||
}
|
||||
var size = mx - mn;
|
||||
float radius = Math.Max(size.X, Math.Max(size.Y, size.Z)) * 0.5f;
|
||||
Console.WriteLine(
|
||||
$"{indent}0x{gid:X8} | {verts.Count,5} | " +
|
||||
$"{mn.X,8:F2} {mn.Y,8:F2} {mn.Z,8:F2} | " +
|
||||
$"{mx.X,8:F2} {mx.Y,8:F2} {mx.Z,8:F2} | " +
|
||||
$"{size.X,8:F2} {size.Y,8:F2} {size.Z,8:F2} | {radius,7:F2}");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue