Commit graph

2 commits

Author SHA1 Message Date
Erik
cb4703e8d5 fix(chargen): CC1 review fix round — Custom is template 0, SkillTable cost fallback, frozen model
Implements all six Opus review findings against 04450041 (Campaign CC
CC1 chargen data layer):

- F1 (HIGH, blocking): ChargenTemplate's doc claimed "Custom" has no
  ChargenTemplate entry and cited two nonexistent addresses. Verified
  against the named retail decomp: gmCGProfessionPage::UpdateProfession
  @ 0x004821b0 resolves BOTH the highlighted button and the description
  string from CharGenState.template_ 0..6, and case 0 is button
  0x100003d9 / ID_CharGen_CustomText. Custom IS template index 0 (the
  "Adventurer" row CC1 already found sitting at the attribute floor).
  CharGenState::SetTemplate @ 0x005C5A60 confirms every button (including
  Custom) calls CharGenState::ApplyTemplate @ 0x005C5080 when committing,
  so selecting Custom resets the sliders/skills to that row rather than
  leaving them untouched.

- F2 (MEDIUM): retail's skill-cost lookup is two-tiered
  (ACCharGenData::GetSkillTrainedCost/GetSkillSpecializedCost @
  0x005C26D0/0x005C27D0 fall through to the global SkillTable,
  portal.dat 0x0E000004, on a heritage-list miss — confirmed against
  ACE's identical PlayerFactory.cs precedence). ChargenTableReader now
  also projects the global SkillTable into
  ChargenOptions.GlobalSkillCostsBySkillId, and
  ChargenSkillCreditMath.ComputeSpent/RemainingCredits check the
  heritage list first and the global list on a miss. Added an
  installed-DAT completeness assertion recording reality: the global
  table prices 38/54 advancement skill ids, every one of the 13
  installed heritages ships exactly one heritage-specific override
  (always also priced globally), and 16 ids are genuinely uncostable in
  both tiers. Also filed a CC7 risk-item note: ACE's own heritage-
  override branch over-deducts on Specialize (PlayerFactory.cs:184-211)
  — a retail-legal build may be rejected by local ACE at the CC7
  connected gate; that is an ACE bug, not an acdream defect.

- F3 (MEDIUM): every collection ChargenTableReader hands into the
  record model is now frozen at projection (ToFrozenDictionary/ToArray,
  matching MagicCatalog's house pattern), including both
  ChargenOptions.Empty dictionaries.

- F4 (LOW): added a reflection guard test
  (ChargenNoChoriziteLeakTests) that walks every public
  AcDream.Core.CharGen member (property/indexer/constructor/method
  types, recursively through generic arguments) and fails if any
  resolves to the DatReaderWriter or a Chorizite* assembly.

- F5 (LOW): ChargenGenderOptions.HasAnyAppearanceOptions's doc now
  states precisely what the installed-DAT gate proves (an OR across
  eight lists, for at least one gender per heritage) rather than the
  stronger claim it previously made, and explicitly calls out the three
  omitted color lists. Added a second installed-DAT gate that records
  per-list reality across every gender of every heritage — found
  complete, no empty lists anywhere in the installed DAT today.

- F6 (LOW): ChargenOptions.TryGetHeritage/TryGetStarterArea now use
  [MaybeNullWhen(false)] instead of null! suppression, matching the
  house pattern already used elsewhere in the test suite. Fixed every
  call site this surfaced (more than the five originally estimated,
  since Content.Tests has TreatWarningsAsErrors).

Core.Tests: 4737 passed / 1 skip (pre-existing, unrelated).
Content.Tests: 145 passed / 0 skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 13:33:37 +02:00
Erik
0445004164 feat(content): Campaign CC CC1 — chargen table reader and typed options model
Adds the CC1 data layer for Campaign CC (retail character creation):
a reader for portal.dat's CharGen table (0x0E000002) plus a
presentation-free, Chorizite-free typed options model, and the pure
attribute/skill credit math the later CC3 Runtime owner needs.

Retail oracle (docs/research/named-retail/acclient_2013_pseudo_c.txt):
- ACCharGenData::Serialize @ 0x005C36D0 (table shape: StartingAreas +
  HeritageGroups)
- HeritageGroup_CG::Serialize @ 0x005C2100
- Sex_CG::Serialize @ 0x005C1600
- Template_CG::Serialize @ 0x005C0450
- CharGenState::SetHeritageGroup @ 0x005C67A0 and the six attribute-slider
  setters (~0x005C46CE..0x005C494E): remainingAtrbCredits = totalAtrbCredits
  - (str+end+coord+quick+focus+self) — a heritage's AttributeCredits is the
  budget the six RAW attribute values must fit, not points above the floor.
- CharGenState::Reset @ 0x005C68A0: atrbMin=10, atrbMax=100.
- gmCharGenMainUI::DoFinish @ 0x004E9170: Finish refuses only when
  remainingAtrbCredits > 0 (attributes only — skill credits are never
  gated to zero, confirmed by reading the function body).
- CharGenState::UpdateRemainingSkillCredits @ 0x005C37C0: exactly one of
  NormalCost/PrimaryCost is charged per Trained/Specialized skill.
- gmCGAppearancePage::Update @ 0x0047E8F0: the mHeritageGroup==0xc/0xd
  (Olthoi/OlthoiAcid) camera-offset branch CC6 will need.

Cross-checked against ACE's ACE.DatLoader.FileTypes.CharGen and
ACE.DatLoader.Entity.HeritageGroupCG/SexCG/TemplateCG/SkillCG loaders
(same field order, different byte format) and ACE.Entity.Enum.HeritageGroup
/ SkillAdvancementClass for the two small stable enums the model exposes.

src/AcDream.Core/CharGen/: ChargenOptions (root: StarterAreas +
HeritagesById), ChargenHeritageOptions, ChargenGenderOptions (BaseObjDesc
+ every appearance-option list: hair styles/colors, eye colors, eye/nose/
mouth strips, headgear/shirt/pants/footwear, clothing colors),
ChargenTemplate, ChargenObjDesc (palette/subpalette/texture/anim-part-swap
shape, mirrors PaletteOverride's presentation-free pattern), and the pure
math: ChargenAttributeMath (RemainingCredits/IsFullySpent/range checks) and
ChargenSkillCreditMath (retail's Trained-xor-Specialized cost sum) plus
ChargenSkillAdvancementSet, a structurally-fixed 55-slot type (reserved
slot 0 + SkillId 1..54) so CC2's future wire builder cannot send anything
but exactly 55 entries.

src/AcDream.Content/CharGen/ChargenTableReader.cs projects the Chorizite
DBObj graph into the Core model (MagicCatalog.Load's shape) — no Chorizite
type crosses into ChargenOptions.

Tests: hand-built-fixture unit tests for the pure math (Core.Tests) and the
Content projector (Content.Tests), plus six installed-DAT gate tests
(ContentConformanceDats pattern) against the real portal.dat: 13 heritage
groups (11 standard + 2 Olthoi), the four named heritages with retail
display names incl. "Gharu'ndim", every heritage has a gender with
non-empty appearance option lists, every template's attributes stay in
10..100 and never exceed its heritage's budget (discovered live: NOT every
template fully spends it — each human heritage's "Adventurer" template
sits at the floor as retail's real-DAT-backed "Custom" starting point),
start-area indices resolve into the shared list, and skill costs key to
valid 1..54 wire ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:53:50 +02:00