using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; namespace AcDream.Core.CharGen; /// /// Top-level, presentation-free, immutable projection of retail's CharGen /// DAT table (portal.dat 0x0E000002, ACCharGenData::Serialize @ /// 0x005C36D0) PLUS the global SkillTable (portal.dat 0x0E000004) that /// retail falls back to when a heritage's own skill-cost list has no entry /// for a given skill id. Retail's ACCharGenData::GetSkillTrainedCost @ /// 0x005C26D0 / GetSkillSpecializedCost @ 0x005C27D0 both scan /// the heritage's own list first and, on a miss (or an empty list), fall /// through to DBCache::GetFromEnumStatic(4, 2, 0x10000004) — the /// SAME global SkillTable every other skill-cost lookup in the client /// reads — rather than treating the skill as free or invalid. See /// . Production builds create this /// from the installed DAT through Content's /// AcDream.Content.CharGen.ChargenTableReader.Load; this type itself /// has no DAT/Chorizite dependency so it is safe to hand to plugin-facing /// or test code. Every collection is frozen/immutable at construction (a /// caller cannot downcast an /// back to a mutable and mutate this /// process-shared model out from under other readers). Everything a "typed /// chargen options model" needs — starter areas, heritages, templates, /// per-gender appearance option lists, skill costs — hangs off this one /// root. /// /// /// Campaign CC gate round 1 closeout (Group 2): the global SkillTable's /// MinLevel/Description/Formula per skill (see 's /// own doc for why these are GLOBAL-only, unlike /// which also has a per-heritage counterpart). Defaults to null (not an /// empty dictionary) so every pre-existing caller that builds a /// without this parameter — five test fixtures /// plus ChargenOptions.Empty below — compiles and behaves exactly as /// before; treats null the same as "empty." /// public sealed record ChargenOptions( IReadOnlyList StarterAreas, IReadOnlyDictionary HeritagesById, IReadOnlyDictionary GlobalSkillCostsBySkillId, IReadOnlyDictionary? GlobalSkillDetailsBySkillId = null) { public static ChargenOptions Empty { get; } = new( Array.Empty(), FrozenDictionary.Empty, FrozenDictionary.Empty); public bool TryGetHeritage(uint heritageId, [MaybeNullWhen(false)] out ChargenHeritageOptions heritage) => HeritagesById.TryGetValue(heritageId, out heritage); public bool TryGetStarterArea(int index, [MaybeNullWhen(false)] out ChargenStarterArea area) { if (index >= 0 && index < StarterAreas.Count) { area = StarterAreas[index]; return true; } area = default; return false; } /// See 's own doc. public bool TryGetSkillDetail(uint skillId, [MaybeNullWhen(false)] out ChargenSkillDetail detail) { if (GlobalSkillDetailsBySkillId is { } details && details.TryGetValue(skillId, out detail)) return true; detail = default; return false; } }