From deeec89955134db978555d7e6ccbd21be02af14c Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:22:03 -0500 Subject: [PATCH] predict + libdebuff + swingtimer: drop hardcoded spell data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace per-locale name tables and per-rank ID lists with single canonical-rank lookups through C_Spell. Spell.dbc bits hoisted to named constants at module top. libpredict: - Four 7-locale tables (PRAYER_OF_HEALING / REJUVENATION / RENEW / REGROWTH) collapsed to one C_Spell.GetSpellName(rank1id) call each. - 25-entry SPELL_IDS (all ranks of Rejuv + Renew) for SPELL_GO_SELF HoT detection replaced by name comparison against REJUVENATION / RENEW. No per-rank ID maintenance. libdebuff: - GetSpellRecField(id, "name") → C_Spell.GetSpellName(id) at all call sites; the presence-guard pattern is gone (ClassicAPI is a hard dep, per memory). - GetSpellRecField(id, "rank") → C_Spell.GetSpellSubtext(id). swingtimer: - Hoist FLAG_AUTOATTACK / ATTR_KEEP_SWINGS / ATTR_ON_NEXT_SWING to module-top constants via tonumber("0xNN", 16) so the SPELL_GO_SELF hot path stops re-parsing them on every call. Lua 5.0 has no hex number literals; strtoul-backed tonumber handles the "0x" prefix. --- libs/libdebuff.lua | 26 +++++------ libs/libpredict.lua | 102 +++++++++-------------------------------- modules/swingtimer.lua | 22 +++++---- 3 files changed, 46 insertions(+), 104 deletions(-) diff --git a/libs/libdebuff.lua b/libs/libdebuff.lua index f4e51009..bcafd453 100644 --- a/libs/libdebuff.lua +++ b/libs/libdebuff.lua @@ -427,7 +427,7 @@ local function GetDebuffSlotMap(guid) local spellId = auras[auraSlot] if spellId and spellId > 0 then displaySlot = displaySlot + 1 - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) local texture = libdebuff:GetSpellIcon(spellId) local stacks = (auraApps and auraApps[auraSlot] or 0) + 1 local dtype = nil @@ -1024,7 +1024,7 @@ if hasNampower then local targetGuid = arg4 local numHit = arg6 or 0 local numMissed = arg7 or 0 - + -- Fire registered SPELL_GO_SELF hooks BEFORE miss guard -- (Swingtimer needs to see ALL casts, even misses, for swing reset) if event == "SPELL_GO_SELF" and pfUI.libdebuff_spell_go_hooks then @@ -1035,15 +1035,15 @@ if hasNampower then if numMissed > 0 or numHit == 0 then return end - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") - local spellRankString = GetSpellRecField and GetSpellRecField(spellId, "rank") + local spellName = C_Spell.GetSpellName(spellId) if not spellName then return end - + local spellRankString = C_Spell.GetSpellSubtext(spellId) + local castRank = 0 if spellRankString and spellRankString ~= "" then castRank = tonumber((string.gsub(spellRankString, "Rank ", ""))) or 0 end - + -- Store in pendingCasts for DEBUFF_ADDED correlation. -- If this cast is a downrank of an already active debuff, fire the downrank blocked hook -- so external addons (e.g. SuperCleveRoidMacros) don't need to re-implement this check. @@ -1209,7 +1209,7 @@ if hasNampower then if not spellId then return end if not targetGuid or targetGuid == "" or targetGuid == "0x0000000000000000" then return end - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) if not spellName then return end -- Deduplicate: Ignore if we processed this exact cast recently (within 100ms) @@ -1469,7 +1469,7 @@ if hasNampower then -- Invalidate slot map cache for this GUID - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) if not spellName then return end if debugStats.enabled then @@ -1599,9 +1599,9 @@ if hasNampower then local auraSlot = auraSlot_0based and (auraSlot_0based + 1) or nil -- Invalidate slot map cache for this GUID - - local spellName = (GetSpellRecField and GetSpellRecField(spellId, "name")) or "?" - + + local spellName = C_Spell.GetSpellName(spellId) or "?" + if debugStats.enabled then debugStats.debuff_removed = debugStats.debuff_removed + 1 if IsCurrentTarget(guid) then @@ -1609,13 +1609,13 @@ if hasNampower then GetDebugTimestamp(), displaySlot, auraSlot or -1, auraSlot_0based or -1, spellName)) end end - + -- If unit is dead, cleanup all if UnitIsDead and UnitIsDead(guid) then CleanupUnit(guid) return end - + -- Get auraSlot from event parameter (Nampower 2.29+) -- Fallback to displayToAura mapping if not available local foundAuraSlot = auraSlot diff --git a/libs/libpredict.lua b/libs/libpredict.lua index 40ecc904..24f43d74 100644 --- a/libs/libpredict.lua +++ b/libs/libpredict.lua @@ -44,78 +44,13 @@ local healGuidToName = {} -- [casterGuid] = casterName, for SPELL_FAILED_OTHER c local ress_timers = {} -- [target][sender] = expiry_timestamp (60s rez window) local RESS_TIMEOUT = 60 -- Vanilla: rez offer expires after 60s -local PRAYER_OF_HEALING -do -- Prayer of Healing - local locales = { - ["deDE"] = "Gebet der Heilung", - ["enUS"] = "Prayer of Healing", - ["esES"] = "Rezo de curación", - ["frFR"] = "Prière de soins", - ["koKR"] = "치유의 기원", - ["ruRU"] = "Молитва исцеления", - ["zhCN"] = "治疗祷言", - } - - PRAYER_OF_HEALING = locales[GetLocale()] or locales["enUS"] -end - -local REJUVENATION -do -- Rejuvenation - local locales = { - ["deDE"] = "Verjüngung", - ["enUS"] = "Rejuvenation", - ["esES"] = "Rejuvenecimiento", - ["frFR"] = "Récupération", - ["koKR"] = "회복", - ["ruRU"] = "Омоложение", - ["zhCN"] = "回春术", - } - - REJUVENATION = locales[GetLocale()] or locales["enUS"] -end - -local RENEW -do -- Renew - local locales = { - ["deDE"] = "Erneuerung", - ["enUS"] = "Renew", - ["esES"] = "Renovar", - ["frFR"] = "Rénovation", - ["koKR"] = "소생", - ["ruRU"] = "Обновление", - ["zhCN"] = "恢复", - } - - RENEW = locales[GetLocale()] or locales["enUS"] -end - -local REGROWTH -do -- Regrowth - local locales = { - ["deDE"] = "Nachwachsen", - ["enUS"] = "Regrowth", - ["esES"] = "Recrecimiento", - ["frFR"] = "Rétablissement", - ["koKR"] = "재생", - ["ruRU"] = "Восстановление", - ["zhCN"] = "愈合", - } - - REGROWTH = locales[GetLocale()] or locales["enUS"] -end - - --- Spell IDs for SPELL_GO_SELF callback (Nampower) - Instant HoT detection -local SPELL_IDS = { - -- Rejuvenation (all ranks) - [774] = "Reju", [1058] = "Reju", [1430] = "Reju", [2090] = "Reju", [2091] = "Reju", - [3627] = "Reju", [8910] = "Reju", [9839] = "Reju", [9840] = "Reju", [9841] = "Reju", - [25299] = "Reju", [26981] = "Reju", [26982] = "Reju", - -- Renew (all ranks) - [139] = "Renew", [6074] = "Renew", [6075] = "Renew", [6076] = "Renew", [6077] = "Renew", - [6078] = "Renew", [10927] = "Renew", [10928] = "Renew", [10929] = "Renew", [25315] = "Renew", - [25221] = "Renew", [25222] = "Renew", -} +-- Localized spell names resolved once from canonical rank-1 spellIDs. +-- Every rank shares the same name, so per-rank comparisons elsewhere can +-- be done against these constants without per-locale or per-rank tables. +local PRAYER_OF_HEALING = C_Spell.GetSpellName(596) -- Prayer of Healing (Rank 1) +local REJUVENATION = C_Spell.GetSpellName(774) -- Rejuvenation (Rank 1) +local RENEW = C_Spell.GetSpellName(139) -- Renew (Rank 1) +local REGROWTH = C_Spell.GetSpellName(8936) -- Regrowth (Rank 1) local libpredict = CreateFrame("Frame") libpredict:RegisterEvent("UNIT_HEALTH") @@ -173,7 +108,7 @@ end local function isRezSpell(spellId) if not L["resurrections"] then return false end - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) return spellName and L["resurrections"][spellName] end @@ -192,7 +127,7 @@ end) -- SPELL_START_SELF: own cast started (heals + rez) pfUI.libdebuff_spell_start_self_hooks = pfUI.libdebuff_spell_start_self_hooks or {} pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGuid, targetGuid, castTime) - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) if not spellName then return end local pendingTarget = nil @@ -319,7 +254,7 @@ end -- SPELL_GO_SELF: own cast landed (HealStop + Regrowth timer) pfUI.libdebuff_spell_go_hooks["libpredict_sender"] = function(spellId) libpredict:HealStop(player) - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) if spellName == REGROWTH then local now = pfUI.uf.now or GetTime() if libpredict.sender.regrowth_timer then @@ -337,7 +272,7 @@ end -- Signature: fn(spellId, casterGuid, targetGuid, castTime) pfUI.libdebuff_spell_start_other_hooks = pfUI.libdebuff_spell_start_other_hooks or {} pfUI.libdebuff_spell_start_other_hooks["libpredict"] = function(spellId, casterGuid, targetGuid, castTime) - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) if not spellName then return end local casterName = resolveNameFromGuid(casterGuid) @@ -404,8 +339,13 @@ end -- Signature: fn(spellId, arg1, arg2, arg3, arg4, arg5, arg6, arg7) pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {} pfUI.libdebuff_spell_go_hooks["libpredict"] = function(spellId, a1, a2, a3, a4, a5, a6, a7) - -- Instant HoTs - local hotType = SPELL_IDS[spellId] + -- Instant HoTs — classify by name (rank-independent) instead of a + -- hardcoded per-rank ID table. + local spellName = C_Spell.GetSpellName(spellId) + local hotType + if spellName == REJUVENATION then hotType = "Reju" + elseif spellName == RENEW then hotType = "Renew" + end if hotType then local targetGuid = a4 local targetName = resolveNameFromGuid(targetGuid) @@ -1236,7 +1176,7 @@ libpredict.sender:SetScript("OnEvent", function() local amount = arg4 local isCrit = arg5 == 1 local isPeriodic = arg6 == 1 - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) if spellName and spell_queue[1] == spellName then UpdateCache(spell_queue[2], amount, isCrit) end @@ -1256,9 +1196,9 @@ libpredict.sender:SetScript("OnEvent", function() local casterName = resolveNameFromGuid(casterGuid) if not casterName or casterName == player then return end -- own heals handled by SPELL_HEAL_BY_SELF - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + local spellName = C_Spell.GetSpellName(spellId) if not spellName then return end - local rankStr = GetSpellRecField and GetSpellRecField(spellId, "rank") or "" + local rankStr = C_Spell.GetSpellSubtext(spellId) local spellKey = spellName .. (rankStr or "") foreignCache[casterName] = foreignCache[casterName] or {} diff --git a/modules/swingtimer.lua b/modules/swingtimer.lua index 5d0e40aa..f27eb8e2 100644 --- a/modules/swingtimer.lua +++ b/modules/swingtimer.lua @@ -10,6 +10,11 @@ pfUI:RegisterModule("swingtimer", function () local ON_SWING_QUEUED = 0 local ON_SWING_QUEUE_POPPED = 1 + -- Spell.dbc bits used to mirror server-side swing-reset rules. + local FLAG_AUTOATTACK = tonumber("0x08", 16) -- SPELL_INTERRUPT_FLAG_AUTOATTACK + local ATTR_KEEP_SWINGS = tonumber("0x20000", 16) -- SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS + local ATTR_ON_NEXT_SWING = tonumber("0x04", 16) -- SPELL_ATTR_ON_NEXT_SWING + -- Consolidate state into a table to avoid Lua 5.0 upvalue limit (32 max) local S = { mhTimer = 0, mhTimerMax = 1, @@ -39,9 +44,8 @@ pfUI:RegisterModule("swingtimer", function () local WAND_SHOOT_SPELLID = 5019 local THROW_SPELLID = 2764 -- one-shot ranged, not auto-repeat - -- SPELL_ATTR_ON_NEXT_SWING (bit 2, value 4): spell replaces next auto-attack swing. + -- ATTR_ON_NEXT_SWING: spell replaces next auto-attack swing. -- Covers Raptor Strike, Maul, Mongoose Bite, Holy Strike, etc. automatically. - local ATTR_ON_NEXT_SWING = 4 local function IsOnSwingSpell(spellId) if S.onSwingCache[spellId] ~= nil then return S.onSwingCache[spellId] end local attr = GetSpellRecField(spellId, "attributes") or 0 @@ -702,12 +706,12 @@ pfUI:RegisterModule("swingtimer", function () -- Freeze the swing timer for cast-time spells that DON'T reset auto- -- attack on completion (Slam, Hammer of Wrath on Turtle, etc.) — those -- let the swing resume from where it paused. Detect dynamically via the - -- absent AUTOATTACK interrupt flag (0x08); spells with 0x08 reset on + -- absent AUTOATTACK interrupt flag (8); spells with that bit reset on -- SPELL_GO_SELF so freezing isn't necessary. Subsumes the old hardcoded -- swingDelaySpells list (no list maintenance for new Slam-style spells). if S.mhActive then local iflags = GetSpellRecField(arg1, "interruptFlags") or 0 - if bit.band(iflags, 0x08) == 0 then + if bit.band(iflags, FLAG_AUTOATTACK) == 0 then S.mhFrozenAt = GetTime() end end @@ -747,13 +751,11 @@ pfUI:RegisterModule("swingtimer", function () -- Mirror the server rule for "does this spell reset the auto-attack -- swing" (Spell::IsMeleeAttackResetSpell in Turtle's core): -- InterruptFlags has SPELL_INTERRUPT_FLAG_AUTOATTACK (0x08) - -- AND AttributesEx2 doesn't have NOT_RESET_AUTO_ACTIONS (0x20000). + -- AND AttributesEx2 lacks NOT_RESET_AUTO_ACTIONS (0x20000). -- If neither path resets and we're holding a frozen-swing-during-cast - -- (mhFrozenAt set by SPELL_START_SELF for non-0x08 spells), this is a - -- Slam-style cast — push the timer forward by the cast duration so - -- the bar resumes from where it paused. - local FLAG_AUTOATTACK = 0x08 -- SPELL_INTERRUPT_FLAG_AUTOATTACK - local ATTR_KEEP_SWINGS = 0x20000 -- SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS + -- (mhFrozenAt set by SPELL_START_SELF for non-AUTOATTACK spells), this + -- is a Slam-style cast — push the timer forward by the cast duration + -- so the bar resumes from where it paused. local iflags = GetSpellRecField(spellId, "interruptFlags") or 0 if bit.band(iflags, FLAG_AUTOATTACK) ~= 0 and bit.band(GetSpellRecField(spellId, "attributesEx2") or 0, ATTR_KEEP_SWINGS) == 0 then