-- Predicts healing based on previously seen healing. -- This library currently is not independent and relies on some Puppeteer code. if not PTUtil.IsSuperWowPresent() then return end PTHealPredict = {} PTUtil.SetEnvironment(PTHealPredict) local _G = getfenv(0) local compost = PTPool RelevantGUIDs = {} -- A set of GUIDs to listen to IncomingHeals = {} -- Key: Receiver | Value: List of incoming casts IncomingHots = {} -- Key: Receiver | Value: {HoT Name: {"caster", "id", "heal"}} Casts = {} -- Key: Caster | Value: {"targets", "spellID", "startTime"} LastCastedSpells = {} -- A cache of expected heal values for spells. These values are saved. -- Key: Spell ID | Value: Typical heal value for the spell ID HealCache = {} -- Since every healer heals for different values based on talents and gear, -- everyone's heal values are cached -- Key: Name | Value: Array: {Spell ID: Heal value} PlayerHealCache = {} ResurrectionTargets = {} -- Key: Receiver | Value: {Caster: {"startTime", "castTime"}} -- An array of functions that listen to changes to incoming healing Listeners = {} local print local colorize = PTUtil.Colorize -- Starts the cleanup check that expires stale casts, HoTs, resurrections and cast icons. It stops itself -- once nothing is left to expire, so it doesn't run every frame while nobody casts. Assigned further down. local WakeCleanup -- Debug messages are only built when debug output is enabled, since building them creates garbage local function debugEnabled() return PTOptions and PTOptions["Debug"] end local PRAYER_OF_HEALING_IDS = PTUtil.ToSet({596, 996, 10960, 10961, 25316}) local ResurrectionSpells = PTUtil.ToSet({ "Resurrection", "Revive Champion", "Redemption", "Ancestral Spirit", "Rebirth" }) PTLocale.Keys(ResurrectionSpells) local TRACKED_HOTS = PTUtil.ToSet({ "Rejuvenation", "Regrowth", -- Druid "Renew", "Greater Heal", -- Priest "Mend Pet", -- Hunter "First Aid" -- Generic }) PTLocale.Keys(TRACKED_HOTS) function OnLoad() print = Puppeteer.print if not PTHealCache then setglobal("PTHealCache", {}) end HealCache = PTHealCache local hardcodedHots = { -- Bandages [746] = 11, -- Linen [1159] = 19, -- Heavy Linen [3267] = 23, -- Wool [3268] = 43, -- Heavy Wool [7926] = 50, -- Silk [7927] = 80, -- Heavy Silk [10838] = 100, -- Mageweave [10839] = 138, -- Heavy Mageweave [18608] = 170, -- Runecloth [18610] = 250, -- Heavy Runecloth -- Mend Pet [136] = 20, [3111] = 38, [3661] = 68, [3662] = 103, [13542] = 142, [13543] = 189, [13544] = 245 } for k, v in pairs(hardcodedHots) do HealCache[k.."-HoT"] = v end if not PTPlayerHealCache then setglobal("PTPlayerHealCache", {}) end if not PTPlayerHealCache[GetRealmName()] then PTPlayerHealCache[GetRealmName()] = {} end PlayerHealCache = PTPlayerHealCache[GetRealmName()] end -- Get the expected heal of a player's spell function GetExpectedHeal(playerName, spellID) local playerCache = PlayerHealCache[playerName] if not playerCache or not playerCache[spellID] then return GetGenericExpectedHeal(spellID) end return playerCache[spellID] end function GetGenericExpectedHeal(spellID) return HealCache[spellID] or 0 end -- Returns all incoming healing and incoming direct healing function GetIncomingHealing(guid) local healing = 0 local casts = IncomingHeals[guid] if casts then for _, cast in pairs(casts) do healing = healing + cast["heal"] end end local directHealing = healing local hots = IncomingHots[guid] if hots then for _, hot in pairs(hots) do healing = healing + hot["heal"] end end return healing, directHealing end -- Returns non-HoT incoming healing function GetIncomingDirectHealing(guid) local healing = 0 local casts = IncomingHeals[guid] if casts then for _, cast in pairs(casts) do healing = healing + cast["heal"] end end return healing end -- To mimick HealComm, but this currently only accepts GUIDs function getHeal(guid) return GetIncomingHealing(guid) end function IsBeingResurrected(guid) return ResurrectionTargets[guid] ~= nil end function GetResurrectionCount(guid) local resses = ResurrectionTargets[guid] if not resses then return 0 end local count = 0 for _ in pairs(resses) do count = count + 1 end return count end -- Used for Prayer of Healing to add incoming healing to multiple players function AddIncomingMultiCast(targets, caster, spellID, healAmount, castTime) Casts[caster] = compost:AcquireHash( "targets", targets, "spellID", spellID, "startTime", GetTime() ) for _, target in ipairs(targets) do AddIncomingCast(target, caster, spellID, healAmount, castTime, true) end end local castIcons = {} function RemoveAllCastIcons() for caster, icons in pairs(castIcons) do for _, icon in ipairs(castIcons[caster]) do icon:End(false) end compost:Reclaim(icons) castIcons[caster] = nil end end function AddIncomingCast(target, caster, spellID, healAmount, castTime, multi) if not multi then Casts[caster] = compost:AcquireHash( "targets", compost:Acquire(target), "spellID", spellID, "startTime", GetTime() ) end local targetTable = IncomingHeals[target] if not targetTable then targetTable = {} IncomingHeals[target] = targetTable end if targetTable[caster] then compost:Reclaim(targetTable[caster]) end targetTable[caster] = compost:AcquireHash( "spellID", spellID, "heal", healAmount, "castTime", castTime, "startTime", GetTime() ) WakeCleanup() UpdateTarget(target) end function RemoveIncomingCast(caster, successful) local cast = Casts[caster] if cast then for _, target in ipairs(cast["targets"]) do local incomingHeals = IncomingHeals[target] compost:Reclaim(incomingHeals[caster]) incomingHeals[caster] = nil UpdateTarget(target) end compost:Reclaim(cast["targets"]) compost:Reclaim(cast) Casts[caster] = nil end end function GetCurrentCast(caster) local cast = Casts[caster] if cast then return table.getn(cast["targets"]) > 0 and IncomingHeals[cast["targets"][1]][caster] or nil end end function AddHot(target, caster, spellID, spellName, healAmount) local hot = compost:AcquireHash( "caster", caster, "id", spellID, "heal", healAmount, "startTime", GetTime() ) local targetTable = IncomingHots[target] if not targetTable then targetTable = {} IncomingHots[target] = targetTable end if targetTable[spellName] then compost:Reclaim(targetTable[spellName]) end targetTable[spellName] = hot WakeCleanup() UpdateTarget(target) end function UpdateTarget(target) for _, listener in ipairs(Listeners) do listener(target, GetIncomingHealing(target)) end end local function trimDecimal(number, places) local factor = 10 ^ places return math.floor(number * factor) / factor end local GENERIC_CHANGE_FACTOR = 0.05 local PLAYER_CHANGE_FACTOR = 0.25 -- spellID and spellName are optional; when given (from the ClassicAPI combat log), a heal from another -- spell leaves the last cast in place instead of being learned as that cast's amount function UpdateCache(heal, name, spellID, spellName) name = name or UnitName("player") local lastCastedSpell = LastCastedSpells[name] if not lastCastedSpell then return end if spellID and lastCastedSpell["spellID"] ~= spellID and SpellInfo(lastCastedSpell["spellID"]) ~= spellName then return end LastCastedSpells[name] = nil local spellID = lastCastedSpell["spellID"] local castTarget = lastCastedSpell["target"] compost:Reclaim(lastCastedSpell) if not PRAYER_OF_HEALING_IDS[spellID] then if castTarget == "" then --print(colorize("Don't have a target of spell cast for "..name.."'s "..spellID, 1, 0, 0)) return end local cache = PTUnit.Get(castTarget) if not cache or cache == PTUnit then --print(colorize("Could not find "..name.."'s unit while updating cache!", 1, 0, 0)) return end if cache.HasHealingModifier then --print(colorize("Not updating cache for "..name.."'s "..spellID.." because of healing modifier", 0.5, 0.5, 0.5)) return end end -- Update the generic cache if not HealCache[spellID] then HealCache[spellID] = heal else local prevHeal = HealCache[spellID] local adjustedHeal = trimDecimal(prevHeal + ((heal - prevHeal) * GENERIC_CHANGE_FACTOR), 2) HealCache[spellID] = adjustedHeal if debugEnabled() then print(colorize("Generic "..spellID..": "..prevHeal.." -> "..adjustedHeal, 0, 0.8, 0.8)) end end if not PlayerHealCache[name] then PlayerHealCache[name] = {} end -- Update the player-specific cache local playerCache = PlayerHealCache[name] if not playerCache[spellID] then playerCache[spellID] = heal if debugEnabled() then print(colorize("Created cache for "..name.."'s "..spellID, 1, 0.5, 1)) end end local prevHeal = playerCache[spellID] local adjustedHeal = trimDecimal(prevHeal + ((heal - prevHeal) * PLAYER_CHANGE_FACTOR), 2) playerCache[spellID] = adjustedHeal playerCache["lastSeen"] = time() if debugEnabled() then print(colorize(name.."'s "..spellID..": "..prevHeal.." -> "..adjustedHeal, 0, 0.8, 0.2)) end end function UpdateCacheHot(spellName, heal, targetGuid, targetName, casterGuid, casterName) if not IncomingHots[targetGuid] then return end local hots = IncomingHots[targetGuid] if not hots[spellName] then return end heal = tonumber(heal) or 0 local hot = hots[spellName] if hot["heal"] ~= heal then local prevHeal = hot["heal"] hot["heal"] = heal UpdateTarget(targetGuid) if not PlayerHealCache[casterName] then PlayerHealCache[casterName] = {} end local spellID = hot["id"] local cache = PTUnit.Get(targetGuid) if not cache or cache == PTUnit then if debugEnabled() then print(colorize("Could not find "..targetName.."'s unit while updating cache!", 1, 0, 0)) end return end if cache.HasHealingModifier then if debugEnabled() then print(colorize("Not updating cache for "..casterName.."'s "..spellID.. " because of healing modifier", 0.5, 0.5, 0.5)) end return end -- Update the player-specific cache local playerCache = PlayerHealCache[casterName] spellID = spellID.."-HoT" if not playerCache[spellID] then playerCache[spellID] = heal if debugEnabled() then print(colorize("Created cache for "..casterName.."'s "..spellID.." ("..spellName..")", 1, 0.5, 1)) end end PlayerHealCache[casterName][spellID] = heal if debugEnabled() then print(colorize(casterName.."'s "..spellID.." ("..spellName..")"..": "..prevHeal.." -> "..heal, 0, 0.8, 0.2)) end end end function RemoveHoT(spellName, targetGuid) if not IncomingHots[targetGuid] then return end if not IncomingHots[targetGuid][spellName] then return end local hot = IncomingHots[targetGuid][spellName] -- A hack needed because overwritten HoTs cause the previous HoT to be removed, -- which happens after the UNIT_CASTEVENT if hot["startTime"] + 0.5 > GetTime() and not hot["swiftmend"] then return end compost:Reclaim(hot) IncomingHots[targetGuid][spellName] = nil UpdateTarget(targetGuid) end local roster = AceLibrary("RosterLib-2.0") local function getGuidFromLogName(name) local petName, owner = PTUtil.cmatch(name, "%s (%s)") local unit if owner then -- A pet is being healed local ownerUnit = roster:GetUnitIDFromName(owner) if not ownerUnit then return end unit = roster:GetPetFromOwner(ownerUnit) else unit = roster:GetUnitIDFromName(name) end if not unit then -- Check custom units for _, guid in pairs(PTUnitProxy.CustomUnitGUIDMap) do if UnitName(guid) == name then return guid end end end if unit then local _, guid = UnitExists(unit) return guid end end local function getSelfGuid() local _, guid = UnitExists("player") return guid end local autoShotName = PTLocale.Translate("Auto Shot") local eventFrame = CreateFrame("Frame", "PTHealPredictCasts") eventFrame:RegisterEvent("UNIT_CASTEVENT") eventFrame:SetScript("OnEvent", function() local caster, target, event, spellID, duration = arg1, arg2, arg3, arg4, arg5 if not RelevantGUIDs[caster] then return end if not UnitIsPlayer(caster) then return end local spellName = SpellInfo(spellID) if ResurrectionSpells[spellName] then if event == "START" then if target then if not ResurrectionTargets[target] then ResurrectionTargets[target] = compost:GetTable() end local resses = ResurrectionTargets[target] if resses[caster] then compost:Reclaim(resses[caster]) end resses[caster] = compost:AcquireHash("startTime", GetTime(), "castTime", duration) WakeCleanup() end elseif event == "CAST" or event == "FAIL" then local cast = Casts[caster] if cast then local target = cast["targets"][1] local resses = ResurrectionTargets[target] if resses and resses[caster] then compost:Reclaim(resses[caster]) resses[caster] = nil if not next(resses) then compost:Reclaim(resses) ResurrectionTargets[target] = nil end end end end UpdateTarget(target) end if event == "CAST" or event == "CHANNEL" then -- Swiftmend could cause Rejuvenation or Regrowth to end very quickly, -- so there needs to be a flag to allow the removal of the HoT if spellName == "Swiftmend" then if IncomingHots[target] then local hots = IncomingHots[target] if hots["Rejuvenation"] then hots["Rejuvenation"]["swiftmend"] = true end if hots["Regrowth"] then hots["Regrowth"]["swiftmend"] = true end end end if TRACKED_HOTS[spellName] then if spellName == "Mend Pet" then -- Mend pet doesn't "target" the pet, so we have to acquire the pet local units = PTGuidRoster.GetUnits(caster) if not units then return end local casterUnit = units[1] if not casterUnit then return end local petUnit = roster:GetPetFromOwner(casterUnit) if not petUnit then return end local _, guid = UnitExists(petUnit) target = guid end if spellName ~= "Greater Heal" or spellID == 22009 then AddHot(target, caster, spellID, spellName, GetExpectedHeal(UnitName(caster), spellID.."-HoT")) end end end -- Check started cast spell ID to prevent instant mid-cast spells from removing incoming healing local currentCast = GetCurrentCast(caster) if event == "CAST" and currentCast and currentCast["spellID"] == spellID then RemoveIncomingCast(caster, true) local casterName = UnitName(caster) local last = LastCastedSpells[casterName] if not last then last = compost:GetTable() LastCastedSpells[casterName] = last end last["unit"] = caster last["target"] = target last["spellID"] = spellID return end if event == "START" or event == "FAIL" then if spellName ~= autoShotName then -- Don't remove the cast when Auto Shot fails RemoveIncomingCast(caster, event == "START") end end if event == "START" and target then if UnitCanAssist(caster, target ~= "" and target or caster) and duration > 0 then if PRAYER_OF_HEALING_IDS[spellID] then local inRange = PTUtil.GetSurroundingPartyMembers(target ~= "" and target or caster, 28) local casterName = UnitName(caster) local expectedHeal = GetExpectedHeal(casterName, spellID) AddIncomingMultiCast(inRange, caster, spellID, expectedHeal, duration) elseif target ~= "" then local casterName = UnitName(caster) local expectedHeal = GetExpectedHeal(casterName, spellID) AddIncomingCast(target, caster, spellID, expectedHeal, duration) end end end end) -- UNIT_CASTEVENT does not fire "FAIL" events on plain 1.12 servers, need to fallback on Nampower local f = CreateFrame("Frame", "PTTempHealPredictFix") f:RegisterEvent("SPELL_FAILED_SELF") f:RegisterEvent("SPELL_FAILED_OTHER") f:SetScript("OnEvent", function() local caster = event == "SPELL_FAILED_SELF" and getSelfGuid() or arg1 local spellID = event == "SPELL_FAILED_SELF" and arg1 or arg2 local spellName = SpellInfo(spellID) if ResurrectionSpells[spellName] then local cast = Casts[caster] if cast then local target = cast["targets"][1] local resses = ResurrectionTargets[target] if resses and resses[caster] then compost:Reclaim(resses[caster]) resses[caster] = nil if not next(resses) then compost:Reclaim(resses) ResurrectionTargets[target] = nil end end end end if spellName ~= autoShotName then -- Don't remove the cast when Auto Shot fails RemoveIncomingCast(caster, false) if castIcons[caster] then for _, icon in ipairs(castIcons[caster]) do icon:End(false) end compost:Reclaim(castIcons[caster]) castIcons[caster] = nil end end end) local trackedHostileSpells = PTUtil.ToSet({"Shackle Undead", "Mind Control", "Fear", "Polymorph", "Polymorph: Turtle", "Polymorph: Cow"}) local castIconFrame = CreateFrame("Frame", "PTCastIcons") castIconFrame:RegisterEvent("UNIT_CASTEVENT") castIconFrame:SetScript("OnEvent", function() local caster, target, event, spellID, duration = arg1, arg2, arg3, arg4, arg5 local spellName = SpellInfo(spellID) if event == "CAST" then if castIcons[caster] and castIcons[caster][1].spellName == spellName then for _, icon in ipairs(castIcons[caster]) do icon:End(true) end compost:Reclaim(castIcons[caster]) castIcons[caster] = nil end end if event == "START" or event == "FAIL" then if spellName ~= autoShotName then -- Don't remove the cast when Auto Shot fails if castIcons[caster] then for _, icon in ipairs(castIcons[caster]) do icon:End(false) end compost:Reclaim(castIcons[caster]) castIcons[caster] = nil end end end if event == "START" then if not PuppeteerSettings.IsExperimentEnabled("CastIcons") or (not UnitCanAssist(caster, target) and not trackedHostileSpells[spellName] and not PRAYER_OF_HEALING_IDS[spellID]) then return end local inRange if PRAYER_OF_HEALING_IDS[spellID] then inRange = PTUtil.GetSurroundingPartyMembers(target ~= "" and target or caster, 28) end inRange = inRange or compost:Acquire(target) for _, target in ipairs(inRange) do local spellName, _, tex = SpellInfo(spellID) local targetFrame for f in Puppeteer.UnitFrames(target) do if f.owningGroup:GetContainer():IsShown() then targetFrame = f break end end if targetFrame then local icon = PTGuiLib.Get("puppeteer_cast_icon") local healAmount = UnitCanAssist(caster, target) and GetExpectedHeal(UnitName(caster), spellID) or 0 icon:Start(spellName, tex, duration / 1000, caster, healAmount, targetFrame) if not castIcons[caster] then castIcons[caster] = compost:GetTable() end table.insert(castIcons[caster], icon) WakeCleanup() end end compost:Reclaim(inRange) end end) -- Because the prediction code is not currently bullet-proof to infinite incoming heals, we're checking once a while for old casts local GARBAGE_CHECK_INTERVAL = 10 local nextGarbageCheck = GetTime() + GARBAGE_CHECK_INTERVAL -- True while any cast, HoT, resurrection or cast icon could still need expiring local function HasPendingEntries() for _, casts in pairs(IncomingHeals) do if next(casts) then return true end end for _, hots in pairs(IncomingHots) do if next(hots) then return true end end return next(ResurrectionTargets) ~= nil or next(castIcons) ~= nil end local function Cleanup_OnUpdate() if GetTime() > nextGarbageCheck then local time = GetTime() nextGarbageCheck = time + GARBAGE_CHECK_INTERVAL for receiver, casts in pairs(IncomingHeals) do for caster, cast in pairs(casts) do if cast["startTime"] + 15 < time then if debugEnabled() then print(colorize("Removed "..caster.."'s heal on "..receiver.." for taking too long", 1, 0, 0)) end compost:Reclaim(cast) casts[caster] = nil UpdateTarget(receiver) end end end for receiver, hots in pairs(IncomingHots) do for name, hot in pairs(hots) do if hot["startTime"] + 25 < time then if debugEnabled() then print(colorize("Removed "..hot["caster"].."'s "..name.." (HoT) on ".. receiver.." for taking too long", 1, 0, 0)) end compost:Reclaim(hot) hots[name] = nil UpdateTarget(receiver) end end end -- Only the expired caster is dropped; other resurrections on the same -- target stay until they finish or expire themselves. for target, resses in pairs(ResurrectionTargets) do local removed = false for caster, res in pairs(resses) do if res["startTime"] + 20 < time then if debugEnabled() then print(colorize("Removed "..caster.."'s resurrection on ".. target.." for taking too long", 1, 0, 0)) end compost:Reclaim(res) resses[caster] = nil removed = true end end if removed then if not next(resses) then compost:Reclaim(resses) ResurrectionTargets[target] = nil end UpdateTarget(target) end end for caster, icons in pairs(castIcons) do local icon = icons[1] if icon:GetOvertime() > 10 then for _, icon in ipairs(castIcons[caster]) do icon:End(false) end compost:Reclaim(castIcons[caster]) castIcons[caster] = nil end end if not HasPendingEntries() then eventFrame:SetScript("OnUpdate", nil) end end end WakeCleanup = function() if not eventFrame:GetScript("OnUpdate") then nextGarbageCheck = GetTime() + GARBAGE_CHECK_INTERVAL eventFrame:SetScript("OnUpdate", Cleanup_OnUpdate) end end local cmatch = PTUtil.cmatch local combatLogFrame = CreateFrame("Frame", "PTHealPredictCombatLog") combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF") combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF") combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF") -- Needed to see casts coming from other players to yourself combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PARTY_BUFF") combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PET_BUFF") combatLogFrame:SetScript("OnEvent", function() if string.find(arg1, "critically") then return end if string.find(arg1, "Bonus Healing") then return end local spell, targetName, heal = cmatch(arg1, HEALEDSELFOTHER) -- "Your %s heals %s for %d." if spell and targetName and heal then UpdateCache(tonumber(heal)) return end local spell, heal = cmatch(arg1, HEALEDSELFSELF) -- "Your %s heals you for %d." if spell and heal then UpdateCache(tonumber(heal)) return end local name, spell, heal = cmatch(arg1, HEALEDOTHERSELF) -- "%s's %s heals you for %d." if name and spell and heal then UpdateCache(tonumber(heal), name) return end local name, spell, targetName, heal = cmatch(arg1, HEALEDOTHEROTHER) -- "%s's %s heals %s for %d." if name and spell and targetName and heal then UpdateCache(tonumber(heal), name) return end end) local periodicCombatLogFrame = CreateFrame("Frame", "PTHealPredictPerCombatLog") periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS") periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS") periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS") periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS") periodicCombatLogFrame:SetScript("OnEvent", function() local heal, spell = cmatch(arg1, PERIODICAURAHEALSELFSELF) -- "You gain %d health from %s." if heal and spell then local selfName = UnitName("player") local selfGuid = getSelfGuid() UpdateCacheHot(spell, heal, selfGuid, selfName, selfGuid, selfName) return end local name, heal, spell = cmatch(arg1, PERIODICAURAHEALSELFOTHER) -- "%s gains %d health from your %s." if name and heal and spell then local casterGuid = getSelfGuid() local targetGuid = getGuidFromLogName(name) UpdateCacheHot(spell, heal, targetGuid, name, casterGuid, UnitName(casterGuid)) return end local heal, name, spell = cmatch(arg1, PERIODICAURAHEALOTHERSELF) -- "You gain %d health from %s's %s." if heal and name and spell then local casterGuid = getGuidFromLogName(name) local targetGuid = getSelfGuid() UpdateCacheHot(spell, heal, targetGuid, UnitName("player"), casterGuid, name) return end local targetName, heal, name, spell = cmatch(arg1, PERIODICAURAHEALOTHEROTHER) -- "%s gains %d health from %s's %s." if targetName and heal and name and spell then local casterGuid = getGuidFromLogName(name) local targetGuid = getGuidFromLogName(targetName) UpdateCacheHot(spell, heal, targetGuid, targetName, casterGuid, name) return end end) local auraCombatLogFrame = CreateFrame("Frame", "PTHealPredictAuraCombatLog") auraCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER") auraCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_PARTY") auraCombatLogFrame:SetScript("OnEvent", function() local spell, name = cmatch(arg1, AURAREMOVEDOTHER) -- "%s fades from %s." if spell and name then local guid = getGuidFromLogName(name) if not guid then return end RemoveHoT(spell, guid) return end end) local selfAuraCombatLogFrame = CreateFrame("Frame", "PTHealPredictSelfAuraCombatLog") selfAuraCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_SELF") selfAuraCombatLogFrame:SetScript("OnEvent", function() local spell = cmatch(arg1, AURAREMOVEDSELF) -- "%s fades from you." if spell then RemoveHoT(spell, getSelfGuid()) return end end) -- ClassicAPI's COMBAT_LOG_EVENT_UNFILTERED replaces the chat combat log parsing above: it works in every -- client language, carries GUIDs and a crit flag, and needs no string matching. It is switched on at the -- first PLAYER_ENTERING_WORLD, once ClassicAPI's GUIDs are confirmed to match SuperWoW's. local classicCombatLogFrame = CreateFrame("Frame", "PTHealPredictClassicCombatLog") local function ClassicCombatLog_OnEvent() local _, subevent, _, sourceGUID, sourceName, _, _, destGUID, destName, _, _, spellID, spellName, _, amount, _, _, critical = CombatLogGetCurrentEventInfo() if subevent == "SPELL_HEAL" then -- Crits would skew the learned amounts, as in the chat parsing if critical or not amount or not sourceName then return end UpdateCache(amount, sourceName, spellID, spellName) elseif subevent == "SPELL_PERIODIC_HEAL" then if not amount or not spellName or not destGUID or not sourceGUID then return end UpdateCacheHot(spellName, amount, destGUID, destName, sourceGUID, sourceName) elseif subevent == "SPELL_AURA_REMOVED" then if spellName and destGUID then RemoveHoT(spellName, destGUID) end end end classicCombatLogFrame:SetScript("OnEvent", function() if event == "PLAYER_ENTERING_WORLD" then classicCombatLogFrame:UnregisterEvent("PLAYER_ENTERING_WORLD") local _, superwowGuid = UnitExists("player") if not superwowGuid or superwowGuid ~= UnitGUID("player") then return -- Different GUID formats; keep the chat parsing end combatLogFrame:UnregisterAllEvents() periodicCombatLogFrame:UnregisterAllEvents() auraCombatLogFrame:UnregisterAllEvents() selfAuraCombatLogFrame:UnregisterAllEvents() classicCombatLogFrame:SetScript("OnEvent", ClassicCombatLog_OnEvent) classicCombatLogFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") end end) function EnableClassicCombatLog() if not PTUtil.IsClassicAPIActive("CombatLog") or not UnitGUID then return end classicCombatLogFrame:RegisterEvent("PLAYER_ENTERING_WORLD") end -- Set the GUIDs to listen to function SetRelevantGUIDs(guidArray) local relevant = RelevantGUIDs for guid in pairs(relevant) do relevant[guid] = nil end for _, guid in ipairs(guidArray) do relevant[guid] = 1 end end -- Same as SetRelevantGUIDs, but takes a map with GUIDs as keys (like the GUID roster) to avoid building an array function SetRelevantGUIDsFromMap(guidMap) local relevant = RelevantGUIDs for guid in pairs(relevant) do relevant[guid] = nil end for guid in pairs(guidMap) do relevant[guid] = 1 end end -- Provided listener function will receive the arguments: Updated GUID, Updated Incoming Healing function HookUpdates(listener) table.insert(Listeners, listener) end