local typeMap = { spell = "S", item = "I", macro = "M", companion = "C", equipmentset = "E", summonmount = "U", } local inverseTypeMap = { S = "spell", I = "item", M = "macro", C = "companion", E = "equipmentset", U = "summonmount", } ---@param setName string ---@return nil local function PickupEquipmentSet(setName) local setIndex = 0 for i = 1, C_EquipmentSet.GetNumEquipmentSets() do local sn = C_EquipmentSet.GetEquipmentSetInfo(i) if sn == setName then setIndex = i end end C_EquipmentSet.PickupEquipmentSet(setIndex) end local pickupActionButton = { item = PickupItem, spell = PickupSpell, macro = PickupMacro, companion = PickupSpell, equipmentset = PickupEquipmentSet, summonmount = C_MountJournal.Pickup, } ---@param index number ---@return nil local function ClearActionButton(index) if GetActionInfo(index) then PickupAction(index) ClearCursor() end end ---@param index number ---@param actionButton {type: string, id: number|string} ---@return boolean, number? local function RestoreActionButton(index, actionButton) ClearActionButton(index) if not actionButton then return true, nil end local aliases = ActionBarSaverDaved.spellAliases[actionButton.id] or {} table.insert(aliases, actionButton.id) for _, id in ipairs(aliases) do pickupActionButton[actionButton.type](id) if GetCursorInfo() == actionButton.type then PlaceAction(index) return true, id end ClearCursor() end return false end ---@param index number ---@return boolean local function ClearActionButton(index) if not index then return false end PickupAction(index) ClearCursor() return true end ---@param actionButton {type: string, id: number|string} ---@return boolean local function IsMacro(actionButton) return actionButton and actionButton.type == "macro" end ---@return table local function GetMacroDuplicates() local t = {} local duplicates = {} for i = 1, MAX_MACROS do local macroName = GetMacroInfo(i) if macroName then if not t[macroName] then t[macroName] = 1 else t[macroName] = t[macroName] + 1 duplicates[macroName] = t[macroName] end end end return duplicates end ---@param warnings table ---@param macroName string ---@param usages number ---@return nil local function AddWarning(warnings, macroName, usages) table.insert( warnings, string.format("Warning: Found %d macros named '%s'. Consider renaming them to avoid issues", usages, macroName) ) end ---@param setName string ---@return nil function SaveActionbarSet(setName) if not setName or setName == "" then print("Set name cannot be empty") return end local duplicates = GetMacroDuplicates() ---@type table local set = {} ---@type string[] local warnings = {} for i = 1, MAX_ACTION_BUTTONS do local type, id = GetActionInfo(i) if type == "macro" then -- use macro name as the ID id = GetMacroInfo(id) if duplicates[id] then AddWarning(warnings, id, duplicates[id]) end end if type and id then set[i] = type and { type = type, id = id, } end end ActionBarSaverDaved.sets[setName] = set print(string.format("Saved set '%s'!", setName)) for _, warning in ipairs(warnings) do print(warning) end end ---@param setName string ---@return nil function RestoreActionbarSet(setName) if not setName or setName == "" then print("Set name cannot be empty") return end local set = ActionBarSaverDaved.sets[setName] if not set then print(string.format("No set with the name '%s' exists", setName)) return end if InCombatLockdown() then print("Cannot restore sets while in combat") return end local duplicates = GetMacroDuplicates() local messages = {} -- Start with an empty cursor ClearCursor() for i = 1, MAX_ACTION_BUTTONS do local actionButton = set[i] ClearActionButton(i) if actionButton then if IsMacro(actionButton) and duplicates[actionButton.id] then ---@cast actionButton {type: string, id: string} AddWarning(messages, actionButton.id, duplicates[actionButton.id]) end local succeeded, restoredID = RestoreActionButton(i, actionButton) if not succeeded or not restoredID then table.insert( messages, string.format( "Error: Unable to restore %s with id [%s] to slot %d", actionButton.type, actionButton.id or "", i ) ) elseif actionButton and restoredID ~= actionButton.id then table.insert( messages, string.format( "Info: Restored spell %d (%s) in place of spell %d", restoredID, GetSpellInfo(restoredID), actionButton.id ) ) end else ClearActionButton(i) end end print(string.format("Restored set '%s'", setName)) for _, warning in ipairs(messages) do print(warning) end end ---@param setName string ---@return nil function DeleteActionbarSet(setName) if not setName or setName == "" then print("Set name cannot be empty") return end if not ActionBarSaverDaved.sets[setName] then print(string.format("No set with the name '%s' exists", setName)) return end ActionBarSaverDaved.sets[setName] = nil print(string.format("Deleted set '%s'", setName)) end ---@return nil function ListActionbarSets() local sets = {} for setName, foo in pairs(ActionBarSaverDaved.sets) do sets[#sets + 1] = setName end table.sort(sets) local setsStr = table.concat(sets, ", ") print(not (not setsStr or setsStr == "") and setsStr or "No sets found") end ---@param of string ---@param to string ---@return nil function AliasSpell(of, to) if not of or not to then print("Must provide args in the format 'spellID aliasID'") return end local ofId = tonumber(of) local toId = tonumber(to) if not (ofId and toId) then print(string.format("Could not parse %s and/or %s to numbers", of, to)) return end local aliases = ActionBarSaverDaved.spellAliases[ofId] or {} for _, id in ipairs(aliases) do if id == toId then print(string.format("Spell %d is already aliased by %d", ofId, toId)) return end end table.insert(ActionBarSaverDaved.spellAliases[ofId], toId) print(string.format("Added %d as an alias for %d", toId, ofId)) end ---@param of string ---@return nil function DeleteSpellAliases(of) if not of then print("Must provide a valid spellID") return end local ofId = tonumber(of) if not ofId then print(string.format("Could not parse spellID from '%s'", of)) return end if not ActionBarSaverDaved.spellAliases[ofId] then print(string.format("No aliases to remove for spell with ID %d", ofId)) return end ActionBarSaverDaved.spellAliases[ofId] = nil print(string.format("Removed all aliases for spell with ID %d", ofId)) end ---@return nil function ListAliases() for spellID, spellAliases in pairs(ActionBarSaverDaved.spellAliases) do print(string.format("Spell %d is aliased by: %s", spellID, table.concat(spellAliases, ", "))) end end local importingSet = nil local importExportFrame = CreateFrame("Frame", "ABSImportExportFrame", UIParent) importExportFrame:SetSize(512, 512) importExportFrame:SetPoint("CENTER") importExportFrame:SetFrameStrata("HIGH") importExportFrame:EnableMouse(true) importExportFrame:SetMovable(true) importExportFrame:SetResizable(false) importExportFrame:SetBackdrop({ bgFile = "Interface/Tooltips/UI-Tooltip-Background", edgeFile = "Interface/Tooltips/UI-Tooltip-Border", tile = true, tileSize = 4, edgeSize = 4, insets = { left = 4, right = 4, top = 4, bottom = 4, }, }) importExportFrame:SetBackdropColor(0, 0, 0, 0.8) importExportFrame:SetBackdropBorderColor(0.5, 0.5, 0.5, 1) importExportFrame:SetMovable(true) importExportFrame:EnableMouse(true) importExportFrame:RegisterForDrag("LeftButton") importExportFrame:SetScript("OnDragStart", function(self) self:StartMoving() end) importExportFrame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end) importExportFrame:SetScript("OnShow", function(self) self:SetScale(1) end) importExportFrame:Hide() local importExportFrameTextBox = CreateFrame("EditBox", "ABSImportExportFrameTextBox", importExportFrame) importExportFrameTextBox:SetSize(512, 512) importExportFrameTextBox:SetPoint("TOPLEFT", importExportFrame, "TOPLEFT", 0, 0) importExportFrameTextBox:SetFont("Fonts\\FRIZQT__.ttf", 12) importExportFrameTextBox:SetTextColor(1, 1, 1, 1) importExportFrameTextBox:SetTextInsets(20, 20, 20, 20) importExportFrameTextBox:SetMultiLine(true) importExportFrameTextBox:SetAutoFocus(true) importExportFrameTextBox:SetMaxLetters(1000000) importExportFrameTextBox:SetScript("OnEscapePressed", function(self) importExportFrame:Hide() if importingSet then local lines = { strsplit("\n", self:GetText()) } for _, line in ipairs(lines) do line = strtrim(line) if line ~= "" then ImportActionbarSet(importingSet, line) end end importingSet = nil end end) ---@param setName string ---@return nil function ExportActionbarSet(setName) local set = ActionBarSaverDaved.sets[setName] if not set then print(string.format("No set with the name '%s' exists", setName)) return end local macros = {} local stringified = {} for slot, action in pairs(set) do local typeChar = typeMap[action.type] if not typeChar then print(string.format("Unknown action type '%s' in set '%s'", action.type, setName)) return end stringified[#stringified + 1] = string.format("%s\\%s\\%s", tostring(slot), tostring(action.id), tostring(typeChar)) if typeChar == "M" then local _, _, macro = GetMacroInfo(action.id) if macro then macros[action.id] = macro end end end local export = {} for name, macro in pairs(macros) do local content = B64.Encode(macro) export[#export + 1] = string.format("Mž%sž%s", name, content) end export[#export + 1] = table.concat(stringified, "ž") local str = table.concat(export, "\n") importExportFrame:Show() importExportFrameTextBox:SetText(str) importExportFrameTextBox:SetFocus() end ---@param action {slot: number, id: number|string, type: string} ---@return string function FormatAction(action) return string.format("slot: %d, id: %s, type: %s", action.slot, action.id, action.type) end ---@param action string ---@return {slot: number, id: number|string, type: string} function ParseAction(action) local ret = { slot = 0, id = 0, type = "", } if not action or action == "" then return ret end action = strtrim(action) ---@type string, string, string local slot, id, typeChar = string.match(action, "([^\\]+)\\([^\\]+)\\([^\\]+)") if not typeChar then print(string.format("Unknown action type '%s' for action '%s'", tostring(typeChar), FormatAction(ret))) return ret end local type = inverseTypeMap[typeChar] if not type then print(string.format("Unknown action type '%s' for action '%s'", tostring(typeChar), FormatAction(ret))) return ret end local slotNum = tonumber(slot) if not slotNum then print(string.format("Unknown slot '%s' for action '%s'", tostring(slot), FormatAction(ret))) return ret end if not id then print(string.format("Unknown id '%s' for action '%s'", tostring(id), FormatAction(ret))) return ret end return { slot = slotNum, id = id, type = type, } end ---@param importString string ---@return nil function ImportMacro(importString) if not importString or importString == "" then print("Must provide a valid macro string") return end importString = strtrim(importString) local name, content = string.match(importString, "^Mž([^ž]+)ž([^ž]+)") if not name or not content then print("Error: Invalid macro part format") return end content = strtrim(content) name = strtrim(name) local reconstructed = B64.Decode(content) local macroIdx = GetMacroIndexByName(name) if macroIdx == 0 then CreateMacro(name, "Inv_misc_questionmark", "") macroIdx = GetMacroIndexByName(name) end EditMacro(macroIdx, name, nil, reconstructed) print(string.format("Imported macro '%s' with index %d and content '%s'", name, macroIdx, reconstructed)) end ---@param setName string ---@param str string ---@return nil function ImportActionbarSet(setName, str) if not setName or setName == "" then print("Must provide a valid set name") return end if string.find(str, "^Mž") then ImportMacro(str) return end local set = ActionBarSaverDaved.sets[setName] or {} -- if set then -- print(string.format("Set '%s' already exists", setName)) -- return -- end str = strtrim(str) local data = { strsplit("ž", str) } for _, action in ipairs(data) do local paction = ParseAction(action) if paction then set[paction.slot] = { type = paction.type, id = paction.id, } end end ActionBarSaverDaved.sets[setName] = set print(string.format("Imported set '%s'", setName)) end ---@param setName string ---@return nil function ImportActionbarSetDialogue(setName) if not setName or setName == "" then print("Must provide a valid set name") return end importingSet = setName importExportFrameTextBox:SetText("") importExportFrame:Show() importExportFrameTextBox:SetFocus() end ---@return nil function PrintActionbarUsage() print("ABS Slash commands") print("/abs save - Saves your current action bar setup under the given ") print("/abs restore - Restores the saved ") print("/abs delete - Deletes the saved ") print("/abs list - Lists all saved sets") print("/abs alias - Adds an alias with to ") print("/abs unalias - Removes all aliases associated with ") print("/abs aliases - List all spell aliases") print("/abs export - Brings up a dialog to export the given ") print("/abs import - Brings up a dialog to import the given ") end SlashCmdList["ABS"] = function(argv) local args = { strsplit(" ", argv) } local cmd = args[1] if cmd == "save" then SaveActionbarSet(args[2]) end if cmd == "restore" then RestoreActionbarSet(args[2]) end if cmd == "delete" then DeleteActionbarSet(args[2]) end if cmd == "list" then ListActionbarSets() end if cmd == "alias" then AliasSpell(args[2], args[3]) end if cmd == "unalias" then DeleteSpellAliases(args[2]) end if cmd == "aliases" then ListAliases() end if cmd == "export" then ExportActionbarSet(args[2]) end if cmd == "import" then ImportActionbarSetDialogue(args[2]) end if cmd == "" or not cmd then PrintActionbarUsage() end end SLASH_ABS1 = "/abs"