// InspectCoatTex — Issue #37 diagnostic. // Inspect the byte-level contents of the SurfaceTextures the server sends in // TextureChanges for the player's Academy Coat (part 9 / Aluvian Male): // new: 0x05001AFE, 0x05001AFC // old: 0x050003D5, 0x050003D4 // And dump every Surface (0x08) under the part 9 GfxObj 0x0100120D, plus the // DefaultPaletteId of each new texture (and the first 32 entries of that // palette to see if it's a SKIN or COAT palette). using System; using System.IO; 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); uint[] surfaceTextureIds = { 0x05001AFEu, 0x05001AFCu, 0x050003D5u, 0x050003D4u }; uint part9GfxObj = 0x0100120Du; Console.WriteLine(); Console.WriteLine("==================== SurfaceTexture (0x05) wrappers ===================="); foreach (var stid in surfaceTextureIds) DumpSurfaceTexture(dats, stid); Console.WriteLine(); Console.WriteLine("==================== Part 9 GfxObj 0x0100120D Surfaces (0x08) ===================="); DumpGfxObjSurfaces(dats, part9GfxObj); Console.WriteLine(); Console.WriteLine("==================== Cross-check: which Surfaces reference the OLD textures? ===================="); DumpReverseLookup(dats, part9GfxObj, new[] { 0x050003D5u, 0x050003D4u }); Console.WriteLine(); Console.WriteLine("==================== Per-polygon flags for gfx 0x0100120D (testing Agent 4 hypotheses) ===================="); DumpPolygons(dats, part9GfxObj); Console.WriteLine(); Console.WriteLine("==================== Academy Coat ClothingBase + its SubPalettes (testing compact-vs-full-palette hypothesis) ===================="); DumpClothingBase(dats, 0x10000ABFu); return 0; // ----------------------------------------------------------------------------- static void DumpSurfaceTexture(DatCollection dats, uint stid) { Console.WriteLine(); Console.WriteLine($"--- SurfaceTexture 0x{stid:X8} ---"); if (!dats.TryGet(stid, out var st) || st is null) { Console.WriteLine(" NOT FOUND as SurfaceTexture."); return; } Console.WriteLine($" Textures.Count = {st.Textures.Count}"); if (st.Textures.Count == 0) return; uint rsid = (uint)st.Textures[0]; Console.WriteLine($" First inner Texture id = 0x{rsid:X8}"); if (!dats.TryGet(rsid, out var rs) || rs is null) { Console.WriteLine(" Inner not found as RenderSurface."); return; } Console.WriteLine($" Format = {rs.Format} (0x{(uint)rs.Format:X8})"); Console.WriteLine($" Dimensions = {rs.Width} x {rs.Height}"); Console.WriteLine($" SourceData bytes = {rs.SourceData?.Length ?? 0}"); Console.WriteLine($" DefaultPaletteId = 0x{rs.DefaultPaletteId:X8}"); Palette? palette = null; if (rs.DefaultPaletteId != 0 && dats.TryGet(rs.DefaultPaletteId, out var pal) && pal is not null) { palette = pal; Console.WriteLine($" Palette.Colors.Count = {pal.Colors.Count}"); Console.WriteLine($" Palette[0..31]:"); int show = Math.Min(32, pal.Colors.Count); for (int i = 0; i < show; i++) { var c = pal.Colors[i]; byte a = c.Alpha, r = c.Red, g = c.Green, b = c.Blue; string desc = ClassifyColor(r, g, b); Console.WriteLine($" [{i:D2}] A={a:X2} R={r:X2} G={g:X2} B={b:X2} {desc}"); } } else if (rs.DefaultPaletteId != 0) { Console.WriteLine(" DefaultPalette not found in dats."); } var dec = SurfaceDecoder.DecodeRenderSurface(rs, palette); if (dec.Rgba8 is null || dec.Rgba8.Length == 0) { Console.WriteLine(" Decode FAILED."); return; } long sumR = 0, sumG = 0, sumB = 0, sumA = 0; int n = dec.Width * dec.Height; for (int i = 0; i < n; i++) { sumR += dec.Rgba8[i * 4 + 0]; sumG += dec.Rgba8[i * 4 + 1]; sumB += dec.Rgba8[i * 4 + 2]; sumA += dec.Rgba8[i * 4 + 3]; } byte mr = (byte)(sumR / n), mg = (byte)(sumG / n), mb = (byte)(sumB / n), ma = (byte)(sumA / n); Console.WriteLine($" Decoded mean RGBA = R={mr:X2} G={mg:X2} B={mb:X2} A={ma:X2} {ClassifyColor(mr, mg, mb)}"); } static void DumpGfxObjSurfaces(DatCollection dats, uint gfxId) { Console.WriteLine(); Console.WriteLine($"--- GfxObj 0x{gfxId:X8} ---"); if (!dats.TryGet(gfxId, out var gfx) || gfx is null) { Console.WriteLine(" NOT FOUND."); return; } Console.WriteLine($" Surfaces.Count = {gfx.Surfaces.Count}"); foreach (var sQid in gfx.Surfaces) { uint sid = (uint)sQid; if (!dats.TryGet(sid, out var surf) || surf is null) { Console.WriteLine($" Surface 0x{sid:X8}: NOT FOUND"); continue; } Console.WriteLine($" Surface 0x{sid:X8}"); Console.WriteLine($" Type = {surf.Type}"); Console.WriteLine($" OrigTextureId = 0x{surf.OrigTextureId:X8}"); Console.WriteLine($" OrigPaletteId = 0x{surf.OrigPaletteId:X8}"); Console.WriteLine($" ColorValue = 0x{surf.ColorValue:X8}"); Console.WriteLine($" Translucency = {surf.Translucency}"); Console.WriteLine($" Luminosity = {surf.Luminosity}"); Console.WriteLine($" Diffuse = {surf.Diffuse}"); // What does that OrigTextureId actually wrap? uint origTex = (uint)surf.OrigTextureId; if (origTex != 0 && dats.TryGet(origTex, out var st) && st is not null && st.Textures.Count > 0) { uint rsid = (uint)st.Textures[0]; if (dats.TryGet(rsid, out var rs) && rs is not null) { Console.WriteLine($" -> wraps RS 0x{rsid:X8}: {rs.Format} {rs.Width}x{rs.Height} defaultPal=0x{rs.DefaultPaletteId:X8}"); } } } } static void DumpReverseLookup(DatCollection dats, uint gfxId, uint[] oldTextures) { if (!dats.TryGet(gfxId, out var gfx) || gfx is null) return; foreach (var sQid in gfx.Surfaces) { uint sid = (uint)sQid; if (!dats.TryGet(sid, out var surf) || surf is null) continue; uint origTex = (uint)surf.OrigTextureId; foreach (var oldT in oldTextures) { if (origTex == oldT) Console.WriteLine($" Surface 0x{sid:X8} OrigTextureId=0x{origTex:X8} matches OLD 0x{oldT:X8} (server replaces this)"); } } } static void DumpClothingBase(DatCollection dats, uint cbId) { Console.WriteLine(); Console.WriteLine($"--- ClothingTable 0x{cbId:X8} ---"); if (!dats.TryGet(cbId, out var cb) || cb is null) { Console.WriteLine(" NOT FOUND."); return; } Console.WriteLine($" ClothingBaseEffects.Count = {cb.ClothingBaseEffects.Count}"); Console.WriteLine($" ClothingSubPalEffects.Count = {cb.ClothingSubPalEffects.Count}"); // For each SubPalEffect (PaletteTemplate variant), dump its CloSubPalettes var palettesProbed = new System.Collections.Generic.HashSet(); int subPalEffectIdx = 0; foreach (var (paletteTemplate, subPalEffect) in cb.ClothingSubPalEffects) { if (subPalEffectIdx++ >= 3) break; // first 3 only Console.WriteLine(); Console.WriteLine($" ClothingSubPalEffect[paletteTemplate=0x{(uint)paletteTemplate:X8}]:"); Console.WriteLine($" Icon = 0x{(uint)subPalEffect.Icon:X8}"); Console.WriteLine($" CloSubPalettes.Count = {subPalEffect.CloSubPalettes.Count}"); foreach (var sp in subPalEffect.CloSubPalettes) { uint subPalId = (uint)sp.PaletteSet; Console.WriteLine($" CloSubPalette PaletteSet=0x{subPalId:X8}, Ranges.Count={sp.Ranges.Count}"); foreach (var r in sp.Ranges) { Console.WriteLine($" Range Offset={r.Offset} NumColors={r.NumColors} (raw palette indices)"); } // Probe whatever this id resolves to. AC has both Palette (0x04) and // PaletteSet (0x0F) types; without strong typing we just probe both. if (palettesProbed.Add(subPalId)) { if (dats.TryGet(subPalId, out var palDirect) && palDirect is not null) { Console.WriteLine($" -> Palette 0x{subPalId:X8}: Colors.Count={palDirect.Colors.Count}"); if (palDirect.Colors.Count > 0) { var c = palDirect.Colors[0]; Console.WriteLine($" [00] R={c.Red:X2} G={c.Green:X2} B={c.Blue:X2}"); } if (palDirect.Colors.Count > 24) { var c = palDirect.Colors[24]; Console.WriteLine($" [24] R={c.Red:X2} G={c.Green:X2} B={c.Blue:X2}"); } if (palDirect.Colors.Count > 100) { var c = palDirect.Colors[100]; Console.WriteLine($" [100] R={c.Red:X2} G={c.Green:X2} B={c.Blue:X2}"); } } else { Console.WriteLine($" -> NOT a direct Palette (might be PaletteSet 0x0F — AC has dat type for set-of-palettes)"); } } } } } static void DumpPolygons(DatCollection dats, uint gfxId) { Console.WriteLine(); Console.WriteLine($"--- Polygons of GfxObj 0x{gfxId:X8} ---"); if (!dats.TryGet(gfxId, out var gfx) || gfx is null) { Console.WriteLine(" NOT FOUND."); return; } Console.WriteLine($" Polygon count = {gfx.Polygons.Count}"); Console.WriteLine($" Surfaces[0..N] (for cross-ref):"); for (int i = 0; i < gfx.Surfaces.Count; i++) { uint sid = (uint)gfx.Surfaces[i]; if (dats.TryGet(sid, out var surf) && surf is not null) Console.WriteLine($" [{i}] surfId=0x{sid:X8} Type={surf.Type}"); } Console.WriteLine(); Console.WriteLine($" {"PolyKey",6} {"SidesType",-12} {"Stippling",-32} {"NumVerts",8} {"PosSurf",7} {"NegSurf",7} {"PosUVs",6} {"NegUVs",6}"); int idx = 0; int stDouble = 0, stBoth = 0, stSingle = 0; int nonImageSurf = 0; var sidesTypeHistogram = new System.Collections.Generic.Dictionary(); foreach (var (key, p) in gfx.Polygons) { // Count first int sti = (int)p.SidesType; if (!sidesTypeHistogram.ContainsKey(sti)) sidesTypeHistogram[sti] = 0; sidesTypeHistogram[sti]++; if (sti == 0) stSingle++; else if (sti == 1) stDouble++; else if (sti == 2) stBoth++; // Check Type & 6 gate if (p.PosSurface >= 0 && p.PosSurface < gfx.Surfaces.Count) { uint sid = (uint)gfx.Surfaces[p.PosSurface]; if (dats.TryGet(sid, out var surf) && surf is not null) { uint typeBits = (uint)surf.Type; bool hasImage = (typeBits & 0x02) != 0; bool hasClipMap = (typeBits & 0x04) != 0; if (!hasImage && !hasClipMap) nonImageSurf++; } } if (idx < 30 || sti != 0) // first 30 + all unusual SidesType { Console.WriteLine($" {key,6} {p.SidesType,-12} {p.Stippling,-32} {p.VertexIds.Count,8} {p.PosSurface,7} {p.NegSurface,7} {p.PosUVIndices.Count,6} {p.NegUVIndices.Count,6}"); } idx++; } Console.WriteLine(); Console.WriteLine($" SidesType histogram: {string.Join(", ", sidesTypeHistogram.Select(kv => $"{kv.Key}={kv.Value}"))}"); Console.WriteLine($" SidesType=0 (ST_SINGLE) = {stSingle}"); Console.WriteLine($" SidesType=1 (ST_DOUBLE / None) = {stDouble} <-- if >0, retail draws BOTH faces; acdream draws ONE"); Console.WriteLine($" SidesType=2 (ST_BOTH / Clockwise) = {stBoth}"); Console.WriteLine($" Polygons with surface lacking Base1Image+Base1ClipMap: {nonImageSurf}"); Console.WriteLine($" (retail SKIPS these polygons in DrawPolyInternal; acdream renders them with surface.ColorValue)"); } // Heuristic name for what RGB looks like: skin tones (peach/tan), coat tones // (blue/purple/red), neutral grey. Cheap classifier so we can eyeball palette // dumps without staring at raw hex. static string ClassifyColor(byte r, byte g, byte b) { int max = Math.Max(r, Math.Max(g, b)); int min = Math.Min(r, Math.Min(g, b)); int range = max - min; if (max < 16) return "(near-black)"; if (range < 12) return "(grey)"; // Skin-tone heuristic: R > G > B, R-G in ~5..40, G-B in ~5..40, all >= 60. if (r > g && g > b && (r - g) >= 4 && (g - b) >= 2 && r >= 60 && g >= 40) return "(skin-tone-ish)"; if (b > r && b > g) return "(blue-ish)"; if (r > g && r > b) return "(red-ish)"; if (g > r && g > b) return "(green-ish)"; return ""; }