feat(net+app): SubPalette overlays applied to palette-indexed textures (Phase 5b)
Implements the other half of ObjDesc: SubPalettes (palette-range
overlays) that repaint palette-indexed textures with per-entity color
schemes. Ported algorithm from ACViewer Render/TextureCache.IndexToColor
after the user pointed out I was prematurely implementing from scratch
instead of checking all the reference repos.
The Nullified Statue of a Drudge sends (setup=0x020007DD with a drudge
GfxObj animPart replacing part 1, plus 2 texChanges targeted at part 1,
plus 1 subpalette id=0x04001351 offset=0 length=0). The TextureChanges
swap fine detail surfaces; the SubPalette with length=0 ("entire palette"
per Chorizite docs) remaps the drudge's flesh-tone palette to stone.
Without this commit, the statue looked like a normal flesh drudge
because palette-indexed textures decoded with the base flesh palette.
Added:
- Core/World/PaletteOverride.cs: per-entity record carrying
BasePaletteId + a list of (SubPaletteId, Offset, Length) range
overlays. Documents the "offset/length are wire-scaled by 8"
convention and the "length=0 means whole palette" sentinel.
- WorldEntity.PaletteOverride nullable field. Per-entity (same across
all parts), in contrast to MeshRef.SurfaceOverrides which is per-part.
- TextureCache.GetOrUploadWithPaletteOverride: new entry point that
composes the effective palette at decode time. Composite cache key
is (surfaceId, origTexOverride, paletteHash) so entities with
equivalent palette setups share the GL texture.
- ComposePalette: ports ACViewer's IndexToColor overlay loop:
for each subpalette sp:
startIdx = sp.Offset * 8 // multiply back from wire
count = sp.Length == 0 ? 2048 : sp.Length * 8 // sentinel
for j in [0, count):
composed[j + startIdx] = subPal.Colors[j + startIdx]
Critical detail: copies from the SAME offset in the sub palette, not
from [0]. Both base and sub are treated as full palettes sharing an
index space.
- StaticMeshRenderer.Draw: three-way switch on (entity.PaletteOverride,
meshRef.SurfaceOverrides) picks the right TextureCache path:
- Both → palette override (it handles origTex override internally)
- Only tex override → GetOrUploadWithOrigTextureOverride
- Neither → plain GetOrUpload
- GameWindow.OnLiveEntitySpawned: builds PaletteOverride from
spawn.BasePaletteId + spawn.SubPalettes when the server sent any.
Reference note: the user asked "but I mean THIS MUST BE IN WORLDBUILDER"
which was the right push. WorldBuilder is actually a dat VIEWER and its
ClothingTableBrowserViewModel is a 10-line stub — it doesn't apply
palette overlays because it doesn't need to. The actual algorithm lives
in ACViewer (a MonoGame character viewer), which I should have checked
earlier. CLAUDE.md updated with a standing rule: always cross-reference
all four of references/ACE, ACViewer, WorldBuilder, Chorizite.ACProtocol,
plus holtburger. A single reference can be misleading; the intersection
is usually the truth.
Tests: 77 core + 83 net = 160, all green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b69d776179
commit
733f8ff601
6 changed files with 238 additions and 14 deletions
46
src/AcDream.Core/World/PaletteOverride.cs
Normal file
46
src/AcDream.Core/World/PaletteOverride.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
namespace AcDream.Core.World;
|
||||
|
||||
/// <summary>
|
||||
/// Server-specified palette composition for a live-mode entity. Gets
|
||||
/// handed to <see cref="StaticMeshRenderer"/> so the texture cache can
|
||||
/// compose the effective palette (base with sub-palette overlays) at
|
||||
/// decode time and use it when decoding palette-indexed textures
|
||||
/// (PFID_P8 / PFID_INDEX16). Non-palette textures ignore this entirely.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Semantics (ported from Chorizite.ACProtocol.Types.Subpalette docs
|
||||
/// and confirmed against ACE's CalculateObjDesc):</b>
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>BasePaletteId</c> — the entity's base palette dat id
|
||||
/// (<c>0x04XXXXXX</c>). Comes from <c>ObjDesc.Palette</c>. Acts
|
||||
/// as the starting point before subpalette overlays are applied.</item>
|
||||
/// <item>Each overlay entry <c>(SubPaletteId, Offset, Length)</c>
|
||||
/// means "copy the first <c>Length*8</c> colors from the
|
||||
/// subpalette into the base palette starting at index
|
||||
/// <c>Offset*8</c>." Length=0 is a sentinel meaning "entire
|
||||
/// palette" (Chorizite documents it as defaulting to <c>256*8</c>
|
||||
/// which is effectively everything).</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// Palette overlays are per-entity, not per-part: all parts of a
|
||||
/// character share the same composed palette (skin color is consistent
|
||||
/// across the whole body even if different body parts use different
|
||||
/// meshes).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed record PaletteOverride(
|
||||
uint BasePaletteId,
|
||||
IReadOnlyList<PaletteOverride.SubPaletteRange> SubPalettes)
|
||||
{
|
||||
/// <summary>
|
||||
/// One overlay range in a <see cref="PaletteOverride"/>. Offset and
|
||||
/// Length are the raw bytes the server sent; callers multiply by 8
|
||||
/// to get palette-index units.
|
||||
/// </summary>
|
||||
public readonly record struct SubPaletteRange(
|
||||
uint SubPaletteId,
|
||||
byte Offset,
|
||||
byte Length);
|
||||
}
|
||||
|
|
@ -9,4 +9,13 @@ public sealed class WorldEntity
|
|||
public required Vector3 Position { get; init; }
|
||||
public required Quaternion Rotation { get; init; }
|
||||
public required IReadOnlyList<MeshRef> MeshRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional per-entity palette override (server-specified base +
|
||||
/// subpalette overlays). When non-null, applies to every palette-
|
||||
/// indexed texture on this entity. Used for character skin/hair
|
||||
/// colors, creature recolors (e.g. stone-colored drudge statue),
|
||||
/// and team colors. Non-palette-indexed textures ignore this field.
|
||||
/// </summary>
|
||||
public PaletteOverride? PaletteOverride { get; init; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue