// Dump the surfaces a geometry export references, as PNGs named the way // tools/IconForge's loader globs for them: <0xID>_x.png. // // PNG is hand-rolled on ZLibStream so this tool needs no image package; the // same approach is used by tools/IconExtract. using System.IO.Compression; using System.Text; using AcDream.Core.Textures; using DatReaderWriter; using DatReaderWriter.DBObjs; namespace MosswartArt; internal static class Textures { /// /// Resolve each id to pixels and write it out. Ids may be Surface (0x08), /// SurfaceTexture (0x05) or RenderSurface (0x06/0x07); the chain is walked /// down to pixels either way. /// public static int Dump(DatCollection dats, IEnumerable ids, string outDir) { Directory.CreateDirectory(outDir); int written = 0; foreach (uint id in ids.Distinct().OrderBy(v => v)) { RenderSurface? rs = Resolve(dats, id); if (rs is null || rs.Width <= 0 || rs.Height <= 0) { Console.Error.WriteLine($" 0x{id:X8} UNRESOLVED"); continue; } Palette? palette = null; if (rs.DefaultPaletteId != 0 && dats.TryGet(rs.DefaultPaletteId, out var pal) && pal is not null) { palette = pal; } var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette); if (decoded.Rgba8 is null || decoded.Rgba8.Length < decoded.Width * decoded.Height * 4) { Console.Error.WriteLine($" 0x{id:X8} DECODE FAILED"); continue; } string path = Path.Combine( outDir, $"0x{id:X8}_{decoded.Width}x{decoded.Height}.png"); WritePng(path, decoded.Rgba8, decoded.Width, decoded.Height); Console.WriteLine($" 0x{id:X8} {decoded.Width}x{decoded.Height} -> {Path.GetFileName(path)}"); written++; } return written; } // DatReaderWriter's TryGet does not validate the file type -- it just // deserializes the bytes as T -- so dispatch on the id range rather than // trying each type in turn. private static RenderSurface? Resolve(DatCollection dats, uint id) { uint lookupId = id; uint type = id >> 24; if (type == 0x08) { if (dats.TryGet(id, out var surface) && surface is not null) lookupId = (uint)surface.OrigTextureId; type = lookupId >> 24; } if (type == 0x05) { if (dats.TryGet(lookupId, out var st) && st is not null && st.Textures.Count > 0 && dats.TryGet((uint)st.Textures[0], out var inner)) { return inner; } return null; } if (type is 0x06 or 0x07) return dats.TryGet(lookupId, out var direct) ? direct : null; return null; } private static void WritePng(string path, byte[] rgba, int w, int h) { using var fs = File.Create(path); fs.Write(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }); var ihdr = new byte[13]; WriteBE(ihdr, 0, (uint)w); WriteBE(ihdr, 4, (uint)h); ihdr[8] = 8; // bit depth ihdr[9] = 6; // colour type: RGBA WriteChunk(fs, "IHDR", ihdr); using var ms = new MemoryStream(); using (var z = new ZLibStream(ms, CompressionLevel.Optimal, leaveOpen: true)) { var row = new byte[1 + w * 4]; for (int y = 0; y < h; y++) { row[0] = 0; // filter: none Array.Copy(rgba, y * w * 4, row, 1, w * 4); z.Write(row, 0, row.Length); } } WriteChunk(fs, "IDAT", ms.ToArray()); WriteChunk(fs, "IEND", Array.Empty()); } private static void WriteBE(byte[] b, int o, uint v) { b[o] = (byte)(v >> 24); b[o + 1] = (byte)(v >> 16); b[o + 2] = (byte)(v >> 8); b[o + 3] = (byte)v; } private static void WriteChunk(Stream s, string type, byte[] data) { Span len = stackalloc byte[4]; WriteBE(len, (uint)data.Length); s.Write(len); byte[] t = Encoding.ASCII.GetBytes(type); s.Write(t); s.Write(data); uint crc = Crc32(t, data); Span c = stackalloc byte[4]; WriteBE(c, crc); s.Write(c); } private static void WriteBE(Span b, uint v) { b[0] = (byte)(v >> 24); b[1] = (byte)(v >> 16); b[2] = (byte)(v >> 8); b[3] = (byte)v; } private static uint Crc32(byte[] a, byte[] b) { uint crc = 0xFFFFFFFFu; foreach (byte[] arr in new[] { a, b }) { foreach (byte by in arr) { crc ^= by; for (int k = 0; k < 8; k++) crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320u : crc >> 1; } } return crc ^ 0xFFFFFFFFu; } }