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