Avoid "Interface action failed because of an AddOn": queue work until combat ends, let the game show and hide frames during a fight, and run code after Blizzard's functions without tainting them.
In combat the game locks protected frames. Until combat ends, an addon may not show, hide or move them, or change a secure button's attributes. If it tries, the player sees "Interface action failed because of an AddOn". These three modules help you work within that lockdown.
Wait for combat to end
Put @outOfCombat on a function that touches protected frames. Out of
combat it runs at once. In combat the call is queued, and runs as soon as
the fight ends (PLAYER_REGEN_ENABLED). If it is called ten times during a
fight, it runs once, with the last arguments.
import outOfCombat from "wowlua:combat" class ActionBar frame: Frame offset: number = 0 constructor(frame: Frame) self.frame = frame end @outOfCombat function relayout() self.frame:ClearAllPoints() self.frame:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, self.offset) endend export const bar = ActionBar.new(CreateFrame("Frame", nil, UIParent, "SecureHandlerStateTemplate")) -- the settings slider can call this at any time, in combat or notexport function onOffsetChanged(value: number) bar.offset = value bar:relayout()endFor a method, calls are collapsed per object. The function must not return anything (E8030), because a queued call has no result to give back.
For a one-off, defer does the same with a function you pass. Give it a key,
and only the latest function queued with that key runs:
import defer, inCombat, onCombatChange from "wowlua:combat"import print from "wowlua:log" local castButton = CreateFrame("Button", nil, UIParent, "SecureActionButtonTemplate")local statusIcon = UIParent:CreateTexture(nil, "OVERLAY") -- typing in a box during combat: only the final name is setexport function setSpell(spellName: string) defer(function() castButton:SetAttribute("spell", spellName) end, "castButton:spell") if inCombat() then print("Changes apply after combat.") endend onCombatChange(function(entering: boolean) statusIcon:SetShown(entering)end)Queued calls run in the order they were first queued. If combat starts again while the queue runs, the rest waits for the next end of combat. A queued function that raises an error is reported, and the others still run.
| Function | What it does |
|---|---|
@outOfCombat |
Runs now, or after combat; repeated calls collapse into one |
defer(fn, key?) |
The same for a function you pass |
inCombat() |
Whether protected calls are blocked right now |
onCombatChange(fn) |
Calls fn with true when combat starts and false when it ends |
Show and hide frames in combat
Your code can't show a protected frame in combat, but the game can, if you give it the rule up front. A state driver is a macro conditional that the game evaluates itself, also in the middle of a fight.
import * as secure from "wowlua:secure"import bar from "./bars" local pageLabel = bar.frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") -- hidden in pet battles and vehicles, shown in combat, hidden otherwisesecure.visibility(bar.frame, "[petbattle][vehicleui] hide; [combat] show; hide") -- a page that follows the bonus bar and Shift, with a label that follows itsecure.state(bar.frame, "page", "[bonusbar:5] 11; [mod:shift] 2; 1", function(page: string) pageLabel:SetText(page)end) if secure.evaluate("[group:raid] yes") then print("in a raid")endA literal conditional is checked when you build. An unpaired bracket or a
condition the client doesn't know, such as [combta], is a build error
(E8440), not a bar that silently never shows. A visibility value other
than show or hide is E8441.
- Registering or removing a driver in combat waits for the end of combat by itself, and the drivers keep working through every fight.
statecalls your function with each new value, the first one right away. It runs as ordinary addon code, so in combat it may only change frames that aren't protected. Cancelling the subscription it returns removes the driver.secure.visibility(frame, nil)removes the driver and leaves the frame as it is.evaluatereturns what the conditional gives right now, or nil when no clause matches.
A click only a secure button may do
Some actions, such as running a slash command or clicking a protected
button, only work from a click on a secure button. secure.overlay lays a
secure button over a button of your own, so yours can stay an ordinary
frame. Make it out of combat:
import * as secure from "wowlua:secure" local myButton = CreateFrame("Button", nil, UIParent, "UIPanelButtonTemplate") local overlay = secure.overlay(myButton)overlay.set("type1", "macro")overlay.set("macrotext1", "/click ExtraActionButton1")overlay.show() -- false in combatset, show and hide return false in combat, where they can't work.
show(other) moves the overlay over another frame instead, such as the row
of a list. overlay.button is the secure button, for PreClick and
PostClick hooks.
| Function | What it does |
|---|---|
secure.visibility(frame, conditional) |
Shows and hides a frame by macro conditions |
secure.state(frame, name, conditional, fn?) |
A named state, with a function called on each change |
secure.evaluate(conditional) |
What a conditional gives right now |
secure.overlay(target, opts?) |
A secure button over one of yours |
Run after Blizzard's functions
@hook runs your function after a global function or a method, as
hooksecurefunc does, without replacing it, so the original stays
untainted. Your function's parameters are checked against those of the
hooked function, so you get its arguments typed.
import hook, hookScript from "wowlua:hooks" @hook("ToggleGameMenu")local function afterGameMenu() print("game menu toggled")end @hook(GameTooltip, "SetUnit")local function afterSetUnit(tip: GameTooltip) tip:AddLine("seen by MyAddon")end @hookScript(PlayerFrame, "OnShow")local function onPlayerFrameShow(frame: Frame) frame:SetAlpha(0.9)endA target is a table or the name of a global, as a string; a string is looked up when the hook is installed.
Hooking a load-on-demand addon
Much of Blizzard's interface only loads when the player first opens it, so the frame you want to hook doesn't exist at login. Name the addon, and the hook waits for it:
import hook from "wowlua:hooks" @hook("PlayerSpellsFrame", "Show", { addon = "Blizzard_PlayerSpells" })local function afterSpellbookShow(frame: Frame) print("spellbook opened", frame:GetName())endIf you leave out addon for a target that lives in such an addon, wowlua
warns you when you build (W8102). Hooking a name the declarations don't know
is warning W8040.
Without decorators, hook(target, name, fn) and hookScript(frame, script, fn) do the same as plain calls, and the function you write in the call gets
its parameter types from the target. { owner = … } in the options is handed
to error listeners when the hook fails (see
wowlua:errors).