namespace AcDream.Core.CharGen; /// /// Retail's four skill states. Wire values match ACE's /// ACE.Entity.Enum.SkillAdvancementClass exactly (0=Inactive, /// 1=Untrained, 2=Trained, 3=Specialized) — ACE unpacks the 0xF656 /// CharacterCreateInfo.SkillAdvancementClasses list with this same /// numbering, and retail's CharGenState::UpdateRemainingSkillCredits @ /// 0x005C37C0 only charges credits for Trained (2) and Specialized (3). /// public enum ChargenSkillAdvancementClass : uint { Inactive = 0, Untrained = 1, Trained = 2, Specialized = 3, } /// /// One skill's retail training cost for a heritage. Retail schema: /// SkillCG, entries of HeritageGroupCG.Skills. /// PrimaryCost is the TOTAL cost to reach Specialized (not an /// increment on top of NormalCost) — retail's /// UpdateRemainingSkillCredits adds exactly one of the two per /// skill, never both. /// public readonly record struct ChargenSkillCost(uint SkillId, int NormalCost, int PrimaryCost); /// /// Retail's fixed-size per-character skill-advancement array /// (CharGenState.skillLevels). ACE's CharacterCreateInfo.Unpack /// terminates the connection if the wire's numSkills count is not /// exactly (55): retail's own loop in /// UpdateRemainingSkillCredits walks indices 1..totalNumSkills /// (skipping reserved slot 0), and Chorizite's SkillId enum runs /// 1..54 — 54 real skills plus the reserved slot 0 is exactly 55. This type /// makes that shape structural: it always holds exactly 55 slots, so a /// caller building the 0xF656 body (CC2) cannot accidentally send a /// different count. /// public sealed class ChargenSkillAdvancementSet { /// Slot 0 is reserved (unused by retail); slots 1..54 map 1:1 /// to Chorizite's DatReaderWriter.Enums.SkillId values. public const int SlotCount = 55; private readonly ChargenSkillAdvancementClass[] _slots = new ChargenSkillAdvancementClass[SlotCount]; /// Skill state by raw skill id. Ids outside 1..54 read /// as and cannot be /// set. public ChargenSkillAdvancementClass this[uint skillId] { get => skillId >= 1 && skillId < SlotCount ? _slots[skillId] : ChargenSkillAdvancementClass.Inactive; set { if (skillId < 1 || skillId >= SlotCount) throw new ArgumentOutOfRangeException( nameof(skillId), skillId, $"Skill id must be in 1..{SlotCount - 1}."); _slots[skillId] = value; } } /// /// Materializes the wire body shape: exactly /// entries, slot 0 first, matching ACE's /// CharacterCreateInfo.SkillAdvancementClasses read order. /// public IReadOnlyList ToWireClasses() { var wire = new uint[SlotCount]; for (int i = 0; i < SlotCount; i++) wire[i] = (uint)_slots[i]; return wire; } }