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,228 @@
// TextureDump — probe rain-streak textures (0x050016A4..0x050016A8) for
// the volumetric rain density-trick decision.
using System;
using System.IO;
using System.IO.Compression;
using AcDream.Core.Textures;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
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);
string outDir = Path.Combine(AppContext.BaseDirectory, "out");
Directory.CreateDirectory(outDir);
Console.WriteLine($"outDir = {outDir}");
// Original ask: 0x050016A4..0x050016A8. A4/A5/A7/A8 don't exist as files; widen
// to 0x050016A0..0x050016AF to catch any related precip textures.
var idList = new System.Collections.Generic.List<uint>();
for (uint i = 0x050016A0; i <= 0x050016AF; i++) idList.Add(i);
uint[] ids = idList.ToArray();
(uint id, double densityFraction)? best = null;
foreach (var id in ids)
{
Console.WriteLine();
Console.WriteLine($"=== 0x{id:X8} ===");
RenderSurface? rs = null;
// SurfaceTexture wrapper (0x05xxxxxx) → first inner RenderSurface (0x06xxxxxx).
if (dats.TryGet<SurfaceTexture>(id, out var st) && st is not null && st.Textures.Count > 0)
{
uint rsid = (uint)st.Textures[0];
Console.WriteLine($" SurfaceTexture wrapper, {st.Textures.Count} mip(s), first = 0x{rsid:X8}");
if (dats.TryGet<RenderSurface>(rsid, out var inner) && inner is not null)
rs = inner;
}
else if (dats.TryGet<RenderSurface>(id, out var direct) && direct is not null)
{
rs = direct;
}
if (rs is null)
{
Console.WriteLine(" (not a SurfaceTexture or RenderSurface, or not found)");
continue;
}
Console.WriteLine($" Dimensions: {rs.Width} x {rs.Height}");
Console.WriteLine($" PixelFormat: {rs.Format} (0x{(uint)rs.Format:X8})");
Console.WriteLine($" SourceData bytes: {rs.SourceData?.Length ?? 0}");
Console.WriteLine($" DefaultPaletteId: 0x{rs.DefaultPaletteId:X8}");
Palette? palette = null;
if (rs.DefaultPaletteId != 0)
{
if (dats.TryGet<Palette>(rs.DefaultPaletteId, out var pal) && pal is not null)
{
palette = pal;
Console.WriteLine($" Palette colors: {pal.Colors.Count}");
}
}
var dec = SurfaceDecoder.DecodeRenderSurface(rs, palette);
if (dec.Rgba8 is null || dec.Rgba8.Length == 0)
{
Console.WriteLine(" DECODE FAILED.");
continue;
}
// Stats: alpha histogram (8 buckets), brightness mean/stddev, bright count.
int n = dec.Width * dec.Height;
int[] alphaBuckets = new int[8]; // 0..31, 32..63, ..., 224..255
long sumB = 0;
long sumBSq = 0;
int brightAlpha = 0; // alpha > 128
int brightLum = 0; // brightness > 200
int litLum = 0; // brightness > 16 (any visible pixel)
int midLum = 0; // brightness > 64
int nonZeroAlpha = 0;
int maxLum = 0;
for (int i = 0; i < n; i++)
{
byte r = dec.Rgba8[i * 4 + 0];
byte g = dec.Rgba8[i * 4 + 1];
byte b = dec.Rgba8[i * 4 + 2];
byte a = dec.Rgba8[i * 4 + 3];
int lum = (r + g + b) / 3;
sumB += lum;
sumBSq += (long)lum * lum;
alphaBuckets[Math.Min(7, a / 32)]++;
if (a > 128) brightAlpha++;
if (lum > 200) brightLum++;
if (lum > 64) midLum++;
if (lum > 16) litLum++;
if (a > 0) nonZeroAlpha++;
if (lum > maxLum) maxLum = lum;
}
double mean = (double)sumB / n;
double variance = ((double)sumBSq / n) - mean * mean;
double stddev = Math.Sqrt(Math.Max(0, variance));
Console.WriteLine($" Pixels: {n}");
Console.Write(" Alpha histogram (8 buckets, 0->255):");
for (int b = 0; b < 8; b++) Console.Write($" {alphaBuckets[b]}");
Console.WriteLine();
Console.WriteLine($" Brightness mean = {mean:F2}, stddev = {stddev:F2}, max = {maxLum}");
Console.WriteLine($" Lit pixels: lum>16 = {litLum} ({100.0 * litLum / n:F3}%) lum>64 = {midLum} ({100.0 * midLum / n:F3}%) lum>200 = {brightLum} ({100.0 * brightLum / n:F3}%)");
Console.WriteLine($" alpha>128 = {brightAlpha} ({100.0 * brightAlpha / n:F2}%) nonZeroAlpha = {nonZeroAlpha} ({100.0 * nonZeroAlpha / n:F2}%)");
// "Density" for fake-volumetric look: % of pixels that contribute a visible
// streak. Use max(alpha>128, lum>200, nonZeroAlpha) as the proxy — different
// textures encode the streak via either alpha-clip or pure luminance.
double densityFrac = Math.Max(brightAlpha, Math.Max(brightLum, nonZeroAlpha)) / (double)n;
if (best is null || densityFrac > best.Value.densityFraction)
best = (id, densityFrac);
string pngPath = Path.Combine(outDir, $"tex_{id:X8}.png");
WritePng(pngPath, dec.Rgba8, dec.Width, dec.Height);
Console.WriteLine($" PNG: {pngPath}");
}
Console.WriteLine();
if (best is not null)
Console.WriteLine($"DENSEST: 0x{best.Value.id:X8} (density frac {best.Value.densityFraction:F4})");
return 0;
// -------- Minimal PNG encoder (RGBA8, no filter, single IDAT, zlib via DeflateStream) --------
static void WritePng(string path, byte[] rgba, int width, int height)
{
using var fs = File.Create(path);
// Signature
fs.Write(new byte[] { 0x89, (byte)'P', (byte)'N', (byte)'G', 0x0D, 0x0A, 0x1A, 0x0A });
// IHDR
var ihdr = new byte[13];
WriteBE(ihdr, 0, (uint)width);
WriteBE(ihdr, 4, (uint)height);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type RGBA
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
WriteChunk(fs, "IHDR", ihdr);
// IDAT: rows prefixed with filter byte 0, zlib-wrapped deflate.
using var raw = new MemoryStream();
for (int y = 0; y < height; y++)
{
raw.WriteByte(0);
raw.Write(rgba, y * width * 4, width * 4);
}
byte[] uncompressed = raw.ToArray();
using var compressed = new MemoryStream();
// zlib header: 0x78 0x9C (deflate, default compression)
compressed.WriteByte(0x78);
compressed.WriteByte(0x9C);
using (var deflate = new DeflateStream(compressed, CompressionLevel.Fastest, leaveOpen: true))
deflate.Write(uncompressed, 0, uncompressed.Length);
// Adler-32 of uncompressed data, big-endian.
uint adler = Adler32(uncompressed);
compressed.WriteByte((byte)(adler >> 24));
compressed.WriteByte((byte)(adler >> 16));
compressed.WriteByte((byte)(adler >> 8));
compressed.WriteByte((byte)adler);
WriteChunk(fs, "IDAT", compressed.ToArray());
WriteChunk(fs, "IEND", Array.Empty<byte>());
}
static void WriteBE(byte[] buf, int offset, uint v)
{
buf[offset + 0] = (byte)(v >> 24);
buf[offset + 1] = (byte)(v >> 16);
buf[offset + 2] = (byte)(v >> 8);
buf[offset + 3] = (byte)v;
}
static void WriteChunk(Stream s, string type, byte[] data)
{
var len = new byte[4];
WriteBE(len, 0, (uint)data.Length);
s.Write(len);
var typeBytes = System.Text.Encoding.ASCII.GetBytes(type);
s.Write(typeBytes);
s.Write(data);
var crcInput = new byte[typeBytes.Length + data.Length];
Buffer.BlockCopy(typeBytes, 0, crcInput, 0, typeBytes.Length);
Buffer.BlockCopy(data, 0, crcInput, typeBytes.Length, data.Length);
uint crc = Crc32(crcInput);
s.WriteByte((byte)(crc >> 24));
s.WriteByte((byte)(crc >> 16));
s.WriteByte((byte)(crc >> 8));
s.WriteByte((byte)crc);
}
static uint Crc32(byte[] data)
{
uint c = 0xFFFFFFFFu;
foreach (var b in data)
{
uint v = (c ^ b) & 0xFF;
for (int k = 0; k < 8; k++)
v = ((v & 1) != 0) ? (0xEDB88320u ^ (v >> 1)) : (v >> 1);
c = v ^ (c >> 8);
}
return c ^ 0xFFFFFFFFu;
}
static uint Adler32(byte[] data)
{
const uint MOD = 65521;
uint a = 1, b = 0;
foreach (var x in data)
{
a = (a + x) % MOD;
b = (b + a) % MOD;
}
return (b << 16) | a;
}

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>TextureDump</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chorizite.DatReaderWriter" Version="2.1.7" />
<ProjectReference Include="..\..\src\AcDream.Core\AcDream.Core.csproj" />
</ItemGroup>
</Project>