docs(issues): #37 — Investigation 2 narrows bug to SubPalette coverage gaps
Five parallel agents + dat probes ruled out: - byte-level decode primitive (matches ACViewer) - polygon emission (no ST_DOUBLE / Surface.Type & 6 issues) - per-PART texture-override scoping (correctly per-MeshRef'd) - SubPalette indexing convention (full-size 2048 palettes, *8 wire un-pack is single-applied) Smoking gun: for +Acdream the server sends 10 SubPaletteSwap ranges that overlay palette indices [0..320), [576..1024), [1392..1488), [1728..1920). The complement — [320..576), [1024..1392), [1488..1728), [1920..2048) — is NOT overlaid. Base palette 0x0400007E at those indices has red/skin tones. Coat texture UVs sampling those non-overlaid indices render as visible "skin stub at top of coat". Either ACE sends incomplete SubPaletteSwap data, or retail does extra client-side ClothingTable computation we (and ACE) don't. Diagnostic harness now lives at tools/InspectCoatTex/Program.cs; GameWindow's DUMP_CLOTHING also probes runtime SubPalette dat sizes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a3f53c2644
commit
5937ebe1c5
3 changed files with 368 additions and 16 deletions
|
|
@ -310,22 +310,35 @@ What we confirmed (data is correct):
|
|||
matches ACME's `StaticObjectManager.cs:256-258` and retail decomp's
|
||||
`Frame::combine` at `0x00518FD0`.
|
||||
|
||||
**Remaining hypothesis space (untested):**
|
||||
**Investigation 2 (2026-05-04, 5 parallel agents + dat probes):**
|
||||
|
||||
1. **Texture decode produces skin pixels** for `0x05001AFE/0x05001AFC`
|
||||
where ACME / retail produces coat pixels. Compare our SurfaceDecoder
|
||||
against ACME's `TextureHelpers.cs` for INDEX16 / palette-indexed
|
||||
chains.
|
||||
2. **Polygon-to-surface mapping off-by-one.** Specific polygons of
|
||||
part 9 reference an unintended surface. Add a dump: for each polygon
|
||||
in gfx 0x0100120D, print `PosSurface` index + the resolved Surface id.
|
||||
3. **Multi-layer texture composition retail does and we skip.** AC's
|
||||
"ApplyCloth" or similar layered texture step. Grep
|
||||
`acclient_2013_pseudo_c.txt` for `BlendBaseLayer`, `LayerSurfaces`,
|
||||
any composition method that combines multiple textures into one.
|
||||
4. **UV mapping bug.** Part 9's polygon UVs map to a skin region of
|
||||
the coat texture. Dump per-vertex UV vs vertex Z; if a high-Z vertex
|
||||
has UV.v near a skin region, that's the source.
|
||||
ALL of the obvious hypotheses ruled out:
|
||||
|
||||
- **Byte-level decode primitive matches ACViewer.** INDEX16/P8/DXT/BGRA paths are byte-identical.
|
||||
- **Polygon emission matches retail.** All 43 polygons of gfx `0x0100120D` are `SidesType=0` (ST_SINGLE), all surfaces are `Base1Image` — NO ST_DOUBLE polygons we'd be missing, NO surfaces lacking the `Type & 6` bits that retail's `DrawPolyInternal` skips.
|
||||
- **Per-PART texture-override scoping is correct.** `resolvedOverridesByPart[partIdx]` gets per-MeshRef'd; not a global flat map (Agent 3's claim was wrong).
|
||||
- **SubPalettes are full-size (Colors.Count=2048) palettes.** Our `subPal.Colors[idx]` indexing matches ACViewer's `newPalette.Colors[j + offset]`.
|
||||
- **The `*8` wire un-pack is correctly single-applied** (parser stores raw bytes; ComposePalette multiplies once).
|
||||
|
||||
**The actual smoking gun (Investigation 2):**
|
||||
|
||||
For `+Acdream` the server sends 10 SubPaletteSwap ranges that overlay palette indices:
|
||||
`[0..320)`, `[576..1024)`, `[1392..1488)`, `[1728..1920)`. **The complement — indices `[320..576)`, `[1024..1392)`, `[1488..1728)`, `[1920..2048)` — is NOT overlaid.** Base palette `0x0400007E` at those indices contains the original red/skin tones (sampled values: `0x46 0x22 0x04`, `0x4A 0x28 0x09`, etc).
|
||||
|
||||
If the coat texture's UVs at the upper region map to texel-bytes whose palette index lands in one of those non-overlaid ranges, those pixels render with base-palette skin tones. That's the visible "skin stub at the top of the coat".
|
||||
|
||||
**Working hypothesis:** either
|
||||
1. ACE sends incomplete SubPalette ranges (retail-original would cover the full palette)
|
||||
2. Retail does *additional* client-side compute that ACE pre-resolves wrongly
|
||||
3. The base palette `0x0400007E` itself is supposed to have coat colors at those indices in retail's interpretation (different palette decode)
|
||||
|
||||
**Next investigation (deferred):**
|
||||
|
||||
- Diff ACE's `WorldObject_Networking.cs` CharGen ObjDesc construction against retail's
|
||||
`ClothingTable::BuildObjDesc` (`acclient_2013_pseudo_c.txt:436261`). Check if ACE
|
||||
actually walks every CloSubPaletteRange in the chosen PaletteTemplate, or skips some.
|
||||
- RenderDoc capture: confirm which texel/palette-index the upper-region polygons sample.
|
||||
- `tools/InspectCoatTex/Program.cs` is the diagnostic harness — extend it.
|
||||
|
||||
**Files (diagnostic env vars committed for next-session reuse):**
|
||||
|
||||
|
|
|
|||
|
|
@ -2008,7 +2008,33 @@ public sealed class GameWindow : IDisposable
|
|||
if (spawn.SubPalettes is { } subPaletteList)
|
||||
{
|
||||
foreach (var subPal in subPaletteList)
|
||||
Console.WriteLine($" SP id=0x{subPal.SubPaletteId:X8} offset={subPal.Offset} length={subPal.Length}");
|
||||
{
|
||||
int rawOffset = subPal.Offset * 8;
|
||||
int rawLen = subPal.Length == 0 ? 2048 : subPal.Length * 8;
|
||||
var pal = _dats.Get<DatReaderWriter.DBObjs.Palette>(subPal.SubPaletteId);
|
||||
string palInfo = pal is null ? "Palette dat NOT FOUND (might be PaletteSet 0x0F?)" : $"Colors.Count={pal.Colors.Count}";
|
||||
Console.WriteLine($" SP id=0x{subPal.SubPaletteId:X8} wireOffset={subPal.Offset} wireLength={subPal.Length} -> rawIdx[{rawOffset}..{rawOffset + rawLen}) {palInfo}");
|
||||
// If pal is non-null and small, show first 4 colors
|
||||
if (pal is not null && pal.Colors.Count > 0)
|
||||
{
|
||||
int sample = Math.Min(4, pal.Colors.Count);
|
||||
for (int s = 0; s < sample; s++)
|
||||
{
|
||||
var c = pal.Colors[s];
|
||||
Console.WriteLine($" pal[{s:D3}] R={c.Red:X2} G={c.Green:X2} B={c.Blue:X2}");
|
||||
}
|
||||
// Also probe at the rawOffset (if in range) — that's where overlay copies FROM in our code
|
||||
if (rawOffset < pal.Colors.Count)
|
||||
{
|
||||
var c = pal.Colors[rawOffset];
|
||||
Console.WriteLine($" pal[{rawOffset:D4}] R={c.Red:X2} G={c.Green:X2} B={c.Blue:X2} <-- our code reads here");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" pal[{rawOffset:D4}] OUT OF RANGE (Colors.Count={pal.Colors.Count}) -- our code's read SKIPS the overlay !!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var change in animPartChanges)
|
||||
|
|
|
|||
313
tools/InspectCoatTex/Program.cs
Normal file
313
tools/InspectCoatTex/Program.cs
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
// 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<SurfaceTexture>(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<RenderSurface>(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<Palette>(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<GfxObj>(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<Surface>(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<SurfaceTexture>(origTex, out var st) && st is not null && st.Textures.Count > 0)
|
||||
{
|
||||
uint rsid = (uint)st.Textures[0];
|
||||
if (dats.TryGet<RenderSurface>(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<GfxObj>(gfxId, out var gfx) || gfx is null) return;
|
||||
foreach (var sQid in gfx.Surfaces)
|
||||
{
|
||||
uint sid = (uint)sQid;
|
||||
if (!dats.TryGet<Surface>(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<ClothingTable>(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<uint>();
|
||||
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<Palette>(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<GfxObj>(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<Surface>(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<int, int>();
|
||||
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<Surface>(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 "";
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue