using System.Collections.Frozen;
namespace AcDream.Core.CharGen;
///
/// One un-resolved dye-shade choice inside a clothing "palette template"
/// (retail's inner CloSubpalEffect array entry, one per
/// ClothingTable::BuildObjDesc @ 0x005A7900 loop iteration; Chorizite
/// projects the identical shape as DatReaderWriter.Types.CloSubPalette
/// — a PaletteSet id plus a list of overlay ranges). Offsets/counts
/// here are the REAL (unpacked) color units read straight off the dat
/// (installed-DAT probe: Aluvian male "Cloth Cap" headgear reads
/// off=2000,n=48 for every one of its 28 palette-template entries) — the
/// *8-packed byte convention only applies to the OUTPUT
/// , converted once at composition time
/// ().
///
public readonly record struct ChargenClothingSubPaletteRange(uint Offset, uint NumColors);
///
/// One resolvable-by-shade colour choice for a clothing palette template:
/// the PalSet id (0x0F......) to resolve via
/// , plus every overlay range
/// to apply once resolved.
///
public readonly record struct ChargenClothingSubPaletteChoice(
uint PalSetId,
IReadOnlyList Ranges);
///
/// One clothing-table "palette template" (retail's CloPaletteTemplate,
/// looked up in ClothingTable::_paletteTemplatesHash by the id
/// CharGenState::GetHeadgearPaletteTemplateID (and its Shirt/Trousers/
/// Footwear siblings, all at 0x005C38F0-0x005C3980) return — which is itself
/// just a bounds-checked passthrough of Sex_CG.ClothingColors[index]:
/// every one of the four per-slot template-id arrays
/// (headgearPaletteTemplateIDs/shirtPaletteTemplateIDs/
/// trousersPaletteTemplateIDs/footwearPaletteTemplateIDs) is
/// populated from the SAME single Sex_CG::ClothingColors dat field —
/// there is no per-clothing-slot color list in the dat schema at all. This
/// CONFIRMS (does not merely approximate) register row AP-208's shared-list
/// design in RuntimeCharacterCreationAppearance/
/// ChargenAppearanceSlot — installed-DAT probe: Aluvian male's
/// ClothingColors = {9,6,4,8,7,5,2,3,13}, and the "Cloth Cap"
/// headgear's ClothingSubPalEffects keys include 2,3,4,5,6,7,8,9,13 —
/// the shared list's raw values ARE the template-id keys, verified live.
///
public sealed record ChargenClothingPaletteTemplate(
IReadOnlyList Choices)
{
public static ChargenClothingPaletteTemplate Empty { get; } =
new(Array.Empty());
}
///
/// One body-Setup-specific part/texture override set (retail's
/// ClothingBaseEffect, applied by
/// ClothingBase::ApplyPartAndTextureChanges @ 0x005A8EB0): for each
/// CloObjectEffect, an unconditional
/// (part index → replacement GfxObj) plus every
/// the SAME object effect carries for
/// that part.
///
public sealed record ChargenClothingBaseEffect(
IReadOnlyList PartChanges,
IReadOnlyList TextureChanges)
{
public static ChargenClothingBaseEffect Empty { get; } = new(
Array.Empty(),
Array.Empty());
}
///
/// Pure projection of one ClothingTable dat object (0x19......, retail
/// ClothingTable::Unpack / Chorizite
/// DatReaderWriter.DBObjs.ClothingTable). One instance is referenced
/// per — a single garment
/// CHOICE (e.g. "Cloth Cowl") carries its own table covering every body
/// Setup it can be worn on plus every dye choice offered for it.
///
///
/// Deliberate scope cut (CC6a) — MEASURED, not just asserted: retail's
/// ClothingTable::BuildObjDesc falls back through a chain of ~8
/// hard-coded Setup-id substitutions (Umbraen crown/no-crown/void,
/// Penumbraen, Undead skeleton/zombie, Anakshay) when
/// has no direct entry for the requested
/// body Setup. CC6a's composer looks up
/// directly and skips a slot's part/texture contribution on a miss (this is
/// the OUTER lookup — ClothingTable::_cloBaseHash — whose retail
/// miss behavior is genuinely a no-op the caller never checks; the SEPARATE
/// inner per-choice PalSet lookup inside the same function's subpalette loop
/// has its own, stricter, abort-on-miss behavior — see
/// ChargenAppearanceFactory.ComposeClothingSlot's own doc, ported
/// faithfully there) rather than porting the Setup-substitution chain. The
/// installed-DAT catalog test (ChargenAppearanceCatalogInstalledDatTests)
/// MEASURED this directly across all 26 heritage/gender combinations rather
/// than assuming it: for the 9 standard heritages where retail's own UI
/// actually shows clothing controls (everything except Gear Knight and the
/// two Olthoi variants, which retail hides the clothes button for entirely
/// — gmCGAppearancePage::Update @ 0x0047E8F0's
/// m_pClothesButton->SetVisible(0) branches for
/// mHeritageGroup == 6 and == 0xc || == 0xd), the default
/// gear choices resolve against their own body Setup with ZERO missing
/// coverage. Undead IS a real gap — retail DOES show clothing
/// controls for Undead, and MEASURED coverage is missing for ALL FOUR
/// clothing slots (headgear, trousers, shirt, AND footwear — not just three
/// of the four), on both genders: neither gender's live body Setup has a
/// entry in any of its four default gear
/// choices' clothing tables, because Undead's live body Setup IS one of the
/// skeleton/zombie variants the un-ported substitution chain exists to
/// redirect. A live preview for Undead will therefore render its default
/// clothing selection with NO part/texture override applied on any of the
/// four slots (the underlying body shows through unclothed) until the
/// substitution chain — or an equivalent per-heritage default-clothing-setup
/// mapping — lands. Filed as a known CC6a limitation for CC6b/a follow-up
/// rather than silently "confirmed unreachable."
///
///
public sealed record ChargenClothingTable(
IReadOnlyDictionary BaseEffectsBySetupId,
IReadOnlyDictionary PaletteTemplatesById)
{
public static ChargenClothingTable Empty { get; } = new(
FrozenDictionary.Empty,
FrozenDictionary.Empty);
}
///
/// Resolves a PalSet dat id (0x0F......) to its pure projection. The
/// production implementation (AcDream.Content.CharGen.ChargenAppearanceCatalog)
/// reads and caches the real dat object; this interface keeps
/// free of any Chorizite dependency
/// (unit tests supply a hand-built fake).
///
public interface IChargenPalSetSource
{
ChargenPalSet? TryGetPalSet(uint palSetId);
}
///
/// Resolves a ClothingTable dat id (0x19......) to its pure projection.
/// Same production/test split as .
///
public interface IChargenClothingTableSource
{
ChargenClothingTable? TryGetClothingTable(uint clothingTableId);
}