using System.Collections.Frozen; using AcDream.Core.Items; using AcDream.Core.Spells; using DatReaderWriter; using DatReaderWriter.DBObjs; using CoreSpellTable = AcDream.Core.Spells.SpellTable; namespace AcDream.Content; /// /// Immutable presentation and gameplay metadata for one retail spell /// component WCID. /// public sealed record SpellComponentDescriptor( uint WeenieClassId, string Name, uint Category, uint IconId); /// /// Process-shareable projection of retail's spell, component, and /// school-focus DAT tables. The catalog contains no player, inventory, /// session, or presentation state. /// public sealed class MagicCatalog { private const uint SpellTableDid = 0x0E00000Eu; private const uint SpellComponentTableDid = 0x0E00000Fu; private const uint ComponentIdMapDid = 0x27000002u; private readonly FrozenDictionary _formulaDefinitions; private readonly FrozenDictionary _wcidByScid; private readonly FrozenDictionary _magicPackWcidBySchool; private readonly FrozenDictionary _spellLevels; private MagicCatalog( CoreSpellTable spellTable, FrozenDictionary components, FrozenDictionary formulaDefinitions, FrozenDictionary wcidByScid, FrozenDictionary magicPackWcidBySchool, FrozenDictionary spellLevels) { SpellTable = spellTable; Components = components; _formulaDefinitions = formulaDefinitions; _wcidByScid = wcidByScid; _magicPackWcidBySchool = magicPackWcidBySchool; _spellLevels = spellLevels; } public static MagicCatalog Empty { get; } = new( CoreSpellTable.Empty, EmptyFrozen(), EmptyFrozen(), EmptyFrozen(), EmptyFrozen(), EmptyFrozen()); public CoreSpellTable SpellTable { get; } public IReadOnlyDictionary Components { get; } public bool TryGetComponentBySpellComponentId( uint spellComponentId, out SpellComponentDescriptor descriptor) { if (_wcidByScid.TryGetValue( spellComponentId, out uint weenieClassId) && Components.TryGetValue( weenieClassId, out SpellComponentDescriptor? found)) { descriptor = found; return true; } descriptor = null!; return false; } public bool IsComponentPack(uint weenieClassId) => Components.ContainsKey(weenieClassId); public int GetSpellLevel(uint spellId) => _spellLevels.TryGetValue(spellId, out int level) ? level : 0; /// /// Creates the per-session policy view over this immutable catalog. The /// returned service borrows only the supplied session's object table and /// identity/account delegates. /// public SpellComponentRequirementService CreateRequirementService( ClientObjectTable objects, Func playerGuid, Func accountName) => new( objects, playerGuid, accountName, _formulaDefinitions, _wcidByScid, _magicPackWcidBySchool); public static MagicCatalog Load(IDatReaderWriter dats) { ArgumentNullException.ThrowIfNull(dats); var components = new Dictionary(); var wcidByScid = new Dictionary(); SpellComponentTable? componentTable = dats.Get(SpellComponentTableDid); DualEnumIDMap? componentIds = dats.Get(ComponentIdMapDid); if (componentTable is not null && componentIds is not null) { foreach (var pair in componentTable.Components) { if (!componentIds.ClientEnumToID.TryGetValue( pair.Key, out uint wcid)) { continue; } wcidByScid[pair.Key] = wcid; components[wcid] = new SpellComponentDescriptor( wcid, pair.Value.Name.Value, pair.Value.Category, pair.Value.Icon.DataId); } } var metadata = new List(); var formulaDefinitions = new Dictionary(); var spellLevels = new Dictionary(); DatReaderWriter.DBObjs.SpellTable spellTable = dats.Get(SpellTableDid) ?? throw new InvalidOperationException( $"Required retail SpellTable 0x{SpellTableDid:X8} is missing from portal.dat."); foreach (var pair in spellTable.Spells) { SpellMetadata spell = RetailSpellMetadataProjector.Project( pair.Key, pair.Value, componentTable); metadata.Add(spell); uint[] formula = spell.FormulaComponents.ToArray(); formulaDefinitions[pair.Key] = new SpellFormulaDefinition( spell.FormulaVersion, formula, (uint)spell.SchoolId); spellLevels[pair.Key] = spell.Generation; } var magicPackWcidBySchool = new Dictionary(); // Retail IsComponentPack resolves enum key 0x10000001 in category // 0x28; that key is not itself a portal.dat file id. uint magicPackMapDid = RetailDataIdResolver.Resolve( dats, 0x10000001u, 0x28u); if (magicPackMapDid != 0u && dats.Portal.TryGet( magicPackMapDid, out EnumIDMap? magicPackMap) && magicPackMap is not null) { foreach (var pair in magicPackMap.ClientEnumToID) magicPackWcidBySchool[pair.Key] = pair.Value; } return new MagicCatalog( CoreSpellTable.Create(metadata), components.ToFrozenDictionary(), formulaDefinitions.ToFrozenDictionary(), wcidByScid.ToFrozenDictionary(), magicPackWcidBySchool.ToFrozenDictionary(), spellLevels.ToFrozenDictionary()); } private static FrozenDictionary EmptyFrozen() => new Dictionary().ToFrozenDictionary(); }