Skip to content

Instantly share code, notes, and snippets.

@ka-pr
Last active August 14, 2026 08:04
Show Gist options
  • Select an option

  • Save ka-pr/7f98f3638f89f3a727d06b694e4a1089 to your computer and use it in GitHub Desktop.

Select an option

Save ka-pr/7f98f3638f89f3a727d06b694e4a1089 to your computer and use it in GitHub Desktop.
Cell (WoW addon) LibHealComm heal prediction arbitration snippet - CHAP/CHAD
-- Cell Heal Arbitration Prediction (CHAP) with Cell Heal Arbitration
-- Diagnostics (CHAD)
--
-- Improves Cell's incoming-heal prediction on raid/party frames by
-- combining Blizzard's native prediction with LibHealComm-4.0's, since
-- native alone misses HoTs/AoE healing and any healer whose own client
-- isn't broadcasting via LibHealComm.
--
-- Requires some addon on this client to provide LibHealComm-4.0, either
-- as its own standalone library addon or bundled inside another addon
-- (e.g. YAHP). If none is found, this prints a message and does nothing
-- else - click Run again in the Code Snippets editor once one is
-- installed if it doesn't pick it up automatically.
--
-- Commands (/chap also works as /cellhealpred, /chad also works as
-- /cellhealdiag; /chap with no args lists all of these in-game):
-- /chap - show status and this command list.
-- /chap debug - toggle a raw trace of widget discovery/hooking.
-- /chap diag - toggle CHAD (the /chad diagnostics below) on/off.
-- /chad - show how many heals were won by native prediction vs
-- LibHealComm.
-- /chad reset - clear the diagnostic counters.
-- /chad debug - toggle a raw trace of each diagnostic decision.
-- /chad test - show collected native/LibHealComm/actual-landed data per
-- target+spell (only while CHAD is on) - useful for checking whether
-- native tracks spell rank/gear the same way LibHealComm does.
-- /chad test reset - clear collected test data.
if CELL_CHAD_ENABLED == nil then
CELL_CHAD_ENABLED = false
end
-- How native and LibHealComm get combined into the displayed value,
-- whenever both have something to say:
-- "max" (default) - always the larger of the two.
-- "min" - always the smaller - more conservative, if you'd rather
-- under-predict than over-predict incoming healing.
-- "avg" - split the difference.
-- Falls back to whichever single source is nonzero if the other is 0
-- (e.g. HoTs, which native never predicts).
local CHAP_AGGREGATE = "max"
local function ChapInit()
local HealComm = LibStub("LibHealComm-4.0", true)
if not HealComm then
print("|cff0099ffCHAP|r: LibHealComm-4.0 isn't available on this client - install it as a standalone library addon, or install any addon that bundles it (e.g. YAHP), for heal prediction arbitration to work. If it IS installed and this just didn't pick it up automatically, click Run on this snippet in the Code Snippets editor to activate it now.")
return
end
if not Cell or not Cell.funcs or not Cell.funcs.HandleUnitButton then
print("|cff0099ffCHAP|r: Cell.funcs.HandleUnitButton isn't available - Cell may not be fully loaded yet, or its internal structure changed. Click Run again once Cell is fully loaded, or report this if it keeps happening.")
return
end
local F = Cell.funcs
local chapDebug = false
local chapErrorCount = 0
local chapHookedCount = 0
local chadDebug = false
local healPredictionDiagLastValues = {}
local healPredictionDiagLastWinner = {}
local HEAL_PREDICTION_DIAG_EPSILON = 1
local chapSpellData = {}
local chapCurrentSpellByGuid = {}
local function GetSpellBucket(destGUID, spellID, spellName)
local targetData = chapSpellData[destGUID]
if not targetData then
targetData = {spells = {}}
chapSpellData[destGUID] = targetData
end
local bucket = targetData.spells[spellID]
if not bucket then
bucket = {spellName = spellName, blizzardMax = 0, libMax = 0, actualHeals = {}, actualCritHeals = {}}
targetData.spells[spellID] = bucket
elseif spellName and not bucket.spellName then
bucket.spellName = spellName
end
return bucket
end
local function HealPredictionDiag_Classify(blizzardValue, libValue)
if libValue > blizzardValue then return "lib"
elseif blizzardValue > libValue then return "blizzard"
else return "tie" end
end
local function HealPredictionDiag_Record(guid, blizzardValue, libValue)
if not guid then return end
local last = healPredictionDiagLastValues[guid]
healPredictionDiagLastValues[guid] = {blizzard = blizzardValue, lib = libValue}
if blizzardValue == 0 and libValue == 0 then
healPredictionDiagLastWinner[guid] = nil
return
end
local grew = (not last)
or (blizzardValue - last.blizzard > HEAL_PREDICTION_DIAG_EPSILON)
or (libValue - last.lib > HEAL_PREDICTION_DIAG_EPSILON)
local winner = HealPredictionDiag_Classify(blizzardValue, libValue)
local winnerChanged = healPredictionDiagLastWinner[guid] ~= winner
if chadDebug and grew then
print(string.format("|cff00FF00CHAD|r %s: blizzard=%.0f lib=%.0f last=%s winner=%s(%s)",
guid:sub(-6), blizzardValue, libValue,
last and string.format("(%.0f/%.0f)", last.blizzard, last.lib) or "none",
winner, winnerChanged and "new" or "same"))
end
if not grew or not winnerChanged then return end
healPredictionDiagLastWinner[guid] = winner
CellDB.healPredictionDiag = CellDB.healPredictionDiag or {total = 0, blizzardWins = 0, libHealCommWins = 0, tie = 0}
local diag = CellDB.healPredictionDiag
diag.total = diag.total + 1
if winner == "lib" then
diag.libHealCommWins = diag.libHealCommWins + 1
elseif winner == "blizzard" then
diag.blizzardWins = diag.blizzardWins + 1
else
diag.tie = diag.tie + 1
end
end
local function AggregateValue(blizzardValue, libValue)
if blizzardValue == 0 or libValue == 0 then
return max(blizzardValue, libValue)
end
if CHAP_AGGREGATE == "min" then
return math.min(blizzardValue, libValue)
elseif CHAP_AGGREGATE == "avg" then
return (blizzardValue + libValue) / 2
else
return max(blizzardValue, libValue)
end
end
local function ComputeArbitratedValue(button)
local unit = button.states and button.states.displayedUnit
if not unit then return nil end
local guid = UnitGUID(unit)
if not guid then return nil end
local blizzardValue = UnitGetIncomingHeals(unit) or 0
local modifier = HealComm:GetHealModifier(guid) or 1
local libValue = (HealComm:GetHealAmount(guid, HealComm.CASTED_HEALS) or 0) * modifier
local hot = (HealComm:GetHealAmount(guid, HealComm.OVERTIME_AND_BOMB_HEALS, GetTime() + 3) or 0) * modifier
libValue = libValue + hot
local arbitratedValue = AggregateValue(blizzardValue, libValue)
if CELL_CHAD_ENABLED then
HealPredictionDiag_Record(guid, blizzardValue, libValue)
local spellID = chapCurrentSpellByGuid[guid]
if spellID then
local spellName = GetSpellInfo and GetSpellInfo(spellID)
local bucket = GetSpellBucket(guid, spellID, spellName)
bucket.blizzardMax = max(bucket.blizzardMax, blizzardValue)
bucket.libMax = max(bucket.libMax, libValue)
end
if chapSpellData[guid] and not chapSpellData[guid].name then
chapSpellData[guid].name = UnitName(unit)
end
end
return arbitratedValue
end
local hookedWidgets = setmetatable({}, {__mode = "k"})
local reentryGuard = setmetatable({}, {__mode = "k"})
local function ApplyArbitratedValue(widget, button)
if reentryGuard[widget] then return end
local ok, arbitratedValue = pcall(ComputeArbitratedValue, button)
if not ok then
chapErrorCount = chapErrorCount + 1
print(string.format("|cff0099ffCHAP ERROR|r (leaving native value as-is, %d total): %s", chapErrorCount, tostring(arbitratedValue)))
return
end
if not arbitratedValue or arbitratedValue == 0 then return end
local healthMax = button.states and button.states.healthMax
if not healthMax or healthMax == 0 then return end
local newFraction = arbitratedValue / healthMax
if chapDebug then
print(string.format("|cff0099ffCHAP APPLY|r unit=%s arbitrated=%.0f healthMax=%.0f fraction=%.3f",
tostring(button.states and button.states.displayedUnit), arbitratedValue, healthMax, newFraction))
end
reentryGuard[widget] = true
local setOk, setErr = pcall(function()
widget:Show()
local getOk, currentFraction = pcall(widget.GetValue, widget)
if not (getOk and currentFraction and math.abs(newFraction - currentFraction) < 0.0001) then
widget:SetValue(newFraction)
end
end)
reentryGuard[widget] = false
if not setOk then
chapErrorCount = chapErrorCount + 1
print(string.format("|cff0099ffCHAP ERROR|r (Show/SetValue failed, %d total): %s", chapErrorCount, tostring(setErr)))
end
end
local function HookButtonWidget(button)
local widget = button and button.widgets and button.widgets.incomingHeal
if not widget or hookedWidgets[widget] then return end
hookedWidgets[widget] = true
chapHookedCount = chapHookedCount + 1
if chapDebug then
print(string.format("|cff0099ffCHAP|r hooked incomingHeal widget #%d for unit %s", chapHookedCount, tostring(button.states and button.states.displayedUnit)))
end
hooksecurefunc(widget, "SetValue", function(w)
ApplyArbitratedValue(w, button)
end)
hooksecurefunc(widget, "Hide", function(w)
ApplyArbitratedValue(w, button)
end)
end
local function HookAndRefresh(button)
HookButtonWidget(button)
local widget = button and button.widgets and button.widgets.incomingHeal
if widget then
ApplyArbitratedValue(widget, button)
end
end
local chapHealComm = {}
local function HealComm_Discover(_, event, casterGUID, spellID, healType, endTime, ...)
for i = 1, select("#", ...) do
local destGUID = select(i, ...)
if CELL_CHAD_ENABLED and spellID then
chapCurrentSpellByGuid[destGUID] = spellID
end
F.HandleUnitButton("guid", destGUID, HookAndRefresh)
end
end
chapHealComm.HealComm_Discover = HealComm_Discover
local function HealComm_DiscoverSingleGUID(_, event, guid)
F.HandleUnitButton("guid", guid, HookAndRefresh)
end
chapHealComm.HealComm_DiscoverSingleGUID = HealComm_DiscoverSingleGUID
HealComm.RegisterCallback(chapHealComm, "HealComm_HealStarted", "HealComm_Discover")
HealComm.RegisterCallback(chapHealComm, "HealComm_HealUpdated", "HealComm_Discover")
HealComm.RegisterCallback(chapHealComm, "HealComm_HealStopped", "HealComm_Discover")
HealComm.RegisterCallback(chapHealComm, "HealComm_HealDelayed", "HealComm_Discover")
HealComm.RegisterCallback(chapHealComm, "HealComm_ModifierChanged", "HealComm_DiscoverSingleGUID")
HealComm.RegisterCallback(chapHealComm, "HealComm_GUIDDisappeared", "HealComm_DiscoverSingleGUID")
local chapNativeDiscoveryFrame = CreateFrame("Frame")
chapNativeDiscoveryFrame:RegisterEvent("UNIT_HEAL_PREDICTION")
chapNativeDiscoveryFrame:SetScript("OnEvent", function(_, event, unit)
local guid = unit and UnitGUID(unit)
if guid then
F.HandleUnitButton("guid", guid, HookAndRefresh)
end
end)
local chapCombatLogFrame = CreateFrame("Frame")
chapCombatLogFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
chapCombatLogFrame:SetScript("OnEvent", function()
if not CELL_CHAD_ENABLED then return end
local _, subevent, _, sourceGUID, sourceName, _, _, destGUID, destName, _, _,
spellID, spellName, spellSchool, amount, overhealing, absorbed, critical = CombatLogGetCurrentEventInfo()
if subevent ~= "SPELL_HEAL" and subevent ~= "SPELL_PERIODIC_HEAL" then return end
if not spellID or not destGUID then return end
if chapDebug then
print(string.format("|cff0099ffCHAP CLEU|r %s spellID=%s school=%s amount=%s overhealing=%s absorbed=%s critical=%s",
subevent, tostring(spellID), tostring(spellSchool), tostring(amount), tostring(overhealing), tostring(absorbed), tostring(critical)))
end
local bucket = GetSpellBucket(destGUID, spellID, spellName)
if destName and not (chapSpellData[destGUID] and chapSpellData[destGUID].name) then
chapSpellData[destGUID].name = destName
end
table.insert(critical and bucket.actualCritHeals or bucket.actualHeals, amount or 0)
end)
local CHAP_TAG = "|cff0099ff[CHAP]|r "
local CHAD_TAG = "|cff00FF00[CHAD]|r "
local CMD_CHAP = "|cff33ccff"
local CMD_CHAD = "|cff99ff99"
local function StateTag(isOn)
return isOn and "[|cff00ff00ON|r]" or "[|cff888888OFF|r]"
end
local function SpellLink(spellID, name)
return "|cff71d5ff|Hspell:" .. spellID .. ":0|h[" .. (name or "?") .. "]|h|r(" .. spellID .. ")"
end
SLASH_CELLHEALARBITRATION1 = "/chap"
SLASH_CELLHEALARBITRATION2 = "/cellhealpred"
SlashCmdList["CELLHEALARBITRATION"] = function(msg)
if msg == "debug" then
chapDebug = not chapDebug
print(CHAP_TAG .. "Widget-hook/discovery trace: " .. (chapDebug and "ON" or "OFF"))
return
end
if msg == "diag" then
CELL_CHAD_ENABLED = not CELL_CHAD_ENABLED
print(CHAP_TAG .. "CHAD diagnostics: " .. (CELL_CHAD_ENABLED and ("ON, use " .. CMD_CHAD .. "/chad|r for stats.") or "OFF"))
return
end
print(CHAP_TAG .. "Active (LibHealComm-4.0 found). "
.. chapHookedCount .. " widgets hooked. Errors caught: " .. chapErrorCount .. ".\n" ..
CMD_CHAP .. "/chap|r (or " .. CMD_CHAP .. "/cellhealpred|r): show this command list and whether LibHealComm is active.\n" ..
CMD_CHAP .. "/chap debug|r: toggle widget-hook/discovery trace. " .. StateTag(chapDebug) .. "\n" ..
CMD_CHAP .. "/chap diag|r: toggle CHAD (" .. CMD_CHAD .. "/chad|r" .. ") diagnostics on/off. " .. StateTag(CELL_CHAD_ENABLED) .. "\n" ..
CMD_CHAD .. "/chad|r (or " .. CMD_CHAD .. "/cellhealdiag|r): show heal-arbitration diagnostic stats.\n" ..
CMD_CHAD .. "/chad reset|r: clear diagnostic counters.\n" ..
CMD_CHAD .. "/chad debug|r: toggle per-arbitration trace. " .. StateTag(chadDebug) .. "\n" ..
CMD_CHAD .. "/chad test|r: show collected blizzard/libhealcomm/actual data per target+spell.\n" ..
CMD_CHAD .. "/chad test reset|r: clear collected test data.")
end
SLASH_CELLHEALDIAG1 = "/cellhealdiag"
SLASH_CELLHEALDIAG2 = "/chad"
SlashCmdList["CELLHEALDIAG"] = function(msg)
if msg == "reset" then
CellDB.healPredictionDiag = nil
print(CHAD_TAG .. "Diagnostic reset.")
return
end
if msg == "debug" then
chadDebug = not chadDebug
local note = (chadDebug and not CELL_CHAD_ENABLED) and (" (won't print anything until CHAD is also on - use " .. CMD_CHAP .. "/chap diag|r)") or ""
print(CHAD_TAG .. "Per-arbitration trace: " .. (chadDebug and "ON" or "OFF") .. note)
return
end
if msg == "test reset" then
wipe(chapSpellData)
wipe(chapCurrentSpellByGuid)
print(CHAD_TAG .. "Test data cleared.")
return
end
if msg == "test" then
if not CELL_CHAD_ENABLED then
print(CHAD_TAG .. "Disabled - use " .. CMD_CHAP .. "/chap diag|r to enable it (data is only collected while CHAD is on).")
return
end
local function StatsText(label, values)
local n = #values
if n == 0 then return nil end
local vMin, vMax, vSum = math.huge, 0, 0
for _, v in ipairs(values) do
vMin, vMax, vSum = math.min(vMin, v), math.max(vMax, v), vSum + v
end
return string.format("%s: n=%d min=%.0f max=%.0f avg=%.0f", label, n, vMin, vMax, vSum / n)
end
local any = false
for destGUID, targetData in pairs(chapSpellData) do
for spellID, bucket in pairs(targetData.spells) do
any = true
local actualText = StatsText("actual", bucket.actualHeals) or "actual: no non-crit landed data yet"
local critText = StatsText("crits", bucket.actualCritHeals)
local arbitratedFromPeaks = AggregateValue(bucket.blizzardMax, bucket.libMax)
print(CHAD_TAG .. string.format("%s - %s predictions: blizzard=%.0f, libhealcomm=%.0f, arbitrated (%s)=%.0f | %s%s",
targetData.name or destGUID:sub(-6), SpellLink(spellID, bucket.spellName),
bucket.blizzardMax, bucket.libMax, CHAP_AGGREGATE, arbitratedFromPeaks, actualText,
critText and (" | " .. critText) or ""))
end
end
if not any then
print(CHAD_TAG .. "No data yet - cast heals and let them land.")
return
end
print(CHAD_TAG .. "Use " .. CMD_CHAD .. "/chad test reset|r before switching test conditions.")
return
end
if not CELL_CHAD_ENABLED then
print(CHAD_TAG .. "Disabled - use " .. CMD_CHAP .. "/chap diag|r to enable it.")
return
end
local diag = CellDB.healPredictionDiag
if not diag or diag.total == 0 then
print(CHAD_TAG .. "No data recorded yet.")
return
end
print(CHAD_TAG .. string.format("%d incoming heals recorded:", diag.total))
print(string.format(" Blizzard native: %d (%.1f%%)", diag.blizzardWins, diag.blizzardWins / diag.total * 100))
print(string.format(" LibHealComm: %d (%.1f%%)", diag.libHealCommWins, diag.libHealCommWins / diag.total * 100))
print(string.format(" Tied: %d (%.1f%%)", diag.tie, diag.tie / diag.total * 100))
print(CHAD_TAG .. "Use " .. CMD_CHAD .. "/chad reset|r to clear.")
end
print(CHAP_TAG .. "LibHealComm-4.0 found - heal prediction arbitration active. Type " .. CMD_CHAP .. "/chap|r (or " .. CMD_CHAP .. "/cellhealpred|r) for commands.")
end
if IsLoggedIn() then
ChapInit()
else
local chapLoginFrame = CreateFrame("Frame")
chapLoginFrame:RegisterEvent("PLAYER_LOGIN")
chapLoginFrame:SetScript("OnEvent", function(self)
self:UnregisterEvent("PLAYER_LOGIN")
ChapInit()
end)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment