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>
228 lines
8.1 KiB
C#
228 lines
8.1 KiB
C#
// 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;
|
|
}
|