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:
Erik 2026-04-26 21:40:34 +02:00
parent a060f4fc98
commit 8db7a9ec28
11 changed files with 1085 additions and 0 deletions

View file

@ -0,0 +1,120 @@
// WeatherSetupProbe — Issue #26: dump weather Setups (0x02000BA6, 0x02000588, 0x02000589)
// to determine if any contain a small near-camera billboard / particle layer.
using System;
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;
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[] setupIds = { 0x02000BA6u, 0x02000588u, 0x02000589u };
foreach (uint sid in setupIds) ProbeSetup(dats, sid);
return 0;
static void ProbeSetup(DatCollection dats, uint sid)
{
Console.WriteLine();
Console.WriteLine($"================ Setup 0x{sid:X8} ================");
if (!dats.TryGet<Setup>(sid, out var s) || s is null)
{
Console.WriteLine(" (NOT FOUND)");
return;
}
Console.WriteLine($" Flags={s.Flags} NumParts={s.NumParts}");
Console.WriteLine($" Setup Height={s.Height:F3} Radius={s.Radius:F3}");
Console.WriteLine($" SortingSphere center=({s.SortingSphere.Origin.X:F2},{s.SortingSphere.Origin.Y:F2},{s.SortingSphere.Origin.Z:F2}) r={s.SortingSphere.Radius:F3}");
Console.WriteLine($" SelectionSphere center=({s.SelectionSphere.Origin.X:F2},{s.SelectionSphere.Origin.Y:F2},{s.SelectionSphere.Origin.Z:F2}) r={s.SelectionSphere.Radius:F3}");
Console.WriteLine($" HoldingLocations={s.HoldingLocations.Count} ConnectionPoints={s.ConnectionPoints.Count} PlacementFrames={s.PlacementFrames.Count} Lights={s.Lights.Count}");
Console.WriteLine($" DefaultAnimation=0x{(uint)s.DefaultAnimation:X8} DefaultScript=0x{(uint)s.DefaultScript:X8} DefaultMotionTable=0x{(uint)s.DefaultMotionTable:X8}");
// PlacementFrames: typically Placement.Default has the per-part frames.
AnimationFrame? defaultFrame = null;
foreach (var kv in s.PlacementFrames)
{
Console.WriteLine($" Placement[{kv.Key}]: Frames={kv.Value.Frames.Count} Hooks={kv.Value.Hooks.Count}");
if (defaultFrame is null) defaultFrame = kv.Value;
if (kv.Key == Placement.Default) defaultFrame = kv.Value;
}
for (int pi = 0; pi < s.Parts.Count; pi++)
{
uint partGid = (uint)s.Parts[pi];
uint parent = (s.Flags.HasFlag(SetupFlags.HasParent) && pi < s.ParentIndex.Count) ? s.ParentIndex[pi] : 0xFFFFFFFFu;
Vector3 scale = (s.Flags.HasFlag(SetupFlags.HasDefaultScale) && pi < s.DefaultScale.Count) ? s.DefaultScale[pi] : new Vector3(1, 1, 1);
Frame? frame = (defaultFrame is not null && pi < defaultFrame.Frames.Count) ? defaultFrame.Frames[pi] : null;
string frameStr = frame is null ? "(no-frame)"
: $"pos=({frame.Origin.X:F2},{frame.Origin.Y:F2},{frame.Origin.Z:F2}) ori=({frame.Orientation.W:F3},{frame.Orientation.X:F3},{frame.Orientation.Y:F3},{frame.Orientation.Z:F3})";
string parentStr = parent == 0xFFFFFFFFu ? "ROOT" : parent.ToString();
Console.WriteLine($" Part[{pi}] GfxObj=0x{partGid:X8} parent={parentStr} scale=({scale.X:F2},{scale.Y:F2},{scale.Z:F2}) {frameStr}");
DumpGfxObj(dats, partGid, " ");
}
}
static void DumpGfxObj(DatCollection dats, uint gid, string indent)
{
if (!dats.TryGet<GfxObj>(gid, out var g) || g is null)
{
Console.WriteLine($"{indent}GfxObj 0x{gid:X8} NOT FOUND");
return;
}
int triCount = g.Polygons.Count;
int physTri = g.PhysicsPolygons.Count;
int vertCount = g.VertexArray?.Vertices?.Count ?? 0;
Vector3 mn = new(float.PositiveInfinity), mx = new(float.NegativeInfinity);
if (g.VertexArray?.Vertices is not null)
{
foreach (var v in g.VertexArray.Vertices.Values)
{
mn = Vector3.Min(mn, v.Origin);
mx = Vector3.Max(mx, v.Origin);
}
}
Vector3 size = (vertCount > 0) ? (mx - mn) : Vector3.Zero;
float radius = (vertCount > 0) ? size.Length() * 0.5f : 0f;
string sizeTag = radius < 5 ? " <<TINY"
: radius < 20 ? " <<SMALL"
: radius < 200 ? "" : " <<HUGE";
Console.WriteLine($"{indent}GfxObj 0x{gid:X8} Flags={g.Flags} Surfaces={g.Surfaces.Count} RenderTris={triCount} PhysTris={physTri} Verts={vertCount}");
if (vertCount > 0)
{
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}) r~{radius:F2}{sizeTag}");
Console.WriteLine($"{indent} SortCenter=({g.SortCenter.X:F2},{g.SortCenter.Y:F2},{g.SortCenter.Z:F2})");
// billboard heuristic: 4 verts and 1-2 polygons
if (vertCount <= 8 && triCount <= 4)
Console.WriteLine($"{indent} >>> BILLBOARD CANDIDATE (verts<=8, tris<=4)");
}
for (int si = 0; si < g.Surfaces.Count; si++)
{
uint surfId = (uint)g.Surfaces[si];
if (!dats.TryGet<Surface>(surfId, out var surf) || surf is null)
{
Console.WriteLine($"{indent} Surf[{si}]=0x{surfId:X8} (not found)");
continue;
}
string tex = "solid";
if (surf.Type.HasFlag(SurfaceType.Base1Image) || surf.Type.HasFlag(SurfaceType.Base1ClipMap))
{
uint stid = (uint)surf.OrigTextureId;
if (stid != 0 && dats.TryGet<SurfaceTexture>(stid, out var st) && st is not null && st.Textures.Count > 0)
{
uint rsid = (uint)st.Textures[0];
if (dats.TryGet<RenderSurface>(rsid, out var rs) && rs is not null)
tex = $"{rs.Width}x{rs.Height} {rs.Format} STex=0x{stid:X8}";
else
tex = $"STex=0x{stid:X8} (rs miss)";
}
else tex = $"STex=0x{stid:X8}";
}
Console.WriteLine($"{indent} Surf[{si}]=0x{surfId:X8} Type={surf.Type} Translucency={surf.Translucency:F3} Lum={surf.Luminosity:F3} Diffuse={surf.Diffuse:F3} {tex}");
}
}