commit 19a7052f75f672083b69030ee670690de1e56384 Author: PhatPhuckDave Date: Sat Jan 4 13:47:04 2025 +0100 Initial commit diff --git a/ActionBarSaverReloaded.toc b/ActionBarSaverReloaded.toc new file mode 100644 index 0000000..c14dae6 --- /dev/null +++ b/ActionBarSaverReloaded.toc @@ -0,0 +1,12 @@ +## Interface: 40400 +## Title: ActionBarSaver: Reloaded +## Version: 1.0.7 +## Notes: Manage, save, and restore action bar profiles +## Author: Voodoomoose +## SavedVariables: ActionBarSaverReloaded + +Constants.lua +StringHelpers.lua +TableHelpers.lua +Main.lua +Actions.lua \ No newline at end of file diff --git a/Actions.lua b/Actions.lua new file mode 100644 index 0000000..25d485b --- /dev/null +++ b/Actions.lua @@ -0,0 +1,277 @@ +local ADDON_LOADED, shared = ... + +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, +} + +local function RestoreActionButton(self, index, actionButton) + -- Clear the slot + if GetActionInfo(index) then + PickupAction(index) + ClearCursor() + end + + if not actionButton then return true, nil end + + local aliases = self.db.class.spellAliases[actionButton.id] or {} + local ids = Array.insert(aliases, actionButton.id, 1) + + for _, id in ipairs(ids) do + pickupActionButton[actionButton.type](id) + + if GetCursorInfo() == actionButton.type then + PlaceAction(index) + return true, id + end + + ClearCursor() + end + + return false +end + +local function IsMacro(actionButton) return actionButton and actionButton.type == "macro" end + +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 + +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 + +function SaveSet(setName) + if Str.nullOrEmpty(setName) then + self:Print("Set name cannot be empty") + return + end + + local duplicates = GetMacroDuplicates() + local set = {} + 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 + + self.db.class.sets[setName] = set + self:Print(string.format("Saved set '%s'!", setName)) + Array.iter(warnings, function(warning) self:Print(warning) end) +end + +function RestoreSet(setName) + if Str.nullOrEmpty(setName) then + self:Print("Set name cannot be empty") + return + end + + local set = self.db.class.sets[setName] + + if not set then + self:Print(string.format("No set with the name '%s' exists", setName)) + return + end + if InCombatLockdown() then + self: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] + + if IsMacro(actionButton) and duplicates[actionButton.id] then AddWarning(messages, actionButton.id, duplicates[actionButton.id]) end + + local succeeded, restoredID = RestoreActionButton(self, i, actionButton) + if not succeeded 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 + end + + self:Print(string.format("Restored set '%s'", setName)) + Array.iter(messages, function(warning) self:Print(warning) end) +end + +function DeleteSet(setName) + if Str.nullOrEmpty(setName) then + self:Print("Set name cannot be empty") + return + end + + if not self.db.class.sets[setName] then + self:Print(string.format("No set with the name '%s' exists", setName)) + return + end + + self.db.class.sets[setName] = nil + + self:Print(string.format("Deleted set '%s'", setName)) +end + +function ListSets() + local sets = Dict.keysAsArray(self.db.class.sets) + table.sort(sets) + local setsStr = table.concat(sets, ", ") + + self:Print(not Str.nullOrEmpty(setsStr) and setsStr or "No sets found") +end + +function AliasSpell(args) + if Str.nullOrEmpty(args) then + self:Print("Must provide args in the format 'spellID aliasID'") + return + end + local spellID, aliasID = string.match(args, "(%d+)%s+(%d+)") + + spellID = tonumber(spellID) + aliasID = tonumber(aliasID) + + if not (spellID and aliasID) then + self:Print(string.format("Could not parse spellID and aliasID from '%s'", args)) + return + end + + local aliases = self.db.class.spellAliases[spellID] or {} + + if Array.contains(aliases, aliasID) then + self:Print(string.format("Spell %d is already aliased by %d", spellID, aliasID)) + return + end + + table.insert(aliases, aliasID) + self.db.class.spellAliases[spellID] = aliases + + self:Print(string.format("Added %d as an alias for %d", aliasID, spellID)) +end + +function DeleteSpellAliases(spellID) + if Str.nullOrEmpty(spellID) then + self:Print("Must provide a valid spellID") + return + end + + spellID = tonumber(spellID) + + if not self.db.class.spellAliases[spellID] then + self:Print(string.format("No aliases to remove for spell with ID %d", spellID)) + return + end + + self.db.class.spellAliases[spellID] = nil + + self:Print(string.format("Removed all aliases for spell with ID %d", spellID)) +end + +function ListAliases() + local aliases = self.db.class.spellAliases + + if Dict.isEmpty(aliases) then + self:Print("No aliases found") + return + end + + Dict.iter(self.db.class.spellAliases, function(spellID, aliases) + self:Print(string.format("Spell %d is aliased by: %s", spellID, table.concat(aliases, ", "))) + end) +end + +function PrintUsage() + self:Print("ABS Slash commands") + self:Print("/abs save - Saves your current action bar setup under the given ") + self:Print("/abs restore - Restores the saved ") + self:Print("/abs delete - Deletes the saved ") + self:Print("/abs list - Lists all saved sets") + self:Print("/abs alias - Adds an alias with to ") + self:Print("/abs unalias - Removes all aliases associated with ") + self:Print("/abs aliases - List all spell aliases") +end + +SlashCmdList["ABS"] = function(argv) + local args = strsplit(" ", argv) + local cmd = args[1] + + if cmd == "save" then + SaveSet(args[2]) + end + if cmd == "restore" then + RestoreSet(args[2]) + end + if cmd == "delete" then + DeleteSet(args[2]) + end + if cmd == "list" then + ListSets() + 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 == "" then + PrintUsage() + end +end +SLASH_ABS1 = "/abs" diff --git a/Main.lua b/Main.lua new file mode 100644 index 0000000..d2a6be1 --- /dev/null +++ b/Main.lua @@ -0,0 +1,30 @@ +local ADDON_NAME, shared = ... + +local frame = CreateFrame("Frame") +frame:RegisterEvent("ADDON_LOADED") +frame:SetScript("OnEvent", function(self, event, addon) + if addon ~= ADDON_NAME then return end + + ActionBarSaverReloaded = ActionBarSaverReloaded or {} + ActionBarSaverReloaded.spellAliases = ActionBarSaverReloaded.spellAliases or {} + ActionBarSaverReloaded.sets = ActionBarSaverReloaded.sets or {} +end) + +--function ABS:OnInitialize() +-- self.commands = { +-- save = self.actions.SaveSet, +-- restore = self.actions.RestoreSet, +-- delete = self.actions.DeleteSet, +-- list = self.actions.ListSets, +-- alias = self.actions.AliasSpell, +-- unalias = self.actions.DeleteSpellAliases, +-- aliases = self.actions.ListAliases, +-- } +--end +-- +--function ABS:HandleCommands(input) +-- local cmd, args = Str.split(input, " ", 2) +-- local fn = self.commands[Str.toLower(cmd)] +-- +-- if fn then fn(self, args) else self.actions.PrintUsage(self) end +--end \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..14e321d --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# ActionBarSaver:Reloaded + +## Overview + +ActionBarSaver:Reloaded is an addon for saving and restoring action bar profiles. It is based on the original ActionBarSaver addon but is a full re-write. + +All sets are saved by class rather than by character. Additionally, when you list profiles, you will only see profiles that pertain to your class. + +Features such as rename have been deleted for simplicity. To perform a rename, simply save the set with a new name and delete the old set. Additionally, restoring a set will no longer try to re-create macros that do not exist. It will simply notify you of the missing macro and restore nothing for that slot. ABS:R will not work properly if you have multiple macros with the same name, and will warn you of potential issues if you restore a set that has a macro with a shared name. + +A new feature has been added for setting up aliases for spells. A common use case for this would be restoring a single set for two characters that share a class but have a different race. For example, you could create a set on a troll shaman that contains `Berserking` and then add an alias for `War Stomp` using `/abs alias 20554 20549`. If you do this, when you restore a set it will first try to restore the proper spell but will also try each alias that you set up. A spell can have as many aliases as you want. + +## Usage + +`/abs save ` - Saves your current action bar setup under the given \ +`/abs restore ` - Restores the saved \ +`/abs delete ` - Deletes the saved \ +`/abs list` - Lists all saved sets\ +`/abs alias ` - Adds an alias with to \ +`/abs unalias ` - Removes all aliases associated with \ +`/abs aliases` - List all spell aliases + +## Known Issues + +* Aliases do not work both ways. If you alias `Berserking` as `War Stomp` and then save a set that contains `Berserking`, it will work properly if you restore that set on a tauren. However, if you save a set that contains `War Stomp` and try to restore it on a troll, it will fail. This will be addressed in a future version. \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..62af3fc --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,31 @@ +## 1.0.7 (2024-06-30) + +- Updated for Cata classic + +## 1.0.6 (2023-08-29) + +- TOC bump for ICC patch + +## 1.0.5 (2023-06-20) + +- TOC bump for ToGC patch + +## 1.0.4 (2023-01-18) + +- TOC bump for WOTLK patch + +## 1.0.3 (2022-11-09) + +- Fixed bug with equipment sets not being restored properly + +## 1.0.2 (2022-10-27) + +- Fixed bug where totem sets were not being saved or restored + +## 1.0.1 (2022-09-26) + +- Fixed bug related to restoring sets containing companions and mounts + +## 1.0.0 (2022-07-15) + +- Initial re-write of the addon diff --git a/StringHelpers.lua b/StringHelpers.lua new file mode 100644 index 0000000..18b0e15 --- /dev/null +++ b/StringHelpers.lua @@ -0,0 +1,9 @@ +Str = {} + +function Str.toLower(s) return string.lower(s or "") end +function Str.nullOrEmpty(s) return not s or s == "" end + +function Str.split(s, delimiter, max) +---@diagnostic disable-next-line: undefined-field + return string.split(delimiter, s, max) +end \ No newline at end of file diff --git a/TableHelpers.lua b/TableHelpers.lua new file mode 100644 index 0000000..01b80ac --- /dev/null +++ b/TableHelpers.lua @@ -0,0 +1,84 @@ +Array = {} +Dict = {} + +function Array.iter(arr, fn) + for _,v in ipairs(arr) do + fn(v) + end +end + +function Array.map(arr, fn) + local t = {} + + for _,v in ipairs(arr) do + table.insert(t, fn(v)) + end + + return t +end + +function Array.mapRange(start, stop, fn) + local arr = {} + + for i = start, stop do + arr[i] = fn(i) + end + + return arr +end + +function Array.iterRange(start, stop, fn) + for i = start, stop do + fn(i) + end +end + +function Array.contains(arr, value) + for _,v in ipairs(arr) do + if v == value then return true end + end + + return false +end + +function Array.insert(arr, value, index) + local t = {} + + for _, v in ipairs(arr) do + table.insert(t, v) + end + + table.insert(t, index, value) + + return t +end + +function Dict.iter(t, fn) + for k,v in pairs(t) do + fn(k,v) + end +end + +function Dict.map(t, fn) + local nt = {} + + for k,v in pairs(t) do + nt[k] = fn(k,v) + end + + return nt +end + +function Dict.keysAsArray(t) + local nt = {} + + for k,_ in pairs(t) do + table.insert(nt, k) + end + + return nt +end + +function Dict.isEmpty(t) + return next(t) == nil +end \ No newline at end of file diff --git a/constants.lua b/constants.lua new file mode 100644 index 0000000..5a48aaf --- /dev/null +++ b/constants.lua @@ -0,0 +1,3 @@ +ADDON_NAME = "ActionBarSaverReloaded" +MAX_MACROS = 138 +MAX_ACTION_BUTTONS = 132 \ No newline at end of file