using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace AcDream.Core.Spells; /// /// Pure retail spell-formula helpers. The formula is an eight-entry list of /// spell-component enum IDs (SCIDs), not inventory weenie class IDs. /// public static class RetailSpellFormula { private const uint LowestTaperId = 63u; private const uint PrismaticTaperId = 0xBCu; /// /// Retail MagicSystem::DeterminePowerLevelOfComponent (0x005BD240). /// public static uint DeterminePowerLevelOfComponent(uint componentId) => componentId switch { >= 1u and <= 6u => componentId, 0x6Eu => 7u, 0x70u => 8u, 0xC0u => 9u, 0xC1u => 10u, _ => 0u, }; /// /// Retail CSpellBase::InqSpellLevelByRoughHeuristic (0x00597260). /// This is the I-VIII level used by gmSpellbookUI's level filters. /// public static int InqSpellLevelByRoughHeuristic(IReadOnlyList components) { uint power = components.Count == 0 ? 0u : DeterminePowerLevelOfComponent(components[0]); return power switch { < 7u => (int)power, >= 9u => (int)power - 2, _ => (int)power - 1, }; } /// /// Retail SpellFormula::GetTargetingType @ 0x005BC910 followed by /// SpellComponentTable::GetTargetTypeFromComponentID @ 0x005BBF50. /// The native SpellFormula has a vtable before its eight components. /// Retail starts at native offset index 5 (component slot 4), scans slots /// 5 through 7 for the end of the formula, then maps the final populated /// component. This deliberately does not search for the first recognized /// target component. /// public static uint GetTargetingType(IReadOnlyList components) { ArgumentNullException.ThrowIfNull(components); if (components.Count <= 4) return 0u; int last = 4; while (last < 7 && last + 1 < components.Count && components[last + 1] != 0u) last++; return GetTargetTypeFromComponentId(components[last]); } /// Retail target-component ID to ItemType mask mapping. public static uint GetTargetTypeFromComponentId(uint componentId) => componentId switch { 0x31u or 0x32u or 0x33u or 0x34u or 0x35u or 0x36u or 0x37u or 0x38u or 0x3Cu or 0x3Du or 0x3Eu or 0xBEu => 0x10u, 0x39u => 0x00088B8Fu, 0x3Bu => 0x10010000u, _ => 0u, }; /// /// Retail CSpellBase::InqScarabOnlyFormula (0x00597050). /// Foci and the infused-magic augmentations replace the ordinary taper /// formula with its power components plus one to four prismatic tapers. /// public static IReadOnlyList InqScarabOnlyFormula( IReadOnlyList components) { ArgumentNullException.ThrowIfNull(components); var result = new List(8); uint strongestPower = 0u; foreach (uint component in components.Take(8)) { if (component == 0u) break; if (!IsPowerComponent(component)) continue; result.Add(component); strongestPower = Math.Max( strongestPower, DeterminePowerLevelOfComponent(component)); } int taperCount = strongestPower switch { 1u => 1, 2u => 2, 3u or 4u or 7u => 3, 5u or 6u or 8u or 9u or 10u => 4, _ => 0, }; while (taperCount-- > 0 && result.Count < 8) result.Add(PrismaticTaperId); return result; } private static bool IsPowerComponent(uint component) => component is >= 1u and <= 6u or 0x6Eu or 0x6Fu or 0x70u or 0xC0u or 0xC1u; /// /// Retail PString's CP-1252 hash used by SpellFormula::RandomizeForName. /// public static uint ComputeNameHash(string value) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); long result = 0; foreach (byte raw in Encoding.GetEncoding(1252).GetBytes(value ?? string.Empty)) { sbyte character = unchecked((sbyte)raw); result = character + (result << 4); if ((result & 0xF0000000L) != 0) result = (result ^ ((result & 0xF0000000L) >> 24)) & 0x0FFFFFFF; } return unchecked((uint)result); } /// /// Retail SpellFormula::RandomizeForName (0x005BD050), cross-checked with /// ACE.DatLoader SpellTable.GetSpellFormula. Only taper SCIDs change. /// public static IReadOnlyList CustomizeForAccount( IReadOnlyList components, uint formulaVersion, string accountName) { uint[] result = components.Take(8).ToArray(); return formulaVersion switch { 1u => RandomizeVersion1(result, accountName), 2u => RandomizeVersion2(result, accountName), 3u => RandomizeVersion3(result, accountName), _ => result, }; } private static IReadOnlyList RandomizeVersion1(uint[] c, string accountName) { int count = c.Count(value => value != 0); if (count < 5) return c; uint seed = ComputeNameHash(accountName) % 0x13D573u; uint scarab = c[0]; int herbIndex = count > 5 ? 2 : 1; uint herb = c[herbIndex]; int powderIndex = herbIndex + 1 + (count > 6 ? 1 : 0); if (powderIndex + 1 >= c.Length) return c; uint powder = c[powderIndex]; uint potion = c[powderIndex + 1]; int talismanIndex = powderIndex + 2 + (count > 7 ? 1 : 0); if (talismanIndex >= c.Length) return c; uint talisman = c[talismanIndex]; if (count > 5) c[1] = unchecked(powder + 2u * herb + potion + talisman + scarab) % 12u + LowestTaperId; if (count > 6) { uint denominator = unchecked(scarab + powder + potion); if (denominator != 0) c[3] = unchecked((scarab + herb + talisman + 2u * (powder + potion)) * (seed / denominator)) % 12u + LowestTaperId; } if (count > 7) { uint denominator = unchecked(talisman + scarab); if (denominator != 0) c[6] = unchecked((powder + 2u * talisman + potion + herb + scarab) * (seed / denominator)) % 12u + LowestTaperId; } return c; } private static IReadOnlyList RandomizeVersion2(uint[] c, string accountName) { if (c.Length < 8) return c; uint seed = ComputeNameHash(accountName) % 0x13D573u; uint p1 = c[0], c4 = c[4], x = c[5], a = c[7]; c[3] = unchecked(a + 3u * p1 + 2u * c4 * x + c[2] + c[1]) % 12u + LowestTaperId; uint denominator = unchecked(c[1] * a + 2u * c4); if (denominator != 0) c[6] = unchecked((a + 3u * p1 * c[2] + 2u * x + c4) * (seed / denominator)) % 12u + LowestTaperId; return c; } private static IReadOnlyList RandomizeVersion3(uint[] c, string accountName) { if (c.Length < 7) return c; uint hash = ComputeNameHash(accountName); uint h0 = unchecked(hash % 0x13D573u + c[0]) % 12u; uint h1 = unchecked(hash % 0x4AEFDu + c[1]) % 12u; uint h2 = unchecked(hash % 0x96A7Fu + c[2]) % 12u; uint h4 = unchecked(hash % 0x100A03u + c[4]) % 12u; uint h5 = unchecked(hash % 0xEB2EFu + c[5]) % 12u; uint h7 = unchecked(hash % 0x121E7Du + (c.Length > 7 ? c[7] : 0u)) % 12u; c[3] = unchecked(h0 + h1 + h2 + h4 + h5 + h2 * h5 + h0 * h1 + h7 * (h4 + 1u)) % 12u + LowestTaperId; c[6] = unchecked(h0 + h1 + h2 + h4 + hash % 0x65039u % 12u + h7 * (h4 * (h0 * h1 * h2 * h5 + 7u) + 1u) + h5 + 5u * h0 * h1 + 11u * h2 * h5) % 12u + LowestTaperId; return c; } }