From 80f5b9ef0d3f6b2489bcc6c08dad92ee4eef5ced Mon Sep 17 00:00:00 2001 From: shagu Date: Fri, 27 Nov 2020 15:22:01 +0100 Subject: [PATCH] libpredict: add library to handle heal predictions * move "prediction" from module into a library * reduce the resize and trigger calls on unitframes * improve performance by caching frames and events * introduce later API commands such as: - UnitGetIncomingHeals(unit) - UnitHasIncomingResurrection(unit) * update unitframes to make use of new api calls * delay spell cache loading to not overwrite previous * remove all unitframe specific code from library --- api/unitframes.lua | 61 ++++- init/libs.xml | 1 + init/modules.xml | 1 - libs/libcast.lua | 1 - libs/libpredict.lua | 476 +++++++++++++++++++++++++++++++++++++ modules/prediction.lua | 523 ----------------------------------------- 6 files changed, 532 insertions(+), 531 deletions(-) create mode 100644 libs/libpredict.lua delete mode 100644 modules/prediction.lua diff --git a/api/unitframes.lua b/api/unitframes.lua index 6786b1cf..02289e91 100644 --- a/api/unitframes.lua +++ b/api/unitframes.lua @@ -556,12 +556,6 @@ function pfUI.uf:UpdateConfig() f.incHeal.texture:SetVertexColor(0, 1, 0, 0.5) f.incHeal:Hide() - f.incHeal:SetScript("OnShow", function() - if pfUI.prediction and f.label and f.id then - pfUI.prediction:TriggerUpdate(UnitName(f.label .. f.id)) - end - end) - if f.config.verticalbar == "0" then f.incHeal:ClearAllPoints() f.incHeal:SetPoint("TOPLEFT", f.hp.bar, "TOPLEFT", 0, 0) @@ -864,6 +858,61 @@ function pfUI.uf.OnUpdate() this.portrait.model.update = nil end + -- get incoming heals and resurections + if libpredict then + local unit = this.label .. this.id + local heal = libpredict:UnitGetIncomingHeals(unit) + local ress = libpredict:UnitHasIncomingResurrection(unit) + + if heal ~= this.lastheal then + local overhealperc = tonumber(this.config.overhealperc) + local health, maxHealth = UnitHealth(unit), UnitHealthMax(unit) + this.lastheal = heal + + if heal > 0 and (health < maxHealth or overhealperc > 0 ) then + local width = this.config.width + local height = this.config.height + + if this.config.verticalbar == "0" then + local healthWidth = width * (health / maxHealth) + local incWidth = width * heal / maxHealth + if healthWidth + incWidth > width * (1+(overhealperc/100)) then + incWidth = width * (1+overhealperc/100) - healthWidth + end + + if this.config.invert_healthbar == "1" then + this.incHeal:SetWidth(incWidth) + else + this.incHeal:SetWidth(incWidth + healthWidth) + end + else + local healthHeight = height * (health / maxHealth) + local incHeight = height * heal / maxHealth + if healthHeight + incHeight > height * (1+(overhealperc/100)) then + incHeight = height * (1+overhealperc/100) - healthHeight + end + + if this.config.invert_healthbar == "1" then + this.incHeal:SetHeight(incHeight) + else + this.incHeal:SetHeight(incHeight + healthHeight) + end + end + + this.incHeal:Show() + else + this.incHeal:Hide() + end + end + + -- update ressurections + if ress and UnitIsDeadOrGhost(unit) then + this.ressIcon:Show() + else + this.ressIcon:Hide() + end + end + -- trigger eventless actions (online/offline/range) if not this.lastTick then this.lastTick = GetTime() + (this.tick or .2) end if this.lastTick and this.lastTick < GetTime() then diff --git a/init/libs.xml b/init/libs.xml index 99b31c29..01d76ce1 100644 --- a/init/libs.xml +++ b/init/libs.xml @@ -8,4 +8,5 @@ + diff --git a/init/modules.xml b/init/modules.xml index f62af00a..a2711765 100644 --- a/init/modules.xml +++ b/init/modules.xml @@ -12,7 +12,6 @@ - diff --git a/libs/libcast.lua b/libs/libcast.lua index fae2e270..509499f5 100644 --- a/libs/libcast.lua +++ b/libs/libcast.lua @@ -180,7 +180,6 @@ libcast:RegisterEvent("SPELLCAST_DELAYED") libcast:RegisterEvent("SPELLCAST_CHANNEL_START") libcast:RegisterEvent("SPELLCAST_CHANNEL_STOP") libcast:RegisterEvent("SPELLCAST_CHANNEL_UPDATE") -libcast:RegisterEvent("PLAYER_TARGET_CHANGED") libcast:SetScript("OnEvent", function() -- Fill database with player casts diff --git a/libs/libpredict.lua b/libs/libpredict.lua new file mode 100644 index 00000000..2272ef50 --- /dev/null +++ b/libs/libpredict.lua @@ -0,0 +1,476 @@ +-- load pfUI environment +setfenv(1, pfUI:GetEnvironment()) + +--[[ libpredict ]]-- +-- A pfUI library that detects, receives and sends heal and resurrection predictions. +-- Healing predictions are done by caching the last known "normal" heal value of the +-- spell when last being used. Those chaches are cleared when new talents are detected. +-- The API provides function calls similar to later WoW expansions such as: +-- UnitGetIncomingHeals(unit) +-- UnitHasIncomingResurrection(unit) +-- +-- The library is able to receive and send compatible messages to HealComm (vanilla) +-- and HealComm (tbc) including the ressurections of both versions. It has an option +-- to disable the sending of those messages in case one of the mentioned libraries +-- is already active. + +-- return instantly when another libpredict is already active +if pfUI.api.libpredict then return end + +local senttarget +local heals, ress, events = {}, {}, {} + +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 libpredict = CreateFrame("Frame") +libpredict:RegisterEvent("UNIT_HEALTH") +libpredict:RegisterEvent("CHAT_MSG_ADDON") +libpredict:RegisterEvent("PLAYER_TARGET_CHANGED") +libpredict:SetScript("OnEvent", function() + if event == "CHAT_MSG_ADDON" and (arg1 == "HealComm" or arg1 == "CTRA") then + this:ParseChatMessage(arg4, arg2, arg1) + elseif event == "UNIT_HEALTH" then + local name = UnitName(arg1) + if ress[name] and not UnitIsDeadOrGhost(arg1) then + ress[UnitName(arg1)] = nil + end + end +end) + +libpredict:SetScript("OnUpdate", function() + -- update on timeout events + for timestamp, targets in pairs(events) do + if GetTime() >= timestamp then + events[timestamp] = nil + end + end +end) + +function libpredict:ParseComm(sender, msg) + local msgtype, target, heal, time + + if msg == "Healstop" or msg == "GrpHealstop" then + msgtype = "Stop" + elseif msg == "Resurrection/stop/" then + msgtype = "RessStop" + else + local msgobj = {strsplit("/", msg)} + + if msgobj and msgobj[1] and msgobj[2] then -- legacy healcomm object + if msgobj[1] == "GrpHealdelay" or msgobj[1] == "Healdelay" then + msgtype, time = "Delay", msgobj[2] + end + + if msgobj[1] and msgobj[1] == "Resurrection" and msgobj[2] then + msgtype, target = "Ress", msgobj[2] + end + + if msgobj[1] == "Heal" and msgobj[2] then + msgtype, target, heal, time = "Heal", msgobj[2], msgobj[3], msgobj[4] + end + + if msgobj[1] == "GrpHeal" and msgobj[2] then + msgtype, target, heal, time = "Heal", {}, msgobj[2], msgobj[3] + for i=4,8 do + if msgobj[i] then table.insert(target, msgobj[i]) end + end + end + elseif select and UnitCastingInfo then -- latest healcomm + msgtype = tonumber(string.sub(msg, 1, 3)) + if not msgtype then return end + + if msgtype == 0 then + msgtype = "Heal" + heal = tonumber(string.sub(msg, 4, 8)) + target = string.sub(msg,9, -1) + + local starttime = select(5, UnitCastingInfo(sender)) + local endtime = select(6, UnitCastingInfo(sender)) + if not starttime or not endtime then return end + time = endtime - starttime + elseif msgtype == 1 then + msgtype = "Stop" + elseif msgtype == 2 then + msgtype = "Heal" + heal = tonumber(string.sub(msg,4, 8)) + target = {strsplit(":", string.sub(msg,9, -1))} + local starttime = select(5, UnitCastingInfo(sender)) + local endtime = select(6, UnitCastingInfo(sender)) + if not starttime or not endtime then return end + time = endtime - starttime + end + end + end + + return msgtype, target, heal, time +end + +function libpredict:ParseChatMessage(sender, msg, comm) + local msgtype, target, heal, time + + if comm == "HealComm" then + msgtype, target, heal, time = libpredict:ParseComm(sender, msg) + elseif comm == "CTRA" then + local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)") + if cmd and ctratarget and cmd == "RES" and ctratarget ~= "" and ctratarget ~= UNKNOWN then + msgtype = "Ress" + target = ctratarget + end + end + + if msgtype == "Stop" and sender then + libpredict:HealStop(sender) + return + elseif ( msg == "RessStop" or msg == "RESNO" ) and sender then + libpredict:RessStop(sender) + return + elseif msgtype == "Delay" and time then + libpredict:HealDelay(sender, time) + elseif msgtype == "Heal" and target and heal and time then + if type(target) == "table" then + for _, name in pairs(target) do + libpredict:Heal(sender, name, heal, time) + end + else + libpredict:Heal(sender, target, heal, time) + end + elseif msgtype == "Ress" then + libpredict:Ress(sender, target) + end +end + +function libpredict:AddEvent(time, target) + events[time] = events[time] or {} + table.insert(events[time], target) +end + +function libpredict:Heal(sender, target, amount, duration) + if not sender or not target or not amount or not duration then + return + end + + local timeout = duration/1000 + GetTime() + heals[target] = heals[target] or {} + heals[target][sender] = { amount, timeout } + libpredict:AddEvent(timeout, target) +end + +function libpredict:HealStop(sender) + for ttarget, t in pairs(heals) do + for tsender in pairs(heals[ttarget]) do + if sender == tsender then + heals[ttarget][tsender] = nil + end + end + end +end + +function libpredict:HealDelay(sender, delay) + local delay = delay/1000 + for target, t in pairs(heals) do + for tsender, amount in pairs(heals[target]) do + if sender == tsender then + amount[2] = amount[2] + delay + libpredict:AddEvent(amount[2], target) + end + end + end +end + +function libpredict:Ress(sender, target) + ress[target] = ress[target] or {} + ress[target][sender] = true +end + +function libpredict:RessStop(sender) + for ttarget, t in pairs(ress) do + for tsender in pairs(ress[ttarget]) do + if sender == tsender then + ress[ttarget][tsender] = nil + end + end + end +end + +function libpredict:UnitGetIncomingHeals(unit) + if not unit or not UnitName(unit) then return 0 end + if UnitIsDeadOrGhost(unit) then return 0 end + local name = UnitName(unit) + + local sumheal = 0 + if not heals[name] then + return sumheal + else + for sender, amount in pairs(heals[name]) do + if amount[2] <= GetTime() then + heals[name][sender] = nil + else + sumheal = sumheal + amount[1] + end + end + end + return sumheal +end + +function libpredict:UnitHasIncomingResurrection(unit) + if not unit or not UnitName(unit) then return nil end + local name = UnitName(unit) + + if not ress[name] then + return nil + else + for sender, val in pairs(ress[name]) do + if val == true then + return val + end + end + end + return nil +end + +local spell_queue = { "DUMMY", "DUMMYRank 9", "TARGET" } +local realm = GetRealmName() +local player = UnitName("player") +local cache, gear_string = {}, "" +local resetcache = CreateFrame("Frame") +resetcache:RegisterEvent("PLAYER_ENTERING_WORLD") +resetcache:RegisterEvent("SKILL_LINES_CHANGED") +resetcache:RegisterEvent("UNIT_INVENTORY_CHANGED") +resetcache:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + -- load and initialize previous caches of spell amounts + pfUI_cache["prediction"] = pfUI_cache["prediction"] or {} + pfUI_cache["prediction"][realm] = pfUI_cache["prediction"][realm] or {} + pfUI_cache["prediction"][realm][player] = pfUI_cache["prediction"][realm][player] or {} + pfUI_cache["prediction"][realm][player]["heals"] = pfUI_cache["prediction"][realm][player]["heals"] or {} + cache = pfUI_cache["prediction"][realm][player]["heals"] + return + end + + if event == "UNIT_INVENTORY_CHANGED" then + -- skip non-player events + if arg1 and arg1 ~= "player" then return end + + local gear = "" + for id = 1, 18 do + gear = gear .. (GetInventoryItemLink("player",id) or "") + end + + -- abort when inventory didn't change + if gear == gear_string then return end + gear_string = gear + end + + -- flag all cached heals for renewal + for k in pairs(cache) do + if type(cache[k]) == "number" or type(cache[k]) == "string" then + -- migrate old data + local oldval = cache[k] + cache[k] = { [1] = oldval } + end + + -- flag for reset + cache[k][2] = true + end +end) + +local function UpdateCache(spell, heal, crit) + if not spell or not heal then return end + local heal = tonumber(heal) + + if not cache[spell] or cache[spell][2] then + -- skills or equipment changed, save whatever is detected + cache[spell] = cache[spell] or {} + cache[spell][1] = crit and heal*2/3 or heal + cache[spell][2] = crit + elseif not crit and cache[spell][1] < heal then + -- safe the best heal we can get + cache[spell][1] = heal + cache[spell][2] = nil + end +end + +-- Gather Data by User Actions +hooksecurefunc("CastSpell", function(id, bookType) + if not libpredict.sender.enabled then return end + local effect, rank = libspell.GetSpellInfo(id, bookType) + if not effect then return end + spell_queue[1] = effect + spell_queue[2] = effect.. ( rank or "" ) + spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") +end, true) + +hooksecurefunc("CastSpellByName", function(effect, target) + if not libpredict.sender.enabled then return end + local effect, rank = libspell.GetSpellInfo(effect) + if not effect then return end + spell_queue[1] = effect + spell_queue[2] = effect.. ( rank or "" ) + spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") +end, true) + +local scanner = libtipscan:GetScanner("prediction") +hooksecurefunc("UseAction", function(slot, target, selfcast) + if not libpredict.sender.enabled then return end + if GetActionText(slot) or not IsCurrentAction(slot) then return end + scanner:SetAction(slot) + local effect, rank = scanner:Line(1) + if not effect then return end + spell_queue[1] = effect + spell_queue[2] = effect.. ( rank or "" ) + spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") +end, true) + +libpredict.sender = CreateFrame("Frame", "pfPredictionSender", UIParent) +libpredict.sender.enabled = true +libpredict.sender.SendHealCommMsg = function(self, msg) + SendAddonMessage("HealComm", msg, "RAID") + SendAddonMessage("HealComm", msg, "BATTLEGROUND") +end +libpredict.sender.SendResCommMsg = function(self, msg) + SendAddonMessage("CTRA", msg, "RAID") + SendAddonMessage("CTRA", msg, "BATTLEGROUND") +end + +-- tbc +libpredict.sender:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_START") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_STOP") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_FAILED") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_SENT") + +-- vanilla +libpredict.sender:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF") +libpredict.sender:RegisterEvent("SPELLCAST_START") +libpredict.sender:RegisterEvent("SPELLCAST_STOP") +libpredict.sender:RegisterEvent("SPELLCAST_FAILED") +libpredict.sender:RegisterEvent("SPELLCAST_INTERRUPTED") +libpredict.sender:RegisterEvent("SPELLCAST_DELAYED") + +-- force cache updates +libpredict.sender:RegisterEvent("UNIT_INVENTORY_CHANGED") +libpredict.sender:RegisterEvent("SKILL_LINES_CHANGED") + +libpredict.sender:SetScript("OnEvent", function() + if event == "CHAT_MSG_SPELL_SELF_BUFF" then -- vanilla + local spell, _, heal = cmatch(arg1, HEALEDSELFOTHER) -- "Your %s heals %s for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end + return + end + + local spell, heal = cmatch(arg1, HEALEDSELFSELF) -- "Your %s heals you for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end + return + end + + local spell, heal = cmatch(arg1, HEALEDCRITSELFOTHER) -- "Your %s critically heals %s for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end + return + end + + local spell, _, heal = cmatch(arg1, HEALEDCRITSELFSELF) -- "Your %s critically heals you for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end + return + end + elseif event == "COMBAT_LOG_EVENT_UNFILTERED" and arg2 == "SPELL_HEAL" and arg4 == player then -- tbc + local spell, heal, crit = arg10, arg12, arg13 + if spell and heal and crit then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end + elseif spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end + end + elseif event == "UNIT_SPELLCAST_SENT" and arg4 then -- fix tbc mouseover macros + senttarget = arg4 + elseif strfind(event, "SPELLCAST_START", 1) then + local spell, time = arg1, arg2 + + if strfind(event, "UNIT_", 1) then -- tbc + if arg1 ~= "player" then return end + local spellname, _, _, _, starttime, endtime = UnitCastingInfo("player") + spell, time = spellname, endtime - starttime + end + + if spell_queue[1] == spell and cache[spell_queue[2]] then + local sender = player + local target = senttarget or spell_queue[3] + local amount = cache[spell_queue[2]][1] + local casttime = time + + if spell == PRAYER_OF_HEALING then + target = sender + + for i=1,4 do + if CheckInteractDistance("party"..i, 4) then + libpredict:Heal(player, UnitName("party"..i), amount, casttime) + if pfUI.client < 20000 then -- vanilla + libpredict.sender:SendHealCommMsg("Heal/" .. UnitName("party"..i) .. "/" .. amount .. "/" .. casttime .. "/") + else -- tbc + libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), UnitName("party"..i))) + end + libpredict.sender.healing = true + end + end + end + + libpredict:Heal(player, target, amount, casttime) + if pfUI.client < 20000 then -- vanilla + libpredict.sender:SendHealCommMsg("Heal/" .. target .. "/" .. amount .. "/" .. casttime .. "/") + else -- tbc + libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), target)) + end + libpredict.sender.healing = true + + elseif spell_queue[1] == spell and L["resurrections"][spell] then + local target = senttarget or spell_queue[3] + libpredict:Ress(player, target) + libpredict.sender:SendHealCommMsg("Resurrection/" .. target .. "/start/") + libpredict.sender:SendResCommMsg("RES " .. target) + libpredict.sender.resurrecting = true + end + elseif strfind(event, "SPELLCAST_FAILED", 1) or strfind(event, "SPELLCAST_INTERRUPTED", 1) then + if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end + if libpredict.sender.healing then + libpredict:HealStop(player) + if pfUI.client < 20000 then -- vanilla + libpredict.sender:SendHealCommMsg("HealStop") + else -- tbc + libpredict.sender:SendHealCommMsg("001F") + end + libpredict.sender.healing = nil + elseif libpredict.sender.resurrecting then + local target = senttarget or spell_queue[3] + libpredict:RessStop(player) + libpredict.sender:SendHealCommMsg("Resurrection/stop/") + libpredict.sender:SendResCommMsg("RESNO " .. target) + libpredict.sender.resurrecting = nil + end + elseif event == "SPELLCAST_DELAYED" then + if libpredict.sender.healing then + libpredict:HealDelay(player, arg1) + libpredict.sender:SendHealCommMsg("Healdelay/" .. arg1 .. "/") + end + elseif strfind(event, "SPELLCAST_STOP", 1) then + if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end + libpredict:HealStop(player) + end +end) + +pfUI.api.libpredict = libpredict diff --git a/modules/prediction.lua b/modules/prediction.lua deleted file mode 100644 index b0b7f56d..00000000 --- a/modules/prediction.lua +++ /dev/null @@ -1,523 +0,0 @@ -pfUI:RegisterModule("prediction", "vanilla:tbc", function () - local heals = {} - local ress = {} - local events = {} - local senttarget - - 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 - - pfUI.prediction = CreateFrame("Frame") - pfUI.prediction:RegisterEvent("UNIT_HEALTH") - pfUI.prediction:RegisterEvent("CHAT_MSG_ADDON") - pfUI.prediction:RegisterEvent("PLAYER_TARGET_CHANGED") - pfUI.prediction:SetScript("OnEvent", function() - if event == "CHAT_MSG_ADDON" and (arg1 == "HealComm" or arg1 == "CTRA") then - this:ParseChatMessage(arg4, arg2, arg1) - elseif event == "UNIT_HEALTH" then - local name = UnitName(arg1) - if ress[name] and not UnitIsDeadOrGhost(arg1) then - ress[UnitName(arg1)] = nil - this:TriggerUpdate(name) - end - - if heals[name] then - this:TriggerUpdate(name) - end - elseif event == "PLAYER_TARGET_CHANGED" then - this:TriggerUpdate(UnitName("target")) - end - end) - - pfUI.prediction:SetScript("OnUpdate", function() - for timestamp, targets in pairs(events) do - if GetTime() >= timestamp then - for id, target in pairs(targets) do - pfUI.prediction:TriggerUpdate(target) - end - events[timestamp] = nil - end - end - end) - - local function ParseComm(sender, msg) - local msgtype, target, heal, time - - if msg == "Healstop" or msg == "GrpHealstop" then - msgtype = "Stop" - elseif msg == "Resurrection/stop/" then - msgtype = "RessStop" - else - local msgobj = {strsplit("/", msg)} - - if msgobj and msgobj[1] and msgobj[2] then -- legacy healcomm object - if msgobj[1] == "GrpHealdelay" or msgobj[1] == "Healdelay" then - msgtype, time = "Delay", msgobj[2] - end - - if msgobj[1] and msgobj[1] == "Resurrection" and msgobj[2] then - msgtype, target = "Ress", msgobj[2] - end - - if msgobj[1] == "Heal" and msgobj[2] then - msgtype, target, heal, time = "Heal", msgobj[2], msgobj[3], msgobj[4] - end - - if msgobj[1] == "GrpHeal" and msgobj[2] then - msgtype, target, heal, time = "Heal", {}, msgobj[2], msgobj[3] - for i=4,8 do - if msgobj[i] then table.insert(target, msgobj[i]) end - end - end - elseif select and UnitCastingInfo then -- latest healcomm - msgtype = tonumber(string.sub(msg, 1, 3)) - if not msgtype then return end - - if msgtype == 0 then - msgtype = "Heal" - heal = tonumber(string.sub(msg, 4, 8)) - target = string.sub(msg,9, -1) - - local starttime = select(5, UnitCastingInfo(sender)) - local endtime = select(6, UnitCastingInfo(sender)) - if not starttime or not endtime then return end - time = endtime - starttime - elseif msgtype == 1 then - msgtype = "Stop" - elseif msgtype == 2 then - msgtype = "Heal" - heal = tonumber(string.sub(msg,4, 8)) - target = {strsplit(":", string.sub(msg,9, -1))} - local starttime = select(5, UnitCastingInfo(sender)) - local endtime = select(6, UnitCastingInfo(sender)) - if not starttime or not endtime then return end - time = endtime - starttime - end - end - end - - return msgtype, target, heal, time - end - - function pfUI.prediction:ParseChatMessage(sender, msg, comm) - local msgtype, target, heal, time - - if comm == "HealComm" then - msgtype, target, heal, time = ParseComm(sender, msg) - elseif comm == "CTRA" then - local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)") - if cmd and ctratarget and cmd == "RES" and ctratarget ~= "" and ctratarget ~= UNKNOWN then - msgtype = "Ress" - target = ctratarget - end - end - - if msgtype == "Stop" and sender then - pfUI.prediction:HealStop(sender) - return - elseif ( msg == "RessStop" or msg == "RESNO" ) and sender then - pfUI.prediction:RessStop(sender) - return - elseif msgtype == "Delay" and time then - pfUI.prediction:HealDelay(sender, time) - elseif msgtype == "Heal" and target and heal and time then - if type(target) == "table" then - for _, name in pairs(target) do - pfUI.prediction:Heal(sender, name, heal, time) - end - else - pfUI.prediction:Heal(sender, target, heal, time) - end - elseif msgtype == "Ress" then - pfUI.prediction:Ress(sender, target) - end - end - - function pfUI.prediction:AddEvent(time, target) - events[time] = events[time] or {} - table.insert(events[time], target) - end - - function pfUI.prediction:Heal(sender, target, amount, duration) - if not sender or not target or not amount or not duration then - return - end - - local timeout = duration/1000 + GetTime() - heals[target] = heals[target] or {} - heals[target][sender] = { amount, timeout } - pfUI.prediction:TriggerUpdate(target) - pfUI.prediction:AddEvent(timeout, target) - end - - function pfUI.prediction:HealStop(sender) - for ttarget, t in pairs(heals) do - for tsender in pairs(heals[ttarget]) do - if sender == tsender then - heals[ttarget][tsender] = nil - pfUI.prediction:TriggerUpdate(ttarget) - end - end - end - end - - function pfUI.prediction:HealDelay(sender, delay) - local delay = delay/1000 - for target, t in pairs(heals) do - for tsender, amount in pairs(heals[target]) do - if sender == tsender then - amount[2] = amount[2] + delay - pfUI.prediction:TriggerUpdate(target) - pfUI.prediction:AddEvent(amount[2], target) - end - end - end - end - - function pfUI.prediction:GetHeal(target) - local sumheal = 0 - if not heals[target] then - return sumheal - else - for sender, amount in pairs(heals[target]) do - if amount[2] <= GetTime() then - heals[target][sender] = nil - else - sumheal = sumheal + amount[1] - end - end - end - return sumheal - end - - function pfUI.prediction:Ress(sender, target) - ress[target] = ress[target] or {} - ress[target][sender] = true - pfUI.prediction:TriggerUpdate(target) - end - - function pfUI.prediction:RessStop(sender) - for ttarget, t in pairs(ress) do - for tsender in pairs(ress[ttarget]) do - if sender == tsender then - ress[ttarget][tsender] = nil - pfUI.prediction:TriggerUpdate(ttarget) - end - end - end - end - - function pfUI.prediction:GetRess(target) - if not ress[target] then - return nil - else - for sender, val in pairs(ress[target]) do - if val == true then - return val - end - end - end - return nil - end - - function pfUI.prediction:TriggerUpdate(target) - if not target then return end - local heal = pfUI.prediction:GetHeal(target) - local ress = pfUI.prediction:GetRess(target) - - if pfUI.uf then - pfUI.prediction:UpdateUnitFrames(target, heal, ress) - end - end - - function pfUI.prediction:UpdateUnitFrames(name, heal, ress) - for id, frame in pairs(_G.pfUI.uf.frames) do - if frame:IsVisible() and frame.label and frame.id then - local overhealperc = tonumber(frame.config.overhealperc) - local unit = frame.label .. frame.id - local health, maxHealth = UnitHealth(unit), UnitHealthMax(unit) - - if UnitName(unit) == name then - if ress and UnitIsDeadOrGhost(unit) then - frame.ressIcon:Show() - elseif heal and heal > 0 and (health < maxHealth or overhealperc > 0 ) then - local width = frame.config.width - local height = frame.config.height - - if frame.config.verticalbar == "0" then - local healthWidth = width * (health / maxHealth) - local incWidth = width * heal / maxHealth - if healthWidth + incWidth > width * (1+(overhealperc/100)) then - incWidth = width * (1+overhealperc/100) - healthWidth - end - - if frame.config.invert_healthbar == "1" then - frame.incHeal:SetWidth(incWidth) - else - frame.incHeal:SetWidth(incWidth + healthWidth) - end - else - local healthHeight = height * (health / maxHealth) - local incHeight = height * heal / maxHealth - if healthHeight + incHeight > height * (1+(overhealperc/100)) then - incHeight = height * (1+overhealperc/100) - healthHeight - end - - if frame.config.invert_healthbar == "1" then - frame.incHeal:SetHeight(incHeight) - else - frame.incHeal:SetHeight(incHeight + healthHeight) - end - end - - frame.incHeal:Show() - else - frame.incHeal:Hide() - frame.ressIcon:Hide() - end - end - end - end - end - - local spell_queue = { "DUMMY", "DUMMYRank 9", "TARGET" } - - local realm = GetRealmName() - local player = UnitName("player") - pfUI_cache["prediction"] = pfUI_cache["prediction"] or {} - pfUI_cache["prediction"][realm] = pfUI_cache["prediction"][realm] or {} - pfUI_cache["prediction"][realm][player] = pfUI_cache["prediction"][realm][player] or {} - pfUI_cache["prediction"][realm][player]["heals"] = pfUI_cache["prediction"][realm][player]["heals"] or {} - - local cache = pfUI_cache["prediction"][realm][player]["heals"] - local gear_string = "" - local resetcache = CreateFrame("Frame") - - resetcache:RegisterEvent("SKILL_LINES_CHANGED") - resetcache:RegisterEvent("UNIT_INVENTORY_CHANGED") - resetcache:SetScript("OnEvent", function() - if event == "UNIT_INVENTORY_CHANGED" then - -- skip non-player events - if arg1 and arg1 ~= "player" then return end - - local gear = "" - for id = 1, 18 do - gear = gear .. (GetInventoryItemLink("player",id) or "") - end - - -- abort when inventory didn't change - if gear == gear_string then return end - gear_string = gear - end - - -- flag all cached heals for renewal - for k in pairs(cache) do - if type(cache[k]) == "number" or type(cache[k]) == "string" then - -- migrate old data - local oldval = cache[k] - cache[k] = { [1] = oldval } - end - - -- flag for reset - cache[k][2] = true - end - end) - - local function UpdateCache(spell, heal, crit) - if not spell or not heal then return end - local heal = tonumber(heal) - - if not cache[spell] or cache[spell][2] then - -- skills or equipment changed, save whatever is detected - cache[spell] = cache[spell] or {} - cache[spell][1] = crit and heal*2/3 or heal - cache[spell][2] = crit - elseif not crit and cache[spell][1] < heal then - -- safe the best heal we can get - cache[spell][1] = heal - cache[spell][2] = nil - end - end - - -- Gather Data by User Actions - hooksecurefunc("CastSpell", function(id, bookType) - if not pfUI.prediction.sender.enabled then return end - local effect, rank = libspell.GetSpellInfo(id, bookType) - if not effect then return end - spell_queue[1] = effect - spell_queue[2] = effect.. ( rank or "" ) - spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") - end, true) - - hooksecurefunc("CastSpellByName", function(effect, target) - if not pfUI.prediction.sender.enabled then return end - local effect, rank = libspell.GetSpellInfo(effect) - if not effect then return end - spell_queue[1] = effect - spell_queue[2] = effect.. ( rank or "" ) - spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") - end, true) - - local scanner = libtipscan:GetScanner("prediction") - hooksecurefunc("UseAction", function(slot, target, selfcast) - if not pfUI.prediction.sender.enabled then return end - if GetActionText(slot) or not IsCurrentAction(slot) then return end - scanner:SetAction(slot) - local effect, rank = scanner:Line(1) - if not effect then return end - spell_queue[1] = effect - spell_queue[2] = effect.. ( rank or "" ) - spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") - end, true) - - pfUI.prediction.sender = CreateFrame("Frame", "pfPredictionSender", UIParent) - pfUI.prediction.sender.enabled = true - pfUI.prediction.sender.SendHealCommMsg = function(self, msg) - SendAddonMessage("HealComm", msg, "RAID") - SendAddonMessage("HealComm", msg, "BATTLEGROUND") - end - pfUI.prediction.sender.SendResCommMsg = function(self, msg) - SendAddonMessage("CTRA", msg, "RAID") - SendAddonMessage("CTRA", msg, "BATTLEGROUND") - end - - -- tbc - pfUI.prediction.sender:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") - pfUI.prediction.sender:RegisterEvent("UNIT_SPELLCAST_START") - pfUI.prediction.sender:RegisterEvent("UNIT_SPELLCAST_STOP") - pfUI.prediction.sender:RegisterEvent("UNIT_SPELLCAST_FAILED") - pfUI.prediction.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") - pfUI.prediction.sender:RegisterEvent("UNIT_SPELLCAST_SENT") - - -- vanilla - pfUI.prediction.sender:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF") - pfUI.prediction.sender:RegisterEvent("SPELLCAST_START") - pfUI.prediction.sender:RegisterEvent("SPELLCAST_STOP") - pfUI.prediction.sender:RegisterEvent("SPELLCAST_FAILED") - pfUI.prediction.sender:RegisterEvent("SPELLCAST_INTERRUPTED") - pfUI.prediction.sender:RegisterEvent("SPELLCAST_DELAYED") - - -- force cache updates - pfUI.prediction.sender:RegisterEvent("UNIT_INVENTORY_CHANGED") - pfUI.prediction.sender:RegisterEvent("SKILL_LINES_CHANGED") - - pfUI.prediction.sender:SetScript("OnEvent", function() - if event == "CHAT_MSG_SPELL_SELF_BUFF" then -- vanilla - local spell, _, heal = cmatch(arg1, HEALEDSELFOTHER) -- "Your %s heals %s for %d." - if spell and heal then - if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end - return - end - - local spell, heal = cmatch(arg1, HEALEDSELFSELF) -- "Your %s heals you for %d." - if spell and heal then - if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end - return - end - - local spell, heal = cmatch(arg1, HEALEDCRITSELFOTHER) -- "Your %s critically heals %s for %d." - if spell and heal then - if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end - return - end - - local spell, _, heal = cmatch(arg1, HEALEDCRITSELFSELF) -- "Your %s critically heals you for %d." - if spell and heal then - if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end - return - end - elseif event == "COMBAT_LOG_EVENT_UNFILTERED" and arg2 == "SPELL_HEAL" and arg4 == player then -- tbc - local spell, heal, crit = arg10, arg12, arg13 - if spell and heal and crit then - if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end - elseif spell and heal then - if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end - end - elseif event == "UNIT_SPELLCAST_SENT" and arg4 then -- fix tbc mouseover macros - senttarget = arg4 - elseif strfind(event, "SPELLCAST_START", 1) then - local spell, time = arg1, arg2 - - if strfind(event, "UNIT_", 1) then -- tbc - if arg1 ~= "player" then return end - local spellname, _, _, _, starttime, endtime = UnitCastingInfo("player") - spell, time = spellname, endtime - starttime - end - - if spell_queue[1] == spell and cache[spell_queue[2]] then - local sender = player - local target = senttarget or spell_queue[3] - local amount = cache[spell_queue[2]][1] - local casttime = time - - if spell == PRAYER_OF_HEALING then - target = sender - - for i=1,4 do - if CheckInteractDistance("party"..i, 4) then - pfUI.prediction:Heal(player, UnitName("party"..i), amount, casttime) - if pfUI.client < 20000 then -- vanilla - pfUI.prediction.sender:SendHealCommMsg("Heal/" .. UnitName("party"..i) .. "/" .. amount .. "/" .. casttime .. "/") - else -- tbc - pfUI.prediction.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), UnitName("party"..i))) - end - pfUI.prediction.sender.healing = true - end - end - end - - pfUI.prediction:Heal(player, target, amount, casttime) - if pfUI.client < 20000 then -- vanilla - pfUI.prediction.sender:SendHealCommMsg("Heal/" .. target .. "/" .. amount .. "/" .. casttime .. "/") - else -- tbc - pfUI.prediction.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), target)) - end - pfUI.prediction.sender.healing = true - - elseif spell_queue[1] == spell and L["resurrections"][spell] then - local target = senttarget or spell_queue[3] - pfUI.prediction:Ress(player, target) - pfUI.prediction.sender:SendHealCommMsg("Resurrection/" .. target .. "/start/") - pfUI.prediction.sender:SendResCommMsg("RES " .. target) - pfUI.prediction.sender.resurrecting = true - end - elseif strfind(event, "SPELLCAST_FAILED", 1) or strfind(event, "SPELLCAST_INTERRUPTED", 1) then - if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end - if pfUI.prediction.sender.healing then - pfUI.prediction:HealStop(player) - if pfUI.client < 20000 then -- vanilla - pfUI.prediction.sender:SendHealCommMsg("HealStop") - else -- tbc - pfUI.prediction.sender:SendHealCommMsg("001F") - end - pfUI.prediction.sender.healing = nil - elseif pfUI.prediction.sender.resurrecting then - local target = senttarget or spell_queue[3] - pfUI.prediction:RessStop(player) - pfUI.prediction.sender:SendHealCommMsg("Resurrection/stop/") - pfUI.prediction.sender:SendResCommMsg("RESNO " .. target) - pfUI.prediction.sender.resurrecting = nil - end - elseif event == "SPELLCAST_DELAYED" then - if pfUI.prediction.sender.healing then - pfUI.prediction:HealDelay(player, arg1) - pfUI.prediction.sender:SendHealCommMsg("Healdelay/" .. arg1 .. "/") - end - elseif strfind(event, "SPELLCAST_STOP", 1) then - if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end - pfUI.prediction:HealStop(player) - end - end) -end)