Skip to content

Dynamic Macros

You no longer need to pre-fill macro names and bodies. Use SuperDPS.SetMacro in game-side initialization code, then call Macro by slot or name in desktop logic. A validated registry connects the two environments.

Minimal example: generate a localized spell macro

Section titled “Minimal example: generate a localized spell macro”

Keep a valid hotkey for slot 1 in the editor; its name and body may be empty. Paste this into Initialization Code. Spell ID 1766 is an example; replace it with a spell appropriate to your character. Save, generate the addon and /reload in game.

local spellID = 1766
local name = C_Spell and C_Spell.GetSpellName
and SuperDPS.SN(C_Spell.GetSpellName(spellID), "macro.name")
if type(name) == "string" and name ~= "" then
local body = "/cast " .. name
SuperDPS.SetMacro(1, "interrupt", body)
end

Use one call in desktop logic: the following uses the name; replace it with Macro(1) to use the slot. Do not call both forms consecutively just to demonstrate them.

local submitted = Macro("interrupt")
Interface Rule
SuperDPS.SetMacro(slot, name, body) Game-side only. A new or updated request returns false, "pending"; identical, applied content returns true, "ready". Invalid arguments raise an error without changing the registry.
Macro(slotOrName) Desktop logic only. Resolves an integer slot or exact name and returns a boolean. Submitting a key does not confirm a successful cast.
1..105 Dynamic slots. Slots 106–150 and reload/SelectTarget names are reserved. Invalid, explicitly empty or conflicting hotkeys cannot be registered.
name / body Names are 1–96 bytes of valid UTF-8, not whitespace-only and without control characters. Names must be unique across slots. Bodies are nonempty and contain no NUL; use Lua \n or table.concat for multiple lines.
SetMacro(1, ...) Replaces the name and body at the same slot while preserving its hotkey; the old name stops resolving. Changes last for the game session and do not rewrite the editor or .wa1. Reload runs initialization again.

Keep initialization in a function and refresh it through events. The API keeps the latest request for each slot; during combat it queues requests without modifying secure buttons. This example shows a multiline body and an out-of-combat refresh; slot 2 also needs a valid hotkey.

local function refresh()
if InCombatLockdown() then return end
local body = table.concat({"/say ready", "/startattack"}, "\n")
SuperDPS.SetMacro(2, "start", body)
end
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:SetScript("OnEvent", refresh)
refresh()

Updates mark the slot unavailable, wait at least 250ms and for combat to end, then write the button and read back its body and effective binding. Macro/Cast calls pause until the registry has fully synchronized. Binding changes invalidate readiness and queue a rebind; submit the same request again to retry a failed write. Longer names and larger registries take longer to synchronize, so prefer short, stable names.

Complete example: minimal Assisted Combat module

Section titled “Complete example: minimal Assisted Combat module”

Initialization reads the official rotation candidates, deduplicates base spell IDs, allocates stable slots and generates macros with localized spell names. One AssistMacro property returns the slot of the current recommendation; one logic script calls it. There are no extra mounted, chat, death or range pause conditions. Missing recommendations, unknown spells or invalid data return 0 and do not dispatch a key.

Download the complete .wa1 example · Download initialization Lua. Import and bind the official logic. Macro names and bodies remain empty in the editor, with template hotkeys. The example requires retail C_AssistedCombat and does nothing when the API is absent.

Name the custom property AssistMacro and use:

return SuperDPS.AssistMacro()

Logic code:

