// 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(); 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(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(rsid, out var inner) && inner is not null) rs = inner; } else if (dats.TryGet(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(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()); } 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; }