using System.Collections.Concurrent;
using System.Collections.Frozen;
using AcDream.Core.CharGen;
using DatClothingTable = DatReaderWriter.DBObjs.ClothingTable;
using DatPalette = DatReaderWriter.DBObjs.Palette;
using DatPalSet = DatReaderWriter.DBObjs.PalSet;
using DatCloObjectEffect = DatReaderWriter.Types.CloObjectEffect;
using DatCloSubPalette = DatReaderWriter.Types.CloSubPalette;
namespace AcDream.Content.CharGen;
///
/// DAT-backed /
/// implementation: reads PalSet (0x0F......) and ClothingTable (0x19......)
/// dat objects on demand and projects them into 's
/// pure Core types, matching ChargenTableReader's "no Chorizite leak"
/// discipline for everything it returns. Both lookups cache by dat id — a
/// live preview re-composes on every appearance change, and the same
/// PalSet/ClothingTable ids repeat constantly across heritages, genders, and
/// re-selections within one session.
///
///
/// NOT thread-safe on its own (fix round F7, CC6b-MOUNT review):
/// / do a lazy raw
/// _dats.Get<T>() read on first use per id — and the shared
/// DatCollection every sibling in this codebase guards with the
/// process-wide DAT lock is itself NOT thread-safe
/// (feedback_phase_a1_hotfix_saga.md). Every call site MUST hold that
/// same lock (ChargenPreviewController's _datLock, the
/// composition root's d.DatLock) around calls into this class, exactly
/// like every other DAT-touching call in this codebase already does. This
/// class's own caches only
/// protect the CACHE from concurrent mutation — they do nothing for the
/// underlying DatCollection read the cache miss triggers.
///
///
///
/// (Campaign CC gate round 1
/// Batch G, R2-5): the real color-wheel/swatch mechanism
/// (ChargenSwatchColorResolver) needs one more DAT read this class
/// didn't previously do — a raw Palette dat object's (0x04......) own color
/// table, retail's Palette::get_color32 equivalent. Same lazy-cache
/// shape as /,
/// same DAT-lock obligation on every call site.
///
///
public sealed class ChargenAppearanceCatalog :
IChargenPalSetSource, IChargenClothingTableSource, IChargenPaletteColorSource
{
private readonly IDatReaderWriter _dats;
private readonly ConcurrentDictionary _palSets = new();
private readonly ConcurrentDictionary _clothingTables = new();
private readonly ConcurrentDictionary _palettes = new();
public ChargenAppearanceCatalog(IDatReaderWriter dats)
{
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
}
public ChargenPalSet? TryGetPalSet(uint palSetId) =>
_palSets.GetOrAdd(palSetId, LoadPalSet);
public ChargenClothingTable? TryGetClothingTable(uint clothingTableId) =>
_clothingTables.GetOrAdd(clothingTableId, LoadClothingTable);
///
/// Retail's ClientCharGenState::GetColorFromPal @0x00563990: load
/// the Palette dat object and read its color table at a fixed index —
/// direct ARGB[index], no averaging, no shade indirection. Unlike
/// retail's own unchecked array read, this bounds-checks
/// against the loaded palette's actual color
/// count and returns false rather than reading out of range (see
/// 's own doc for
/// why that divergence is deliberate).
///
public bool TryGetColor(uint paletteId, int index, out ChargenSwatchRgb color)
{
color = default;
DatPalette? palette = _palettes.GetOrAdd(paletteId, id => _dats.Get(id));
if (palette is null || index < 0 || index >= palette.Colors.Count)
return false;
DatReaderWriter.Types.ColorARGB c = palette.Colors[index];
color = new ChargenSwatchRgb(c.Red, c.Green, c.Blue);
return true;
}
private ChargenPalSet? LoadPalSet(uint id)
{
DatPalSet? palSet = _dats.Get(id);
if (palSet is null)
return null;
var ids = new uint[palSet.Palettes.Count];
for (int i = 0; i < palSet.Palettes.Count; i++)
ids[i] = palSet.Palettes[i].DataId;
return new ChargenPalSet(Array.AsReadOnly(ids));
}
private ChargenClothingTable? LoadClothingTable(uint id)
{
DatClothingTable? table = _dats.Get(id);
if (table is null)
return null;
var baseEffects = new Dictionary(
table.ClothingBaseEffects.Count);
foreach (var pair in table.ClothingBaseEffects)
baseEffects[pair.Key.DataId] = ProjectBaseEffect(pair.Value.CloObjectEffects);
var templates = new Dictionary(
table.ClothingSubPalEffects.Count);
foreach (var pair in table.ClothingSubPalEffects)
templates[pair.Key] = ProjectPaletteTemplate(pair.Value.CloSubPalettes);
return new ChargenClothingTable(
baseEffects.ToFrozenDictionary(),
templates.ToFrozenDictionary());
}
private static ChargenClothingBaseEffect ProjectBaseEffect(
IReadOnlyList objectEffects)
{
var partChanges = new List(objectEffects.Count);
var textureChanges = new List();
foreach (DatCloObjectEffect effect in objectEffects)
{
var partIndex = (byte)effect.Index;
partChanges.Add(new ChargenAnimPartChange(partIndex, effect.ModelId.DataId));
foreach (var tex in effect.CloTextureEffects)
{
textureChanges.Add(new ChargenTextureChange(
partIndex, tex.OldTexture.DataId, tex.NewTexture.DataId));
}
}
return new ChargenClothingBaseEffect(
Array.AsReadOnly(partChanges.ToArray()),
Array.AsReadOnly(textureChanges.ToArray()));
}
private static ChargenClothingPaletteTemplate ProjectPaletteTemplate(
IReadOnlyList subPalettes)
{
var choices = new ChargenClothingSubPaletteChoice[subPalettes.Count];
for (int i = 0; i < subPalettes.Count; i++)
{
DatCloSubPalette sub = subPalettes[i];
var ranges = new ChargenClothingSubPaletteRange[sub.Ranges.Count];
for (int j = 0; j < sub.Ranges.Count; j++)
ranges[j] = new ChargenClothingSubPaletteRange(sub.Ranges[j].Offset, sub.Ranges[j].NumColors);
choices[i] = new ChargenClothingSubPaletteChoice(sub.PaletteSet.DataId, Array.AsReadOnly(ranges));
}
return new ChargenClothingPaletteTemplate(Array.AsReadOnly(choices));
}
}