local slot = Prop("AssistMacro")
if slot > 0 then Macro(slot) end
Show complete initialization code
-- 事件合并读取候选;只在脱战时更新变化的宏,保留仍在使用的槽位。
local slots, active, entries = {}, {}, {}
local dirty, queued, notified, announce = true, false, false, false
local checks, count = 0, 0
local schedule
-- 只扫描一次预设;缺少占用信息时拒绝猜测,避免旧客户端覆盖只有正文的宏。
local firstSlot, capacity, available, reservedNames = 1, 0, {}, {}
local compatible = true
for slot, row in pairs(SuperDPS.DynamicSlots) do
if row.name and row.name ~= "" then reservedNames[row.name] = true end
if slot >= 1 and slot <= 105 then
if type(row.occupied) ~= "boolean" then compatible = false end
if row.occupied then firstSlot = math.max(firstSlot, slot + 1) end
end
end
for slot = firstSlot, 105 do
local row = SuperDPS.DynamicSlots[slot]
if row and row.occupied == false and row.key and row.key ~= "" then
available[slot], capacity = true, capacity + 1
end
end
local function macroName(name)
while reservedNames[name] do name = name .. "_" end
return name
end
local function baseSpell(id)
id = SuperDPS.SN(id, "assist.spell")
if type(id) ~= "number" or id <= 0 then return 0 end
local base = SuperDPS.SN(C_Spell.GetBaseSpell(id), "assist.base")
return type(base) == "number" and base > 0 and base or 0
end
local function request(slot, name, body)
local entry = entries[slot]
if entry and entry.name == name and entry.body == body then return end
entries[slot] = {name = name, body = body, sentName = entry and entry.sentName}
announce = true
end
local function refresh()
if InCombatLockdown() then return end
if not compatible then
error("请更新客户端并重新生成插件:缺少预设宏占用信息")
end
if dirty then
if not (C_AssistedCombat and C_AssistedCombat.GetRotationSpells
and C_Spell and C_Spell.GetBaseSpell and C_Spell.GetSpellName) then
active = {}
return
end
local wanted, order, used, nextSlots = {}, {}, {}, {}
for _, id in ipairs(C_AssistedCombat.GetRotationSpells()) do
local base = baseSpell(id)
local name = base > 0 and SuperDPS.SN(C_Spell.GetSpellName(base), "assist.name")
if type(name) == "string" and name ~= "" and not wanted[base] then
wanted[base] = "/cast " .. name
order[#order + 1] = base
end
end
if #order > capacity then
active = {}
error("辅助宏槽位不足:需要" .. #order .. "个,预设宏之后可用" .. capacity .. "个")
end
-- 先保留仍在使用的槽位,再回收移出的技能;旧宏改为无动作正文。
for base, slot in pairs(slots) do
if wanted[base] then
nextSlots[base], used[slot] = slot, true
else
request(slot, macroName("assist_empty_" .. slot), "/stopmacro")
end
end
local free = firstSlot
for _, base in ipairs(order) do
local slot = nextSlots[base]
if not slot then
while not available[free] or used[free] do free = free + 1 end
slot = free
nextSlots[base], used[slot] = slot, true
end
request(slot, macroName("a" .. base), wanted[base])
end
slots, count, active = nextSlots, #order, {}
dirty, checks = false, 0
end
-- 先释放已提交的旧名称,避免连续切换列表时跨槽位名称冲突。
for slot, entry in pairs(entries) do
if entry.sentName and entry.sentName ~= entry.name then
if InCombatLockdown() then return end
local emptyName = macroName("assist_empty_" .. slot)
SuperDPS.SetMacro(slot, emptyName, "/stopmacro")
entry.sentName = emptyName
end
end
local ready = true
for slot, entry in pairs(entries) do
if not entry.ready then
if InCombatLockdown() then return end
entry.ready = SuperDPS.SetMacro(slot, entry.name, entry.body)
entry.sentName = entry.name
ready = entry.ready and ready
end
end
-- 仅公布已写入的宏;客户端仍通过完整快照和新鲜度校验后执行。
active = {}
for base, slot in pairs(slots) do
if entries[slot].ready then active[base] = slot end
end
if ready then
if announce and (notified or count > 0) then
local label = notified and "辅助宏更新完毕" or "辅助宏初始化完毕"
SuperDPS.Print(label .. "(" .. count .. " 个),请等待客户端同步")
notified = true
end
announce = false
elseif checks < 20 then
checks = checks + 1
schedule(0.3)
end
end
schedule = function(delay)
if queued or InCombatLockdown() then return end
queued = true
C_Timer.After(delay or 0.1, function()
queued = false
local ok, err = pcall(refresh)
if not ok then
active, dirty = {}, true
SuperDPS.Print("辅助宏更新失败:" .. tostring(err))
end
end)
end
SuperDPS.AssistMacro = function()
if not (C_AssistedCombat and C_AssistedCombat.GetNextCastSpell
and C_Spell and C_Spell.GetBaseSpell) then return 0 end
return active[baseSpell(C_AssistedCombat.GetNextCastSpell())] or 0
end
local frame = CreateFrame("Frame")
for _, event in ipairs({"PLAYER_LOGIN", "PLAYER_ENTERING_WORLD", "SPELLS_CHANGED",
"PLAYER_SPECIALIZATION_CHANGED", "PLAYER_REGEN_ENABLED"}) do
frame:RegisterEvent(event)
end
frame:SetScript("OnEvent", function(_, event, unit)
if event ~= "PLAYER_SPECIALIZATION_CHANGED" or unit == "player" then
dirty, checks = true, 0
schedule()
end
end)
schedule()

Preset macros are preserved. Assisted macros append after the last preset slot, skipping invalid or conflicting hotkeys. Only changed assisted macros are updated out of combat; their slots can be reused. Insufficient remaining capacity is reported. This example requires the generator occupied flag, available in client 20260913.120630 or later. The example passed import/export, cloud round-trip, Lua 5.1 and cross-runtime dispatch tests; in-game acceptance is still pending. Code tests are not a measurement of live casting success.