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); }