using AcDream.Content.CharGen; using AcDream.Core.CharGen; using DatReaderWriter; using DatReaderWriter.Options; namespace AcDream.Content.Tests.CharGen; /// /// Installed-DAT gate for : proves the real /// CharGen table (portal.dat 0x0E000002) loads through the SAME /// production uses and lands in a /// plausible shape. Env-gated like the rest of Content.Tests' installed-DAT /// suite () — skips /// cleanly (with a console note) when no DAT directory is configured, /// matching e.g. PakEquivalenceTests / RetailDatLoaderTests. /// public sealed class ChargenTableReaderInstalledDatTests { // ACE ACE.Entity.Enum.HeritageGroup — the four heritages CC1's spec // calls out by name. private const uint AluvianId = 1u; private const uint GharundimId = 2u; private const uint ShoId = 3u; private const uint ViamontianId = 4u; private const uint OlthoiId = 12u; private const uint OlthoiAcidId = 13u; [Fact] public void InstalledCharGenTable_LoadsAndHasThirteenHeritageGroups() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); // ACE's loader comment: "11 standard player races and 2 Olthoi". Assert.Equal(13, options.HeritagesById.Count); Assert.NotEmpty(options.StarterAreas); } [Fact] public void InstalledCharGenTable_HasTheFourNamedHeritagesWithRetailDisplayNames() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); Assert.True(options.TryGetHeritage(AluvianId, out ChargenHeritageOptions? aluvian)); Assert.Equal("Aluvian", aluvian!.Name); Assert.True(options.TryGetHeritage(GharundimId, out ChargenHeritageOptions? gharundim)); Assert.Equal("Gharu'ndim", gharundim!.Name); Assert.True(options.TryGetHeritage(ShoId, out ChargenHeritageOptions? sho)); Assert.Equal("Sho", sho!.Name); Assert.True(options.TryGetHeritage(ViamontianId, out ChargenHeritageOptions? viamontian)); Assert.Equal("Viamontian", viamontian!.Name); Assert.True(options.TryGetHeritage(OlthoiId, out ChargenHeritageOptions? olthoi)); Assert.True(olthoi!.IsOlthoi); Assert.True(options.TryGetHeritage(OlthoiAcidId, out ChargenHeritageOptions? olthoiAcid)); Assert.True(olthoiAcid!.IsOlthoi); } [Fact] public void InstalledHeritages_EachHasAtLeastOneGenderWithNonEmptyAppearanceOptions() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); Assert.NotEmpty(options.HeritagesById); foreach (ChargenHeritageOptions heritage in options.HeritagesById.Values) { Assert.NotEmpty(heritage.GendersByKey); Assert.Contains( heritage.GendersByKey.Values, gender => gender.HasAnyAppearanceOptions); } } /// /// Every template's six attributes fall within retail's 10..100 range, /// and no template's total exceeds its heritage's AttributeCredits /// budget. NOT every template fully spends the budget — each of the /// four human heritages (and Olthoi Acid) ships an "Adventurer" template /// sitting at the floor (60 of a 330 budget; confirmed against the /// installed DAT), which is retail's "Custom" starting point delivered /// as a real TemplateCG entry rather than a special-cased UI-only /// option. Every OTHER named human template (Bow Hunter, Swashbuckler, /// Life Caster, War Mage, Wayfarer, Soldier) exactly exhausts its /// heritage's credits, and Olthoi's single "Ripper" template exactly /// exhausts its (much smaller, non-customizable) 60-credit budget — so /// SOME template somewhere fully spends its budget, even though no /// single heritage is guaranteed to (Olthoi Acid's only template is the /// unspent "Adventurer" one). /// [Fact] public void InstalledHeritages_EveryTemplateAttributeSpreadFitsTheAttributeBudget() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); int templatesChecked = 0; bool anyFullySpentAnywhere = false; foreach (ChargenHeritageOptions heritage in options.HeritagesById.Values) { foreach (ChargenTemplate template in heritage.Templates) { templatesChecked++; Assert.True( ChargenAttributeMath.AreAllWithinRange(template.Attributes), $"{heritage.Name}/{template.Name}: an attribute fell outside " + $"{ChargenAttributeMath.AttributeMin}..{ChargenAttributeMath.AttributeMax} " + $"({template.Attributes})."); Assert.True( template.Attributes.Total <= heritage.AttributeCredits, $"{heritage.Name}/{template.Name}: attribute spread totals " + $"{template.Attributes.Total}, exceeding the {heritage.AttributeCredits}-credit budget."); anyFullySpentAnywhere |= ChargenAttributeMath.IsFullySpent(heritage.AttributeCredits, template.Attributes); } } Assert.True(templatesChecked > 0, "Expected at least one profession template across all heritages."); Assert.True( anyFullySpentAnywhere, "Expected at least one named preset template to fully spend its heritage's credit budget."); } [Fact] public void InstalledHeritages_PrimaryAndSecondaryStartAreaIndicesResolveIntoTheSharedList() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); int indicesChecked = 0; foreach (ChargenHeritageOptions heritage in options.HeritagesById.Values) { foreach (int index in heritage.PrimaryStartAreaIndices.Concat(heritage.SecondaryStartAreaIndices)) { indicesChecked++; Assert.True( options.TryGetStarterArea(index, out ChargenStarterArea? area), $"{heritage.Name}: start-area index {index} does not resolve into the shared StarterAreas list."); Assert.False(string.IsNullOrEmpty(area!.Name)); } } Assert.True(indicesChecked > 0, "Expected at least one heritage start-area reference."); } [Fact] public void InstalledHeritages_SkillCostsResolveToKnownWireSkillIds() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); Assert.True(options.TryGetHeritage(AluvianId, out ChargenHeritageOptions? aluvian)); Assert.NotEmpty(aluvian!.SkillCostsBySkillId); foreach (var pair in aluvian.SkillCostsBySkillId) { Assert.Equal(pair.Key, pair.Value.SkillId); Assert.InRange(pair.Key, 1u, 54u); Assert.True(pair.Value.NormalCost >= 0); Assert.True(pair.Value.PrimaryCost >= 0); } } /// /// F2's completeness assertion: records EXACTLY what the installed DAT's /// heritage-vs-global skill-cost coverage looks like (found /// 2026-08-15), so 's two-tier /// fallback (see ) /// has a real regression gate instead of only synthetic unit fixtures. /// Findings: the global SkillTable (portal.dat 0x0E000004) prices 38 of /// the 54 advancement skill ids; EVERY one of the 13 installed /// heritages ships EXACTLY one heritage-specific skill-cost override /// (never zero, never more), and that one override always ALSO has a /// global entry — no heritage in this DAT relies on a heritage-only /// price the global table doesn't know about. The remaining 16 skill /// ids are absent from BOTH tiers in every heritage — retail's genuine /// -1/"no cost" case (non-advancement or deprecated skill slots such as /// the pre-Skill-DID-remap gaps). If a future DAT drop changes any of /// this shape, this test should fail and get updated with the new /// reality, not be loosened silently. /// [Fact] public void InstalledHeritages_SkillCostFallbackCoversTheKnownUncostableSkillSet() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); Assert.Equal(38, options.GlobalSkillCostsBySkillId.Count); // Retail's -1/"no cost" case: absent from BOTH the global SkillTable // AND every heritage's own list in the installed DAT. uint[] expectedUncostableSkillIds = [1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 17, 25, 26, 42, 53]; Assert.NotEmpty(options.HeritagesById); foreach (ChargenHeritageOptions heritage in options.HeritagesById.Values) { Assert.Single(heritage.SkillCostsBySkillId); var uncostable = new List(); foreach (KeyValuePair pair in heritage.SkillCostsBySkillId) { Assert.True( options.GlobalSkillCostsBySkillId.ContainsKey(pair.Key), $"{heritage.Name}: skill {pair.Key} is heritage-only with no global fallback entry — " + "new ground truth found; update this test's recorded reality."); } for (uint skillId = 1; skillId < ChargenSkillAdvancementSet.SlotCount; skillId++) { bool inHeritage = heritage.SkillCostsBySkillId.ContainsKey(skillId); bool inGlobal = options.GlobalSkillCostsBySkillId.ContainsKey(skillId); if (!inHeritage && !inGlobal) uncostable.Add(skillId); } Assert.Equal(expectedUncostableSkillIds, uncostable.OrderBy(id => id)); } } /// /// Campaign CC gate round 1 closeout (Group 2): pins /// 's real /// shape against the installed DAT — same 38-entry population as /// (both read the /// SAME SkillTable loop), MinLevel distributed 23 at <= 1 /// (useable while Untrained) / 15 at exactly 2 (Trained /// required), per the Batch F investigation's own recorded finding, and /// never above 2 — UpdateSkillEntry's own bucket test only ever /// distinguishes <= 1 from > 1, so a MinLevel of 3 /// would be observationally identical to 2 but is worth flagging if a /// future DAT drop ever introduces one. /// [Fact] public void InstalledSkillTable_GlobalSkillDetails_MinLevelDistributionMatchesCostCoverage() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); Assert.Equal(options.GlobalSkillCostsBySkillId.Count, options.GlobalSkillDetailsBySkillId!.Count); Assert.Equal(38, options.GlobalSkillDetailsBySkillId.Count); int useableUntrained = 0; int trainedRequired = 0; bool anyDescriptionNonEmpty = false; foreach (ChargenSkillDetail detail in options.GlobalSkillDetailsBySkillId.Values) { Assert.True(detail.MinLevel <= 2u, $"skill {detail.SkillId}: MinLevel {detail.MinLevel} exceeds Trained(2)."); if (detail.MinLevel <= 1u) useableUntrained++; else trainedRequired++; anyDescriptionNonEmpty |= !string.IsNullOrEmpty(detail.Description); } Assert.Equal(23, useableUntrained); Assert.Equal(15, trainedRequired); Assert.True(anyDescriptionNonEmpty, "Expected at least one skill to carry a non-empty description."); } /// /// F5's strengthened installed-DAT gate. /// only proves an OR across eight lists for at least one gender per /// heritage (see /// above). This test records the STRONGER fact actually found in the /// installed DAT (2026-08-15): every one of the eight /// HasAnyAppearanceOptions lists, PLUS the three color lists it /// deliberately excludes (, /// , /// ), is non-empty for /// EVERY gender of EVERY one of the 13 heritages — even the sparse ones /// (Gear Knight and both Olthoi variants ship as few as 1-2 entries per /// list, but never 0). This is a fact about today's data, not a /// structural guarantee the DAT format enforces — the type's own doc /// comment still warns callers to defend against an empty list /// individually, and a future DAT could reintroduce a gap (e.g. a /// bald-only heritage's hair styles). If this test starts failing on a /// new DAT drop, that is this gate doing its job, not a reader bug. /// [Fact] public void InstalledHeritages_AppearanceOptionListsRecordedPerListCompleteness() { string? datDir = ContentConformanceDats.ResolveDatDir(); if (datDir is null) { Console.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ChargenOptions options = ChargenTableReader.Load(adapter); Assert.NotEmpty(options.HeritagesById); var emptyLists = new List(); foreach (ChargenHeritageOptions heritage in options.HeritagesById.Values) { foreach (KeyValuePair genderPair in heritage.GendersByKey) { ChargenGenderOptions gender = genderPair.Value; string label = $"{heritage.Name}/{gender.Name} (heritage={heritage.HeritageId}, gender={genderPair.Key})"; void RecordIfEmpty(string listName, int count) { if (count == 0) emptyLists.Add($"{label}: {listName}"); } RecordIfEmpty(nameof(gender.HairStyles), gender.HairStyles.Count); RecordIfEmpty(nameof(gender.EyeStrips), gender.EyeStrips.Count); RecordIfEmpty(nameof(gender.NoseStrips), gender.NoseStrips.Count); RecordIfEmpty(nameof(gender.MouthStrips), gender.MouthStrips.Count); RecordIfEmpty(nameof(gender.Headgears), gender.Headgears.Count); RecordIfEmpty(nameof(gender.Shirts), gender.Shirts.Count); RecordIfEmpty(nameof(gender.Pants), gender.Pants.Count); RecordIfEmpty(nameof(gender.Footwear), gender.Footwear.Count); RecordIfEmpty(nameof(gender.HairColors), gender.HairColors.Count); RecordIfEmpty(nameof(gender.EyeColors), gender.EyeColors.Count); RecordIfEmpty(nameof(gender.ClothingColors), gender.ClothingColors.Count); } } Assert.Empty(emptyLists); } }