From 0c1608bcc8ae5a53a2048a15d4424849af5e1945 Mon Sep 17 00:00:00 2001 From: Torchlite Date: Tue, 27 Jan 2026 11:45:34 -0500 Subject: [PATCH 1/4] Revise README for clarity and updates Updated macro slot information, improved clarity on installation and uninstallation processes, and enhanced acknowledgments. --- README.md | 102 +++++++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index afb6e2a..db623ad 100644 --- a/README.md +++ b/README.md @@ -12,40 +12,38 @@ Once macro import is complete, `/m` will only show the new macro UI. ### More macro slots! -This AddOn seamlessly provides you with a massive increase to macro slots over the native amounts of -120 global and 18 per-character slots that the Blizzard UI provides. How does it do this? By segmenting -the 138 total macro slots into 5 categories and hot-switching macros based on your Class and Specialization. +This AddOn seamlessly provides you with a massive increase to macro slots over the native amounts (120 global +and 30 per-character slots). How does it do this? By segmenting the total macro slots into categories and +hot-switching macros based on your Class and Specialization. The breakdown of macro slots is as follows: - 1. 60 global macros 2. 30 per-class macros 3. 30 per-specialization macros -4. 8 per-character macros -5. 10 per-character per-specialization macros - -That's an amazing 30 slots you can use to setup your macros for each specialization where previously you -would be forced to fit all your class macros for all specializations into a max of 18 slots!! +4. 30 per-character macros +5. (Legacy support for inactive/archived macros) -I'm not just trying to be a sales person here - I've wanted this feature for ages. Especially as a Druid -main! +That's an amazing 60 slots you can use to setup your macros for each class/specialization, where previously +you would be forced to fit all your class macros for all specializations into the limited character-specific +slots! ### Shared macros across characters -Building on the last feature, the per-class and per-specialization macros are not character specific so -you don't need to copy your macros around manually if you have more than one character of a particular -class. +Building on the last feature, the per-class and per-specialization macros are not character specific. +You don't need to copy your macros around manually if you have more than one character of the same +class—Mega Macro syncs them for you automatically. ### Improved macro icon/tooltip evaluation This is the other big feature added by this AddOn. By default the Blizzard UI picks your icon based on -`#showtooltip` or uses the icon of the spell that will be cast (and doesn't use it's tooltip). Mega Macro -does the following: +`#showtooltip` or uses the icon of the spell that will be cast. Mega Macro improves this by doing the +following: -1. `#showtooltip` as source for icon and tooltip always when present -2. The spell/item/toy that will be cast will have its icon _and tooltip_ used -3. You can make the icon setting be the fallback icon to display -4. If no spell/item/toy/icon will be used, picks the first spell/item/toy in the macro and shows that +1. `#showtooltip` is always used as the source for icon and tooltip when present. +2. If no `#showtooltip` is present, the spell/item/toy that will be cast is dynamically evaluated to determine + the correct icon. +4. You can set a specific fallback icon to display if no conditions are met. +5. Smart Fallback: If no spell/item/toy/icon will be used, it picks the first valid spell/item/toy found in the macro code. Number 3 is the kicker here. This means if you are a healer and have heal-safe macros like this one: @@ -53,49 +51,59 @@ Number 3 is the kicker here. This means if you are a healer and have heal-safe m /cast [help, no dead] Heal ``` -Then you don't need to prefix your macro code with `#showtooltip Heal` - the AddOn handles that for you! This -cuts down on some of the redundant manual work required to write a macro and reduces the amount of macro code -capacity you have to use on non-functional code. +You don't need to prefix your macro code with `#showtooltip Heal` —the AddOn handles that for you! This cuts down +on redundant manual work and saves character space for functional code. -As a bonus to the above improvement, cast-sequence commands will not only effortlessly display the correct icon, -you'll also get the tooltip for the sequence abilities - impossible in the current macro implementation. +As a bonus, `castsequence` commands will not only effortlessly display the correct icon, but you'll also get +the tooltip for the current ability in the sequence—something impossible in the default UI. ### Bigger macros! -Write macros up to 1023 characters long. No more having to minify your macro code making it harder to -read. Spread out those casts, have them on separate lines for more readability and/or add comments. You can do -so much more with four times the code capacity! +Write macros up to 1024 characters long. No more minifying your code or using obscure abbreviations just to fit +the limit. Spread out those casts, use separate lines for readability, and add comments. You can do so much more +with four times the code capacity! + +*(Note: Macros over 255 characters are handled via a secure clicking system invisible to the user.)* ### Ids are preserved -Your action bars will not break. Your macros defined in the Mega Macro UI will use the same macro slot -for their entire lifetime so you don't have to worry about your action bars breaking when switching -characters and specializations. +Your action bars will not break. Your macros defined in the Mega Macro UI are synchronized to the native macro slots, so you don't have to worry about your action bars breaking when switching characters, specializations, or uninstalling the addon. ### Improved macro UI -The Mega Macro UI replicates the native macro UI down to every last detail **except for the following** -**improvements**: +The Mega Macro UI replicates the native macro UI feel but adds: -* Searchable icon list -* Wider, making better use of screen space -* Higher, expanding the macro text box so you'll rarely have to scroll to view your code +* Searchable icon list (Updated for Patch 12.0) +* Wider interface, making better use of screen space +* Taller text box, so you rarely have to scroll to view code * Buttons are contextually disabled -* "Change Name/Icon" changed to "Rename" -* Icons in macros list and selected macro are updated dynamically based on macro conditionals -* Icons in macros list and selected macro trigger tooltips +* "Change Name/Icon" simplified to "Rename" +* Icons in the macro list update dynamically based on conditionals ## Before you use -This AddOn will destroy your existing macros once they have been imported. If the import fails, the AddOn -won't do anything to your macros until it is successful. +This AddOn will take over your existing macros once they have been imported. If the import fails, the AddOn won't do anything to your macros. -Once successfully imported, if you remove the AddOn you will have 138 stub macros and your old macros will -be gone. +Once successfully imported, if you remove the AddOn folder without uninstalling properly, you will be left with "stub" macros (macros that point to Mega Macro logic). To avoid this, follow the instructions below. -### When you install the AddOn +## How to Install +1. Install the addon to your `_retail_/Interface/AddOns` folder. +2. Login to your character. +3. The addon will automatically prompt to import your existing macros into the Mega Macro storage. + +## How to Uninstall (The Easy Way) +1. Open Mega Macro (`/m`). +2. Click the Config tab at the bottom right. +3. Click the Uninstall button. +4. The addon will restore your macros to their standard Blizzard format (shortening them if necessary). +5. You can now safely delete the AddOn folder. + +## How to Uninstall (The Manual "Nuclear" Way) +If you cannot access the game or the uninstall button fails, you can restore your macros if you made a backup of your WTF folder. + +### When you install the AddOn (The Manual Way) **Before you start the game**, you'll want to back up your macros. To do this: @@ -108,7 +116,7 @@ robocopy "C:\Program Files (x86)\World of Warcraft\_retail_\WTF\Account" %USERPR 3. click OK -### When you want to remove the AddOn +### When you want to remove the AddOn (The Manual Way) Do the following steps **after exiting the game**. @@ -121,6 +129,6 @@ xcopy /e /i /y %USERPROFILE%\Games\wow-macro-backup "C:\Program Files (x86)\Worl 3. click OK -## Special Thanks +### Special Thanks -Special thanks to `aurelion314` (`Cubelicious` in-game) and `Dannez83` for contributing many hours to update this addon for Dragonflight! \ No newline at end of file +Special thanks to `aurelion314` (`Cubelicious` in-game) and `Dannez83` for their contributions during Dragonflight, and to the community for helping update the addon for Patch 12.0.0 (Midnight). This Addon is being updated with AI assistance using Gemini 3 Pro. From 0d6a04af9a4d89a534a224dcf397e5c0635323d8 Mon Sep 17 00:00:00 2001 From: Torchlite Date: Tue, 27 Jan 2026 12:13:14 -0500 Subject: [PATCH 2/4] Add files via upload --- MegaMacro.toc | 57 +- README.md | 268 +-- scripts/publish.bat | 6 +- scripts/reset-data.bat | 2 +- src/config.lua | 65 +- src/constants.lua | 241 ++- src/engine/mega-macro-action-bar-engine.lua | 1101 ++++++----- src/engine/mega-macro-code-info.lua | 684 +++---- src/engine/mega-macro-engine.lua | 1210 ++++++------ src/engine/mega-macro-icon-evaluator.lua | 735 ++++---- src/engine/mega-macro-parser.lua | 709 +++---- src/engine/parsing/colours.lua | 55 +- src/engine/parsing/conditions.lua | 573 +++--- src/engine/parsing/parsing-functions.lua | 123 +- src/main.lua | 205 ++- src/mega-macro-icon-navigator.lua | 367 ++-- src/mega-macro.lua | 510 +++--- src/tooltip-functions.lua | 62 +- src/windows/mega-macro.window.lua | 1833 ++++++++++--------- src/windows/mega-macro.window.xml | 1215 ++++++------ 20 files changed, 5081 insertions(+), 4940 deletions(-) diff --git a/MegaMacro.toc b/MegaMacro.toc index 691053b..ae3708a 100644 --- a/MegaMacro.toc +++ b/MegaMacro.toc @@ -1,28 +1,29 @@ -## Interface: 110000 -## Title: Mega Macro -## Author: Sellorio -## Version: 1.0.0 -## OptionalDeps: ElvUI - -## SavedVariables: MegaMacroConfig, MegaMacroGlobalData -## SavedVariablesPerCharacter: MegaMacroCharacterData - -src/constants.lua -src/config.lua -src/tooltip-functions.lua -src/mega-macro.lua -src/mega-macro-icon-navigator.lua - -src/engine/mega-macro-action-bar-engine.lua -src/engine/mega-macro-code-info.lua -src/engine/mega-macro-icon-evaluator.lua -src/engine/mega-macro-engine.lua - -src/engine/parsing/parsing-functions.lua -src/engine/parsing/colours.lua -src/engine/parsing/conditions.lua -src/engine/mega-macro-parser.lua - -src/windows/mega-macro.window.xml - -src/main.lua +## Interface: 120000 +## Title: Mega Macro +## Author: Sellorio +## Project Successor: Torchlite +## Version: 1.6.5 +## OptionalDeps: ElvUI + +## SavedVariables: MegaMacroConfig, MegaMacroGlobalData +## SavedVariablesPerCharacter: MegaMacroCharacterData + +src/constants.lua +src/config.lua +src/tooltip-functions.lua +src/mega-macro.lua +src/mega-macro-icon-navigator.lua + +src/engine/mega-macro-action-bar-engine.lua +src/engine/mega-macro-code-info.lua +src/engine/mega-macro-icon-evaluator.lua +src/engine/mega-macro-engine.lua + +src/engine/parsing/parsing-functions.lua +src/engine/parsing/colours.lua +src/engine/parsing/conditions.lua +src/engine/mega-macro-parser.lua + +src/windows/mega-macro.window.xml + +src/main.lua diff --git a/README.md b/README.md index db623ad..35b1fe1 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,134 @@ -# Mega Macro (a World of Warcraft AddOn) - -![Screenshot 1](https://raw.githubusercontent.com/Sellorio/mega-macro/master/Screenshot1.png) - -**IMPORTANT:** Before you use this AddOn, make sure you read the `before you use` section! - -## Features - -Use the `/m` command to begin your new macro experience! - -Once macro import is complete, `/m` will only show the new macro UI. - -### More macro slots! - -This AddOn seamlessly provides you with a massive increase to macro slots over the native amounts (120 global -and 30 per-character slots). How does it do this? By segmenting the total macro slots into categories and -hot-switching macros based on your Class and Specialization. - -The breakdown of macro slots is as follows: -1. 60 global macros -2. 30 per-class macros -3. 30 per-specialization macros -4. 30 per-character macros -5. (Legacy support for inactive/archived macros) - -That's an amazing 60 slots you can use to setup your macros for each class/specialization, where previously -you would be forced to fit all your class macros for all specializations into the limited character-specific -slots! - -### Shared macros across characters - -Building on the last feature, the per-class and per-specialization macros are not character specific. -You don't need to copy your macros around manually if you have more than one character of the same -class—Mega Macro syncs them for you automatically. - -### Improved macro icon/tooltip evaluation - -This is the other big feature added by this AddOn. By default the Blizzard UI picks your icon based on -`#showtooltip` or uses the icon of the spell that will be cast. Mega Macro improves this by doing the -following: - -1. `#showtooltip` is always used as the source for icon and tooltip when present. -2. If no `#showtooltip` is present, the spell/item/toy that will be cast is dynamically evaluated to determine - the correct icon. -4. You can set a specific fallback icon to display if no conditions are met. -5. Smart Fallback: If no spell/item/toy/icon will be used, it picks the first valid spell/item/toy found in the macro code. - -Number 3 is the kicker here. This means if you are a healer and have heal-safe macros like this one: - -``` -/cast [help, no dead] Heal -``` - -You don't need to prefix your macro code with `#showtooltip Heal` —the AddOn handles that for you! This cuts down -on redundant manual work and saves character space for functional code. - -As a bonus, `castsequence` commands will not only effortlessly display the correct icon, but you'll also get -the tooltip for the current ability in the sequence—something impossible in the default UI. - -### Bigger macros! - -Write macros up to 1024 characters long. No more minifying your code or using obscure abbreviations just to fit -the limit. Spread out those casts, use separate lines for readability, and add comments. You can do so much more -with four times the code capacity! - -*(Note: Macros over 255 characters are handled via a secure clicking system invisible to the user.)* - -### Ids are preserved - -Your action bars will not break. Your macros defined in the Mega Macro UI are synchronized to the native macro slots, so you don't have to worry about your action bars breaking when switching characters, specializations, or uninstalling the addon. - -### Improved macro UI - -The Mega Macro UI replicates the native macro UI feel but adds: - -* Searchable icon list (Updated for Patch 12.0) -* Wider interface, making better use of screen space -* Taller text box, so you rarely have to scroll to view code -* Buttons are contextually disabled -* "Change Name/Icon" simplified to "Rename" -* Icons in the macro list update dynamically based on conditionals - -## Before you use - -This AddOn will take over your existing macros once they have been imported. If the import fails, the AddOn won't do anything to your macros. - -Once successfully imported, if you remove the AddOn folder without uninstalling properly, you will be left with "stub" macros (macros that point to Mega Macro logic). - -To avoid this, follow the instructions below. - -## How to Install -1. Install the addon to your `_retail_/Interface/AddOns` folder. -2. Login to your character. -3. The addon will automatically prompt to import your existing macros into the Mega Macro storage. - -## How to Uninstall (The Easy Way) -1. Open Mega Macro (`/m`). -2. Click the Config tab at the bottom right. -3. Click the Uninstall button. -4. The addon will restore your macros to their standard Blizzard format (shortening them if necessary). -5. You can now safely delete the AddOn folder. - -## How to Uninstall (The Manual "Nuclear" Way) -If you cannot access the game or the uninstall button fails, you can restore your macros if you made a backup of your WTF folder. - -### When you install the AddOn (The Manual Way) - -**Before you start the game**, you'll want to back up your macros. To do this: - -1. Windows + R (open the run dialog) -2. Enter the following command: - -``` -robocopy "C:\Program Files (x86)\World of Warcraft\_retail_\WTF\Account" %USERPROFILE%\Games\wow-macro-backup macros-cache.txt /s -``` - -3. click OK - -### When you want to remove the AddOn (The Manual Way) - -Do the following steps **after exiting the game**. - -1. Windows + R (open the run dialog) -2. Enter the following command: - -``` -xcopy /e /i /y %USERPROFILE%\Games\wow-macro-backup "C:\Program Files (x86)\World of Warcraft\_retail_\WTF\Account" -``` - -3. click OK - -### Special Thanks - -Special thanks to `aurelion314` (`Cubelicious` in-game) and `Dannez83` for their contributions during Dragonflight, and to the community for helping update the addon for Patch 12.0.0 (Midnight). This Addon is being updated with AI assistance using Gemini 3 Pro. +# Mega Macro (a World of Warcraft AddOn) + +![Screenshot 1](https://raw.githubusercontent.com/Sellorio/mega-macro/master/Screenshot1.png) + +**IMPORTANT:** Before you use this AddOn, make sure you read the `before you use` section! + +## Features + +Use the `/m` command to begin your new macro experience! + +Once macro import is complete, `/m` will only show the new macro UI. + +### More macro slots! + +This AddOn seamlessly provides you with a massive increase to macro slots over the native amounts (120 global +and 30 per-character slots). How does it do this? By segmenting the total macro slots into categories and +hot-switching macros based on your Class and Specialization. + +The breakdown of macro slots is as follows: +1. 60 global macros +2. 30 per-class macros +3. 30 per-specialization macros +4. 30 per-character macros +5. (Legacy support for inactive/archived macros) + +That's an amazing 60 slots you can use to setup your macros for each class/specialization, where previously +you would be forced to fit all your class macros for all specializations into the limited character-specific +slots! + +### Shared macros across characters + +Building on the last feature, the per-class and per-specialization macros are not character specific. +You don't need to copy your macros around manually if you have more than one character of the same +classMega Macro syncs them for you automatically. + +### Improved macro icon/tooltip evaluation + +This is the other big feature added by this AddOn. By default the Blizzard UI picks your icon based on +`#showtooltip` or uses the icon of the spell that will be cast. Mega Macro improves this by doing the +following: + +1. `#showtooltip` is always used as the source for icon and tooltip when present. +2. If no `#showtooltip` is present, the spell/item/toy that will be cast is dynamically evaluated to determine + the correct icon. +4. You can set a specific fallback icon to display if no conditions are met. +5. Smart Fallback: If no spell/item/toy/icon will be used, it picks the first valid spell/item/toy found in the macro code. + +Number 3 is the kicker here. This means if you are a healer and have heal-safe macros like this one: + +``` +/cast [help, no dead] Heal +``` + +You don't need to prefix your macro code with `#showtooltip Heal` the AddOn handles that for you! This cuts down +on redundant manual work and saves character space for functional code. + +As a bonus, `castsequence` commands will not only effortlessly display the correct icon, but you'll also get +the tooltip for the current ability in the sequencesomething impossible in the default UI. + +### Bigger macros! + +Write macros up to 1024 characters long. No more minifying your code or using obscure abbreviations just to fit +the limit. Spread out those casts, use separate lines for readability, and add comments. You can do so much more +with four times the code capacity! + +*(Note: Macros over 255 characters are handled via a secure clicking system invisible to the user.)* + +### Ids are preserved + +Your action bars will not break. Your macros defined in the Mega Macro UI are synchronized to the native macro slots, so you don't have to worry about your action bars breaking when switching characters, specializations, or uninstalling the addon. + +### Improved macro UI + +The Mega Macro UI replicates the native macro UI feel but adds: + +* Searchable icon list (Updated for Patch 12.0) +* Wider interface, making better use of screen space +* Taller text box, so you rarely have to scroll to view code +* Buttons are contextually disabled +* "Change Name/Icon" simplified to "Rename" +* Icons in the macro list update dynamically based on conditionals + +## Before you use + +This AddOn will take over your existing macros once they have been imported. If the import fails, the AddOn won't do anything to your macros. + +Once successfully imported, if you remove the AddOn folder without uninstalling properly, you will be left with "stub" macros (macros that point to Mega Macro logic). + +To avoid this, follow the instructions below. + +## How to Install +1. Install the addon to your `_retail_/Interface/AddOns` folder. +2. Login to your character. +3. The addon will automatically prompt to import your existing macros into the Mega Macro storage. + +## How to Uninstall (The Easy Way) +1. Open Mega Macro (`/m`). +2. Click the Config tab at the bottom right. +3. Click the Uninstall button. +4. The addon will restore your macros to their standard Blizzard format (shortening them if necessary). +5. You can now safely delete the AddOn folder. + +## How to Uninstall (The Manual "Nuclear" Way) +If you cannot access the game or the uninstall button fails, you can restore your macros if you made a backup of your WTF folder. + +### When you install the AddOn (The Manual Way) + +**Before you start the game**, you'll want to back up your macros. To do this: + +1. Windows + R (open the run dialog) +2. Enter the following command: + +``` +robocopy "C:\Program Files (x86)\World of Warcraft\_retail_\WTF\Account" %USERPROFILE%\Games\wow-macro-backup macros-cache.txt /s +``` + +3. click OK + +### When you want to remove the AddOn (The Manual Way) + +Do the following steps **after exiting the game**. + +1. Windows + R (open the run dialog) +2. Enter the following command: + +``` +xcopy /e /i /y %USERPROFILE%\Games\wow-macro-backup "C:\Program Files (x86)\World of Warcraft\_retail_\WTF\Account" +``` + +3. click OK + +### Special Thanks + +Special thanks to `aurelion314` (`Cubelicious` in-game) and `Dannez83` for their contributions during Dragonflight, and to the community for helping update the addon for Patch 12.0.0 (Midnight). This Addon is being updated with AI assistance using Gemini 3 Pro. diff --git a/scripts/publish.bat b/scripts/publish.bat index 0d50ef6..daf4cf5 100644 --- a/scripts/publish.bat +++ b/scripts/publish.bat @@ -1,4 +1,4 @@ -CALL .\depublish.bat -mkdir "C:\Program Files (x86)\World of Warcraft\_retail_\Interface\AddOns\MegaMacro" -xcopy /e /i /exclude:..\.publishignore .. "C:\Program Files (x86)\World of Warcraft\_retail_\Interface\AddOns\MegaMacro" +CALL .\depublish.bat +mkdir "C:\Program Files (x86)\World of Warcraft\_retail_\Interface\AddOns\MegaMacro" +xcopy /e /i /exclude:..\.publishignore .. "C:\Program Files (x86)\World of Warcraft\_retail_\Interface\AddOns\MegaMacro" PAUSE \ No newline at end of file diff --git a/scripts/reset-data.bat b/scripts/reset-data.bat index bfd01aa..fbceb96 100644 --- a/scripts/reset-data.bat +++ b/scripts/reset-data.bat @@ -1,2 +1,2 @@ -cd "C:\Program Files (x86)\World of Warcraft\_retail_\WTF" +cd "C:\Program Files (x86)\World of Warcraft\_retail_\WTF" del /s /q /f "MegaMacro.*" \ No newline at end of file diff --git a/src/config.lua b/src/config.lua index 4527fbc..a6996bd 100644 --- a/src/config.lua +++ b/src/config.lua @@ -1,35 +1,30 @@ -function MegaMacro_InitialiseConfig() - if MegaMacroGlobalData == nil then - MegaMacroGlobalData = { - Activated = false, - Macros = {}, - InactiveMacros = {}, - Classes = {} - } - end - - if MegaMacroCharacterData == nil then - MegaMacroCharacterData = { - Activated = false, - Macros = {}, - Specializations = {} - } - end - - if MegaMacroConfig == nil then - MegaMacroConfig = { - UseNativeActionBar = true, - } - end -end - -function MegaMacroConfig_IsWindowDialog() - return not MegaMacroGlobalData.WindowInfo and true or MegaMacroGlobalData.WindowInfo.IsDialog -end - -function MegaMacroConfig_GetWindowPosition() - if MegaMacroGlobalData.WindowInfo then - return MegaMacroGlobalData.WindowInfo.RelativePoint, MegaMacroGlobalData.WindowInfo.X, MegaMacroGlobalData.WindowInfo.Y - end -end - +function MegaMacro_InitialiseConfig() + -- New: Keep track of the addon data version + local currentDataVersion = 120 -- Corresponds to 12.0 + + if MegaMacroGlobalData == nil then + MegaMacroGlobalData = { + Activated = false, + Macros = {}, + InactiveMacros = {}, + Classes = {}, + Version = currentDataVersion -- Added versioning + } + end + + if MegaMacroCharacterData == nil then + MegaMacroCharacterData = { + Activated = false, + Macros = {}, + Specializations = {} + } + end + + if MegaMacroConfig == nil then + MegaMacroConfig = { + UseNativeActionBar = true, + -- New: Default UI transparency for 12.0 HUD + WindowOpacity = 1.0, + } + end +end \ No newline at end of file diff --git a/src/constants.lua b/src/constants.lua index ee2b4f5..9907c0a 100644 --- a/src/constants.lua +++ b/src/constants.lua @@ -1,124 +1,119 @@ -MacroLimits = { - -- limit: 120 non-character specific macro slots - GlobalCount = 60, - PerClassCount = 30, - PerSpecializationCount = 30, - -- limit: 18 character specific macro slots - PerCharacterCount = 30, - PerCharacterSpecializationCount = 0, - InactiveCount = 160, - MaxGlobalMacros = 120, - MaxCharacterMacros = 30 -} - -MacroIndexOffsets = { - Global = 0, - PerClass = MacroLimits.GlobalCount, - PerSpecialization = MacroLimits.GlobalCount + MacroLimits.PerClassCount, - PerCharacter = MacroLimits.GlobalCount + MacroLimits.PerClassCount + MacroLimits.PerSpecializationCount, - PerCharacterSpecialization = MacroLimits.GlobalCount + MacroLimits.PerClassCount + MacroLimits.PerSpecializationCount + MacroLimits.PerCharacterCount, - Inactive = MacroLimits.GlobalCount + MacroLimits.PerClassCount + MacroLimits.PerSpecializationCount + MacroLimits.PerCharacterCount + MacroLimits.PerCharacterSpecializationCount, - NativeCharacterMacros = 120 -} - -MegaMacroScopes = { - Global = "gg", - Class = "gc", - Specialization = "gs", - Character = "ch", - CharacterSpecialization = "cs" -} - -PetActionTextures = { - Attack = 132152, - Assist = 524348, - Passive = 132311, - Defensive = 132110, - Follow = 132328, - MoveTo = 457329, - Stay = 136106, - Dismiss = 136095 -} - -MegaMacroTexture = 134400 -MegaMacroActiveStanceTexture = 136116 -MegaMacroCodeMaxLength = 250 -MegaMacroCodeMaxLengthForNative = 250 -HighestMaxMacroCount = math.max(MacroLimits.GlobalCount, MacroLimits.PerClassCount, MacroLimits.PerSpecializationCount, MacroLimits.PerCharacterCount, MacroLimits.PerCharacterSpecializationCount) - -MegaMacroInfoFunctions = { - Spell = { - GetCooldown = function(abilityId) - local spellCooldownInfo = C_Spell.GetSpellCooldown(abilityId); - if spellCooldownInfo then - return spellCooldownInfo.startTime, spellCooldownInfo.duration, spellCooldownInfo.isEnabled, spellCooldownInfo.modRate; - end - end, - GetCount = C_Spell.GetSpellCastCount, - GetCharges = function(spellId) return C_Spell.GetSpellCharges(spellId) end, - IsUsable = C_Spell.IsSpellUsable, - IsInRange = function(spellId, target) - local spellIndex = FindSpellBookSlotBySpellID(spellId) - if spellIndex then - local result = C_Spell.IsSpellInRange(spellIndex, "spell", target) - - if result == nil then - return nil - else - return result ~= 0 - end - end - end, - IsCurrent = C_Spell.IsCurrentSpell, - IsEquipped = function(_) return false end, - IsAutoRepeat = C_Spell.IsAutoRepeatSpell, - IsLocked = C_LevelLink.IsSpellLocked, - GetLossOfControlCooldown = C_Spell.GetSpellLossOfControlCooldown, - IsOverlayed = IsSpellOverlayed - }, - Item = { - GetCooldown = C_Item.GetItemCooldown, - GetCount = function(itemId) return C_Item.GetItemCount(itemId, false, true) end, - GetCharges = function(_) return 0, 0, -1, 0, 1 end, -- charges, maxCharges, chargeStart, chargeDuration, chargeModRate - IsUsable = function(itemId) return C_Item.IsUsableItem(itemId), false end, - IsInRange = function(itemId) - if C_Item.GetItemInfo(itemId) then - return function(unit) - return C_Item.IsItemInRange(itemId, unit) - end - end - end, - IsCurrent = C_Item.IsCurrentItem, - IsEquipped = function(itemId) return C_Item.IsEquippedItem(itemId) end, - IsAutoRepeat = function(_) return false end, - IsLocked = function(_) return false end, - GetLossOfControlCooldown = function(_) return -1, 0 end, - IsOverlayed = function(_) return false end - }, - Fallback = { - GetCooldown = function(_) return -1, 0, true end, - GetCount = function(_) return 0 end, - GetCharges = function(_) return 0, 0, -1, 0, 1 end, -- charges, maxCharges, chargeStart, chargeDuration, chargeModRate - IsUsable = function(_) return false, false end, - IsInRange = function(_, _) return nil end, - IsCurrent = function(_) return false end, - IsEquipped = function(_) return false end, - IsAutoRepeat = function(_) return false end, - IsLocked = function(_) return false end, - GetLossOfControlCooldown = function(_) return -1, 0 end, - IsOverlayed = function(_) return false end - }, - Unknown = { - GetCooldown = function(_) return -1, 0, true end, - GetCount = function(_) return 0 end, - GetCharges = function(_) return 0, 0, -1, 0, 1 end, -- charges, maxCharges, chargeStart, chargeDuration, chargeModRate - IsUsable = function(_) return true, false end, - IsInRange = function(_, _) return nil end, - IsCurrent = function(_) return false end, - IsEquipped = function(_) return false end, - IsAutoRepeat = function(_) return false end, - IsLocked = function(_) return false end, - GetLossOfControlCooldown = function(_) return -1, 0 end, - IsOverlayed = function(_) return false end - } +MacroLimits = { + -- limit: 120 non-character specific macro slots + GlobalCount = 60, + PerClassCount = 30, + PerSpecializationCount = 30, + -- limit: 18 character specific macro slots + PerCharacterCount = 30, + PerCharacterSpecializationCount = 0, + InactiveCount = 160, + MaxGlobalMacros = 120, + MaxCharacterMacros = 30 +} + +MacroIndexOffsets = { + Global = 0, + PerClass = MacroLimits.GlobalCount, + PerSpecialization = MacroLimits.GlobalCount + MacroLimits.PerClassCount, + PerCharacter = MacroLimits.GlobalCount + MacroLimits.PerClassCount + MacroLimits.PerSpecializationCount, + PerCharacterSpecialization = MacroLimits.GlobalCount + MacroLimits.PerClassCount + MacroLimits.PerSpecializationCount + MacroLimits.PerCharacterCount, + Inactive = MacroLimits.GlobalCount + MacroLimits.PerClassCount + MacroLimits.PerSpecializationCount + MacroLimits.PerCharacterCount + MacroLimits.PerCharacterSpecializationCount, + NativeCharacterMacros = 120 +} + +MegaMacroScopes = { + Global = "gg", + Class = "gc", + Specialization = "gs", + Character = "ch", + CharacterSpecialization = "cs" +} + +PetActionTextures = { + Attack = 132152, + Assist = 524348, + Passive = 132311, + Defensive = 132110, + Follow = 132328, + MoveTo = 457329, + Stay = 136106, + Dismiss = 136095 +} + +MegaMacroTexture = 134400 +MegaMacroActiveStanceTexture = 136116 +MegaMacroCodeMaxLength = 1024 -- Increased for 12.0 internal storage +MegaMacroCodeMaxLengthForNative = 255 -- Hard limit for Blizzard UI sync +HighestMaxMacroCount = math.max(MacroLimits.GlobalCount, MacroLimits.PerClassCount, MacroLimits.PerSpecializationCount, MacroLimits.PerCharacterCount, MacroLimits.PerCharacterSpecializationCount) + +MegaMacroInfoFunctions = { + Spell = { + GetCooldown = function(abilityId) + local spellCooldownInfo = C_Spell.GetSpellCooldown(abilityId) + if spellCooldownInfo then + return spellCooldownInfo.startTime, spellCooldownInfo.duration, spellCooldownInfo.isEnabled, spellCooldownInfo.modRate + end + end, + GetCount = C_Spell.GetSpellCastCount, + GetCharges = function(spellId) + return C_Spell.GetSpellCharges(spellId) + end, + IsUsable = C_Spell.IsSpellUsable, + IsInRange = function(spellId, target) + -- 12.0: IsSpellInRange now accepts SpellID or Name directly without needing the SpellBook index + local result = C_Spell.IsSpellInRange(spellId, target) + if result == nil then + return nil + else + return result + end + end, + IsCurrent = C_Spell.IsCurrentSpell, + IsEquipped = function(_) return false end, + IsAutoRepeat = C_Spell.IsAutoRepeatSpell, + IsLocked = C_LevelLink.IsSpellLocked, + GetLossOfControlCooldown = C_Spell.GetSpellLossOfControlCooldown, + IsOverlayed = C_Spell.GetSpellOverlayed -- Updated for 12.0 namespace + }, + Item = { + GetCooldown = C_Item.GetItemCooldown, + GetCount = function(itemId) return C_Item.GetItemCount(itemId, false, true) end, + GetCharges = function(_) return 0, 0, -1, 0, 1 end, + IsUsable = function(itemId) return C_Item.IsUsableItem(itemId) end, + IsInRange = function(itemId, target) + return C_Item.IsItemInRange(itemId, target) + end, + IsCurrent = C_Item.IsCurrentItem, + IsEquipped = function(itemId) return C_Item.IsEquippedItem(itemId) end, + IsAutoRepeat = function(_) return false end, + IsLocked = function(_) return false end, + GetLossOfControlCooldown = function(_) return -1, 0 end, + IsOverlayed = function(_) return false end + }, + Fallback = { + GetCooldown = function(_) return -1, 0, true end, + GetCount = function(_) return 0 end, + GetCharges = function(_) return 0, 0, -1, 0, 1 end, + IsUsable = function(_) return false, false end, + IsInRange = function(_, _) return nil end, + IsCurrent = function(_) return false end, + IsEquipped = function(_) return false end, + IsAutoRepeat = function(_) return false end, + IsLocked = function(_) return false end, + GetLossOfControlCooldown = function(_) return -1, 0 end, + IsOverlayed = function(_) return false end + }, + Unknown = { + GetCooldown = function(_) return -1, 0, true end, + GetCount = function(_) return 0 end, + GetCharges = function(_) return 0, 0, -1, 0, 1 end, + IsUsable = function(_) return true, false end, + IsInRange = function(_, _) return nil end, + IsCurrent = function(_) return false end, + IsEquipped = function(_) return false end, + IsAutoRepeat = function(_) return false end, + IsLocked = function(_) return false end, + GetLossOfControlCooldown = function(_) return -1, 0 end, + IsOverlayed = function(_) return false end + } } \ No newline at end of file diff --git a/src/engine/mega-macro-action-bar-engine.lua b/src/engine/mega-macro-action-bar-engine.lua index a05ccd0..356a6e9 100644 --- a/src/engine/mega-macro-action-bar-engine.lua +++ b/src/engine/mega-macro-action-bar-engine.lua @@ -1,591 +1,510 @@ ---[[ - -For developer refererence, these are the features of an action bar button: - - Texture - - Cooldown (including silence/stun cooldown display) - - Recharge cooldown - - Is Usable - - Insufficient Mana (or resource) - - Is In Range - - Is Current - - Current Shapeshift Form (should appear as Is Current) - - Count (spells or items). Example of spell count: Lesser Soul Fragments as displayed on Soul Cleave or Spirit Bomb. - - Charges (spells only atm) - - Spell Glow (such as on empowerments for Balance Druid) - - Active Auto Attack Flash (flashes red when auto attack is active) - intentionally omitted from this addon - -]] - - - -local LibActionButton = nil -local ActionBarSystem = nil -- Blizzard or LAB or Dominos -local BlizzardActionBars = { "Action", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarRight", "MultiBarLeft", "MultiBar5", "MultiBar6", "MultiBar7" } - -local rangeTimer = 5 -local updateRange = false - -local ActionsBoundToMegaMacros = {} - --- Cache that remembers the last macro ID and modifier state for each button. --- It prevents unnecessary full‑icon recomputation on every click. -local buttonCache = {} -- [button] = { macroId = number|nil, mods = "SCA"|"" } - --- Global snapshot of the modifier signature from the previous OnUpdate tick. --- When this value changes we will flush the per‑button cache so every button --- gets a fresh icon on the next frame. -local previousGlobalMods = "" - -local function UpdateCurrentActionState(button, functions, abilityId) - local isChecked = functions.IsCurrent(abilityId) or functions.IsAutoRepeat(abilityId) - - if not isChecked and functions == MegaMacroInfoFunctions.Spell then - local shapeshiftFormIndex = GetShapeshiftForm() - if shapeshiftFormIndex and shapeshiftFormIndex > 0 and abilityId == select(4, GetShapeshiftFormInfo(shapeshiftFormIndex)) then - isChecked = true - end - end - - if isChecked then - button:SetChecked(true) - else - button:SetChecked(false) - end -end - -local function UpdateUsable(button, functions, abilityId) - local icon = button.icon - local normalTexture = button.NormalTexture - if not normalTexture then - return; - end - - local isUsable, notEnoughMana = functions.IsUsable(abilityId) - if isUsable then - icon:SetVertexColor(1.0, 1.0, 1.0) - normalTexture:SetVertexColor(1.0, 1.0, 1.0) - elseif ( notEnoughMana ) then - icon:SetVertexColor(0.5, 0.5, 1.0) - normalTexture:SetVertexColor(0.5, 0.5, 1.0) - else - icon:SetVertexColor(0.4, 0.4, 0.4) - normalTexture:SetVertexColor(1.0, 1.0, 1.0) - end - - -- local isLevelLinkLocked = functions.IsLocked(abilityId) - -- if not icon:IsDesaturated() then - -- icon:SetDesaturated(isLevelLinkLocked) - -- end - - -- if button.LevelLinkLockIcon then - -- button.LevelLinkLockIcon:SetShown(isLevelLinkLocked) - -- end -end - -local function LibActionButton_EndChargeCooldown(self) - self:Hide() - self:SetParent(UIParent) - - -- ----------------------------------------------------------------- - -- Defensive clean‑up – the parent may already be nil (see the - -- earlier fix) and the pool table may not exist yet. - -- ----------------------------------------------------------------- - if self.parent then - self.parent.chargeCooldown = nil - self.parent = nil - end - - -- Ensure the pool table exists before inserting. - if not LibActionButton.ChargeCooldowns then - LibActionButton.ChargeCooldowns = {} - end - tinsert(LibActionButton.ChargeCooldowns, self) -end - -local function LibActionButton_StartChargeCooldown(parent, chargeStart, chargeDuration, chargeModRate) - ------------------------------------------------------------------------- - -- 1️⃣ Ensure the pool tables exist - ------------------------------------------------------------------------- - if not LibActionButton.ChargeCooldowns then - LibActionButton.ChargeCooldowns = {} - end - if not LibActionButton.NumChargeCooldowns then - LibActionButton.NumChargeCooldowns = 0 - end - - ------------------------------------------------------------------------- - -- 2️⃣ Pull a frame from the pool (or create a fresh one) - ------------------------------------------------------------------------- - if not parent.chargeCooldown then - local cooldown = tremove(LibActionButton.ChargeCooldowns) - if not cooldown then - LibActionButton.NumChargeCooldowns = LibActionButton.NumChargeCooldowns + 1 - cooldown = CreateFrame( - "Cooldown", - "LAB10ChargeCooldown"..LibActionButton.NumChargeCooldowns, - parent, - "CooldownFrameTemplate" - ) - cooldown:SetScript("OnCooldownDone", LibActionButton_EndChargeCooldown) - end - - ----------------------------------------------------------------- - -- 3️⃣ **Critical** – always (re)assign the custom .parent field. - ----------------------------------------------------------------- - cooldown.parent = parent - - ----------------------------------------------------------------- - -- 4️⃣ **Force the correct visual defaults** every time we take a frame. - ----------------------------------------------------------------- - cooldown:SetHideCountdownNumbers(false) -- <<< show the timer text - cooldown:SetDrawSwipe(true) -- <<< draw the normal spiral - cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") -- same edge as normal cooldown - cooldown:SetSwipeColor(0, 0, 0) -- default colour (matches normal) - - ----------------------------------------------------------------- - -- 5️⃣ Anchor the frame to the button - ----------------------------------------------------------------- - cooldown:SetParent(parent) - cooldown:SetAllPoints(parent) - cooldown:SetFrameStrata("TOOLTIP") - cooldown:Show() - - parent.chargeCooldown = cooldown - end - - ------------------------------------------------------------------------- - -- 6️⃣ Visual configuration that depends on the button’s opacity - ------------------------------------------------------------------------- - parent.chargeCooldown:SetDrawBling(parent.chargeCooldown:GetEffectiveAlpha() > 0.5) - - ------------------------------------------------------------------------- - -- 7️⃣ Feed the charge‑cooldown data into the frame - ------------------------------------------------------------------------- - CooldownFrame_Set(parent.chargeCooldown, chargeStart, chargeDuration, true, true, chargeModRate) - - ------------------------------------------------------------------------- - -- 8️⃣ Masque support (unchanged) - ------------------------------------------------------------------------- - if Masque and Masque.UpdateCharge then - Masque:UpdateCharge(parent) - end - - ------------------------------------------------------------------------- - -- 9️⃣ Edge case – if the charge is already ready, hide the frame - ------------------------------------------------------------------------- - if not chargeStart or chargeStart == 0 then - LibActionButton_EndChargeCooldown(parent.chargeCooldown) - end -end - -local function UpdateCooldownLibActionButton(button, functions, abilityId) - -- 1️⃣ Gather all cooldown‑related data - local locStart, locDuration = functions.GetLossOfControlCooldown(abilityId) - local start, duration, enable, modRate = functions.GetCooldown(abilityId) - local charges, maxCharges, chargeStart, chargeDuration, chargeModRate = functions.GetCharges(abilityId) - - ------------------------------------------------------------------------- - -- 2️⃣ Normal cooldown handling (unchanged apart from a tiny refactor) - ------------------------------------------------------------------------- - button.cooldown:SetDrawBling(button.cooldown:GetEffectiveAlpha() > 0.5) - - if (locStart + locDuration) > (start + duration) then - if button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_LOSS_OF_CONTROL then - button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge-LoC") - button.cooldown:SetSwipeColor(0.17, 0, 0) - button.cooldown:SetHideCountdownNumbers(true) - button.cooldown.currentCooldownType = COOLDOWN_TYPE_LOSS_OF_CONTROL - end - CooldownFrame_Set(button.cooldown, locStart, locDuration, true, true, modRate) - else - if button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_NORMAL then - button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") - button.cooldown:SetSwipeColor(0, 0, 0) - button.cooldown:SetHideCountdownNumbers(false) - button.cooldown.currentCooldownType = COOLDOWN_TYPE_NORMAL - end - - if locStart > 0 then - button.cooldown:SetScript("OnCooldownDone", - function() UpdateCooldownLibActionButton(button, functions, abilityId) end) - end - - ----------------------------------------------------------------- - -- 3️⃣ **Charge‑cooldown handling** - ----------------------------------------------------------------- - local hasCharges = charges and maxCharges and maxCharges > 1 - if hasCharges and charges > 0 and charges < maxCharges then - -- *** NEW: use the main cooldown widget for the charge timer *** - -- The fourth argument (enable) is always true for charge cooldowns. - -- We keep the same modRate that the original code passed. - CooldownFrame_Set(button.cooldown, chargeStart, chargeDuration, true, false, chargeModRate) - - -- Ensure the normal cooldown edge/colour is appropriate for a charge. - button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") - button.cooldown:SetSwipeColor(0, 0, 0) - button.cooldown:SetHideCountdownNumbers(false) -- show numbers - else - -- No pending charge → clear any leftover charge timer. - -- (The regular cooldown will be set later in the function.) - button.cooldown:SetCooldown(0, 0) -- clears the frame - end - - ----------------------------------------------------------------- - -- 4️⃣ Finally set the *regular* cooldown (spell/item cooldown) - ----------------------------------------------------------------- - CooldownFrame_Set(button.cooldown, start, duration, enable, false, modRate) - end -end - -local function UpdateCooldownBlizzard(button, functions, abilityId) - locStart, locDuration = functions.GetLossOfControlCooldown(abilityId) - start, duration, enable, modRate = functions.GetCooldown(abilityId) - charges, maxCharges, chargeStart, chargeDuration, chargeModRate = functions.GetCharges(abilityId) - - if ( (locStart + locDuration) > (start + duration) ) then - if ( button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_LOSS_OF_CONTROL ) then - button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge-LoC") - button.cooldown:SetSwipeColor(0.17, 0, 0) - button.cooldown:SetHideCountdownNumbers(true) - button.cooldown.currentCooldownType = COOLDOWN_TYPE_LOSS_OF_CONTROL - end - - CooldownFrame_Set(button.cooldown, locStart, locDuration, true, true, modRate) - ClearChargeCooldown(button) - else - if ( button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_NORMAL ) then - button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") - button.cooldown:SetSwipeColor(0, 0, 0) - button.cooldown:SetHideCountdownNumbers(false) - button.cooldown.currentCooldownType = COOLDOWN_TYPE_NORMAL - end - - if( locStart > 0 ) then - button.cooldown:SetScript("OnCooldownDone", ActionButton_OnCooldownDone) - end - - if ( charges and maxCharges and maxCharges > 1 and charges < maxCharges ) then - StartChargeCooldown(button, chargeStart, chargeDuration, chargeModRate) - else - ClearChargeCooldown(button) - end - - CooldownFrame_Set(button.cooldown, start, duration, enable, false, modRate) - end -end - -local function UpdateCount(button, functions, abilityId) - local countLabel = button.Count - local count = functions.GetCount(abilityId) - - local isNonEquippableItem = functions == MegaMacroInfoFunctions.Item and not C_Item.IsEquippableItem(abilityId) - local isNonItemWithCount = functions ~= MegaMacroInfoFunctions.Item and count and count > 0 - - if isNonEquippableItem or isNonItemWithCount then - countLabel:SetText(count > (button.maxDisplayCount or 9999) and "*" or count) - else - local charges, maxCharges = functions.GetCharges(abilityId) - if charges and maxCharges and maxCharges > 1 then - countLabel:SetText(charges) - else - countLabel:SetText("") - end - end -end - -local function UpdateEquipped(button, functions, abilityId) - if functions.IsEquipped(abilityId) then - button.Border:SetVertexColor(0, 1.0, 0, 0.35) - button.Border:Show() - else - button.Border:Hide() - end -end - -local function UpdateOverlayGlow(button, functions, abilityId) - if functions.IsOverlayed(abilityId) then - ActionButton_ShowOverlayGlow(button) - else - ActionButton_HideOverlayGlow(button) - end -end - -local function UpdateRangeTimer(elapsed) - rangeTimer = rangeTimer - elapsed - - if rangeTimer < 0 then - updateRange = true - rangeTimer = 1 - else - updateRange = false - end -end - -local function UpdateRange(button, functions, abilityId, target) - local valid = functions.IsInRange(abilityId, target) - -- local valid = IsSpellInRange(spellName, target) or IsItemInRange(abilityId, target) - -- local valid = true - local checksRange = (valid ~= nil); - local inRange = checksRange and valid; - rangeTimer = 1; - - local hotkeyUpdated = false - if Bartender4 then - if checksRange and not inRange then - if Bartender4.db.profile.outofrange == "button" then - button.icon:SetVertexColor( - Bartender4.db.profile.colors.range.r, - Bartender4.db.profile.colors.range.g, - Bartender4.db.profile.colors.range.b) - elseif Bartender4.db.profile.outofrange == "hotkey" then - button.HotKey:SetVertexColor( - Bartender4.db.profile.colors.range.r, - Bartender4.db.profile.colors.range.g, - Bartender4.db.profile.colors.range.b) - end - return - end - end - - if button.HotKey:GetText() == RANGE_INDICATOR then - if checksRange then - button.HotKey:Show(); - if ( inRange ) then - button.HotKey:SetVertexColor(LIGHTGRAY_FONT_COLOR:GetRGB()); - elseif not hotkeyUpdated then - button.HotKey:SetVertexColor(RED_FONT_COLOR:GetRGB()); - end - else - button.HotKey:Hide(); - end - else - if checksRange and not inRange and not hotkeyUpdated then - button.HotKey:SetVertexColor(RED_FONT_COLOR:GetRGB()); - else - button.HotKey:SetVertexColor(LIGHTGRAY_FONT_COLOR:GetRGB()); - end - end -end - -local function UpdateActionBar(button, macroId) - local data = MegaMacroIconEvaluator.GetCachedData(macroId) - local functions = MegaMacroInfoFunctions.Unknown - - if data then - if data.Type == "spell" then - functions = MegaMacroInfoFunctions.Spell - elseif data.Type == "item" then - functions = MegaMacroInfoFunctions.Item - elseif data.Type == "fallback" then - functions = MegaMacroInfoFunctions.Fallback - end - - UpdateCurrentActionState(button, functions, data.Id) - UpdateUsable(button, functions, data.Id) - UpdateCount(button, functions, data.Id) - UpdateEquipped(button, functions, data.Id) - UpdateOverlayGlow(button, functions, data.Id) - button.icon:SetTexture(data.Icon or MegaMacroTexture) - - if LibActionButton then - UpdateCooldownLibActionButton(button, functions, data.Id) - else - UpdateCooldownBlizzard(button, functions, data.Id) - end - - -- this throttle was causing a flickering icon issue for bartender users - -- after having it disabled for about a month, I've noticed no performance drop - --if updateRange then - UpdateRange(button, functions, data.Id, data.Target) - --end - end -end - -local function ResetActionBar(button) - button:SetChecked(false) - button.Count:SetText("") - button.Border:Hide() -- reset eqipped border - ActionButton_HideOverlayGlow(button) - ClearChargeCooldown(button) - UpdateRange(button, MegaMacroInfoFunctions.Unknown) - button.icon:SetVertexColor(1.0, 1.0, 1.0) -- reset opacity (is usable visuals) - - local normalTexture = button.NormalTexture - if normalTexture then - normalTexture:SetVertexColor(1.0, 1.0, 1.0) -- reset blue shift - end -end - -local function ForEachLibActionButton(func) - for button, _ in pairs(LibActionButton.buttonRegistry) do - func(button) - end -end - -local function ForEachDominosButton(func) - for i=1, 120 do - local button = nil - if i <= 12 then - button = _G[('ActionButton%d'):format(i)] - elseif i <= 24 then - button = _G["DominosActionButton"..(i - 12)] - elseif i <= 36 then - button = _G[('MultiBarRightButton%d'):format(i - 24)] - elseif i <= 48 then - button = _G[('MultiBarLeftButton%d'):format(i - 36)] - elseif i <= 60 then - button = _G[('MultiBarBottomRightButton%d'):format(i - 48)] - elseif i <= 72 then - button = _G[('MultiBarBottomLeftButton%d'):format(i - 60)] - else - button = _G["DominosActionButton"..(i - 60)] - end - if button then - func(button) - end - end -end - -local function ForEachBlizzardActionButton(func) - for actionBarIndex=1, #BlizzardActionBars do - for i=1, 12 do - local button = _G[BlizzardActionBars[actionBarIndex].."Button"..i] - if button then - func(button) - end - end - end -end - -MegaMacroActionBarEngine = {} - -function MegaMacroActionBarEngine.Initialize() - if _G["BT4Button1"] then - LibActionButton = LibStub("LibActionButton-1.0") - ActionBarSystem = "LAB" - elseif _G["ElvUI_Bar1Button1"] then - LibActionButton = LibStub("LibActionButton-1.0-ElvUI") - ActionBarSystem = "LAB" - elseif Dominos then - ActionBarSystem = "Dominos" - else - ActionBarSystem = "Blizzard" - end - - MegaMacroIconEvaluator.OnIconUpdated(function() - rangeTimer = -1 - end) -end - --- Returns a short string that uniquely represents the current modifier key state. --- "S" = Shift, "C" = Ctrl, "A" = Alt. Empty string means no modifiers. -local function GetCurrentModifierSignature() - local sig = "" - if IsShiftKeyDown() then sig = sig .. "S" end - if IsControlKeyDown() then sig = sig .. "C" end - if IsAltKeyDown() then sig = sig .. "A" end - return sig -end - -function MegaMacroActionBarEngine.OnUpdate(elapsed) - UpdateRangeTimer(elapsed) - - ----------------------------------------------------------------- - -- 1️⃣ Detect a change in the *global* modifier state. - ----------------------------------------------------------------- - local currentGlobalMods = GetCurrentModifierSignature() - if currentGlobalMods ~= previousGlobalMods then - -- Modifier keys were pressed or released since the last frame. - -- Invalidate every button's cached signature so they will all be - -- refreshed on this tick. - for btn, _ in pairs(buttonCache) do - buttonCache[btn] = nil -- clear the entry completely - end - previousGlobalMods = currentGlobalMods - end - ----------------------------------------------------------------- - -- 2️⃣ Continue with the rest of the original OnUpdate logic. - ----------------------------------------------------------------- - local focus = GetMouseFoci()[1] - local iterator = ForEachBlizzardActionButton - - if ActionBarSystem == "LAB" then - iterator = ForEachLibActionButton - elseif ActionBarSystem == "Dominos" then - iterator = ForEachDominosButton - end - - iterator(function(button) - local action = button:GetAttribute("action") or button.action - local macroName = GetActionText(action) - local macroCode = GetMacroBody(macroName) - local type, arg1 = GetActionInfo(action) - - -- Resolve the Mega‑Macro ID (identical to the original logic) - local macroId = type == "macro" and macroCode and tonumber(string.sub(macroCode, 2, 4)) - - ------------------------------------------------------------------------- - -- 1️⃣ Decide if we need to rebuild the icon. - ------------------------------------------------------------------------- - local needRefresh = false - local curMods = GetCurrentModifierSignature() -- ← captures BOTH press AND release - - if macroId then - -- Button is bound to a Mega‑Macro – look at (or create) its cache entry - local cache = buttonCache[button] - if not cache then - cache = { macroId = nil, mods = nil } - buttonCache[button] = cache - end - - -- Refresh if the macro ID changed **or** the modifier signature changed - if cache.macroId ~= macroId or cache.mods ~= curMods then - needRefresh = true - cache.macroId = macroId - cache.mods = curMods - end - else - -- Not a Mega‑Macro (or unbound). Clean any stale cache entry. - if buttonCache[button] then - buttonCache[button] = nil - end - needRefresh = false - end - - ------------------------------------------------------------------------- - -- 2️⃣ Run the full update only when we decided it’s necessary. - ------------------------------------------------------------------------- - if macroId and needRefresh then - ActionsBoundToMegaMacros[button] = true - UpdateActionBar(button, macroId) - - -- Tooltip handling (unchanged) - if button == focus then - ShowToolTipForMegaMacro(macroId) - end - elseif ActionsBoundToMegaMacros[button] then - -- The button *was* a Mega‑Macro but now isn’t – clean up. - ActionsBoundToMegaMacros[button] = nil - if not arg1 then - ResetActionBar(button) - end - end - - ------------------------------------------------------------------------- - -- 3️⃣ The rest of the original per‑button loop (range handling, etc.) - -- stays exactly as it was – we only prevented unnecessary icon - -- recomputation. - ------------------------------------------------------------------------- - -- (no additional code needed here – the original file already had the - -- range‑timer, tooltip, and other housekeeping calls after the block - -- you just replaced.) -end) -end - -function MegaMacroActionBarEngine.OnTargetChanged() - rangeTimer = -1 -end - -hooksecurefunc("ActionButton_UpdateRangeIndicator", function() - rangeTimer = -1 -end) \ No newline at end of file +--[[ + +For developer refererence, these are the features of an action bar button: + - Texture + - Cooldown (including silence/stun cooldown display) + - Recharge cooldown + - Is Usable + - Insufficient Mana (or resource) + - Is In Range + - Is Current + - Current Shapeshift Form (should appear as Is Current) + - Count (spells or items). + - Charges (spells only atm) + - Spell Glow (such as on empowerments for Balance Druid) + - Active Auto Attack Flash (flashes red when auto attack is active) - intentionally omitted from this addon + +]] + +local LibActionButton = nil +local ActionBarSystem = nil -- Blizzard or LAB or Dominos +local BlizzardActionBars = { "Action", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarRight", "MultiBarLeft", "MultiBar5", "MultiBar6", "MultiBar7" } + +local rangeTimer = 5 +local updateRange = false + +local ActionsBoundToMegaMacros = {} + +-- Cache that remembers the last macro ID and modifier state for each button. +local buttonCache = {} -- [button] = { macroId = number|nil, mods = "SCA"|"" } + +-- Global snapshot of the modifier signature from the previous OnUpdate tick. +local previousGlobalMods = "" + +local function UpdateCurrentActionState(button, functions, abilityId) + local isChecked = functions.IsCurrent(abilityId) or functions.IsAutoRepeat(abilityId) + + if not isChecked and functions == MegaMacroInfoFunctions.Spell then + local shapeshiftFormIndex = GetShapeshiftForm() + if shapeshiftFormIndex and shapeshiftFormIndex > 0 and abilityId == select(4, GetShapeshiftFormInfo(shapeshiftFormIndex)) then + isChecked = true + end + end + + if isChecked then + button:SetChecked(true) + else + button:SetChecked(false) + end +end + +local function UpdateUsable(button, functions, abilityId) + local icon = button.icon + local normalTexture = button.NormalTexture + if not normalTexture then + return + end + + local isUsable, notEnoughMana = functions.IsUsable(abilityId) + if isUsable then + icon:SetVertexColor(1.0, 1.0, 1.0) + normalTexture:SetVertexColor(1.0, 1.0, 1.0) + elseif ( notEnoughMana ) then + icon:SetVertexColor(0.5, 0.5, 1.0) + normalTexture:SetVertexColor(0.5, 0.5, 1.0) + else + icon:SetVertexColor(0.4, 0.4, 0.4) + normalTexture:SetVertexColor(1.0, 1.0, 1.0) + end +end + +local function LibActionButton_EndChargeCooldown(self) + self:Hide() + self:SetParent(UIParent) + + if self.parent then + self.parent.chargeCooldown = nil + self.parent = nil + end + + if not LibActionButton.ChargeCooldowns then + LibActionButton.ChargeCooldowns = {} + end + tinsert(LibActionButton.ChargeCooldowns, self) +end + +local function LibActionButton_StartChargeCooldown(parent, chargeStart, chargeDuration, chargeModRate) + if not LibActionButton.ChargeCooldowns then + LibActionButton.ChargeCooldowns = {} + end + if not LibActionButton.NumChargeCooldowns then + LibActionButton.NumChargeCooldowns = 0 + end + + if not parent.chargeCooldown then + local cooldown = tremove(LibActionButton.ChargeCooldowns) + if not cooldown then + LibActionButton.NumChargeCooldowns = LibActionButton.NumChargeCooldowns + 1 + cooldown = CreateFrame( + "Cooldown", + "LAB10ChargeCooldown"..LibActionButton.NumChargeCooldowns, + parent, + "CooldownFrameTemplate" + ) + cooldown:SetScript("OnCooldownDone", LibActionButton_EndChargeCooldown) + end + + cooldown.parent = parent + cooldown:SetHideCountdownNumbers(false) + cooldown:SetDrawSwipe(true) + cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") + cooldown:SetSwipeColor(0, 0, 0) + + cooldown:SetParent(parent) + cooldown:SetAllPoints(parent) + cooldown:SetFrameStrata("TOOLTIP") + cooldown:Show() + + parent.chargeCooldown = cooldown + end + + parent.chargeCooldown:SetDrawBling(parent.chargeCooldown:GetEffectiveAlpha() > 0.5) + CooldownFrame_Set(parent.chargeCooldown, chargeStart, chargeDuration, true, true, chargeModRate) + + if Masque and Masque.UpdateCharge then + Masque:UpdateCharge(parent) + end + + if not chargeStart or chargeStart == 0 then + LibActionButton_EndChargeCooldown(parent.chargeCooldown) + end +end + +local function UpdateCooldownLibActionButton(button, functions, abilityId) + local locStart, locDuration = functions.GetLossOfControlCooldown(abilityId) + local start, duration, enable, modRate = functions.GetCooldown(abilityId) + local charges, maxCharges, chargeStart, chargeDuration, chargeModRate = functions.GetCharges(abilityId) + + button.cooldown:SetDrawBling(button.cooldown:GetEffectiveAlpha() > 0.5) + + if (locStart + locDuration) > (start + duration) then + if button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_LOSS_OF_CONTROL then + button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge-LoC") + button.cooldown:SetSwipeColor(0.17, 0, 0) + button.cooldown:SetHideCountdownNumbers(true) + button.cooldown.currentCooldownType = COOLDOWN_TYPE_LOSS_OF_CONTROL + end + CooldownFrame_Set(button.cooldown, locStart, locDuration, true, true, modRate) + else + if button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_NORMAL then + button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") + button.cooldown:SetSwipeColor(0, 0, 0) + button.cooldown:SetHideCountdownNumbers(false) + button.cooldown.currentCooldownType = COOLDOWN_TYPE_NORMAL + end + + if locStart > 0 then + button.cooldown:SetScript("OnCooldownDone", + function() UpdateCooldownLibActionButton(button, functions, abilityId) end) + end + + local hasCharges = charges and maxCharges and maxCharges > 1 + if hasCharges and charges > 0 and charges < maxCharges then + CooldownFrame_Set(button.cooldown, chargeStart, chargeDuration, true, false, chargeModRate) + button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") + button.cooldown:SetSwipeColor(0, 0, 0) + button.cooldown:SetHideCountdownNumbers(false) + else + button.cooldown:SetCooldown(0, 0) + end + + CooldownFrame_Set(button.cooldown, start, duration, enable, false, modRate) + end +end + +local function UpdateCooldownBlizzard(button, functions, abilityId) + local locStart, locDuration = functions.GetLossOfControlCooldown(abilityId) + local start, duration, enable, modRate = functions.GetCooldown(abilityId) + local charges, maxCharges, chargeStart, chargeDuration, chargeModRate = functions.GetCharges(abilityId) + + if ( (locStart + locDuration) > (start + duration) ) then + if ( button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_LOSS_OF_CONTROL ) then + button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge-LoC") + button.cooldown:SetSwipeColor(0.17, 0, 0) + button.cooldown:SetHideCountdownNumbers(true) + button.cooldown.currentCooldownType = COOLDOWN_TYPE_LOSS_OF_CONTROL + end + + CooldownFrame_Set(button.cooldown, locStart, locDuration, true, true, modRate) + if ClearChargeCooldown then ClearChargeCooldown(button) end + else + if ( button.cooldown.currentCooldownType ~= COOLDOWN_TYPE_NORMAL ) then + button.cooldown:SetEdgeTexture("Interface\\Cooldown\\edge") + button.cooldown:SetSwipeColor(0, 0, 0) + button.cooldown:SetHideCountdownNumbers(false) + button.cooldown.currentCooldownType = COOLDOWN_TYPE_NORMAL + end + + if( locStart > 0 ) then + button.cooldown:SetScript("OnCooldownDone", ActionButton_OnCooldownDone) + end + + if ( charges and maxCharges and maxCharges > 1 and charges < maxCharges ) then + if StartChargeCooldown then + StartChargeCooldown(button, chargeStart, chargeDuration, chargeModRate) + end + else + if ClearChargeCooldown then ClearChargeCooldown(button) end + end + + CooldownFrame_Set(button.cooldown, start, duration, enable, false, modRate) + end +end + +local function UpdateCount(button, functions, abilityId) + local countLabel = button.Count + local count = functions.GetCount(abilityId) + + local isNonEquippableItem = functions == MegaMacroInfoFunctions.Item and not C_Item.IsEquippableItem(abilityId) + local isNonItemWithCount = functions ~= MegaMacroInfoFunctions.Item and count and count > 0 + + if isNonEquippableItem or isNonItemWithCount then + countLabel:SetText(count > (button.maxDisplayCount or 9999) and "*" or count) + else + local charges, maxCharges = functions.GetCharges(abilityId) + if charges and maxCharges and maxCharges > 1 then + countLabel:SetText(charges) + else + countLabel:SetText("") + end + end +end + +local function UpdateEquipped(button, functions, abilityId) + if functions.IsEquipped(abilityId) then + button.Border:SetVertexColor(0, 1.0, 0, 0.35) + button.Border:Show() + else + button.Border:Hide() + end +end + +local function UpdateOverlayGlow(button, functions, abilityId) + if functions.IsOverlayed(abilityId) then + ActionButton_ShowOverlayGlow(button) + else + ActionButton_HideOverlayGlow(button) + end +end + +local function UpdateRangeTimer(elapsed) + rangeTimer = rangeTimer - elapsed + + if rangeTimer < 0 then + updateRange = true + rangeTimer = 1 + else + updateRange = false + end +end + +local function UpdateRange(button, functions, abilityId, target) + local valid = functions.IsInRange(abilityId, target) + local checksRange = (valid ~= nil); + local inRange = checksRange and valid; + + -- Optimized: Set timer based on state to reduce checks + rangeTimer = 1; + + local hotkeyUpdated = false + if Bartender4 then + if checksRange and not inRange then + if Bartender4.db.profile.outofrange == "button" then + button.icon:SetVertexColor( + Bartender4.db.profile.colors.range.r, + Bartender4.db.profile.colors.range.g, + Bartender4.db.profile.colors.range.b) + elseif Bartender4.db.profile.outofrange == "hotkey" then + button.HotKey:SetVertexColor( + Bartender4.db.profile.colors.range.r, + Bartender4.db.profile.colors.range.g, + Bartender4.db.profile.colors.range.b) + end + return + end + end + + if button.HotKey:GetText() == RANGE_INDICATOR then + if checksRange then + button.HotKey:Show(); + if ( inRange ) then + button.HotKey:SetVertexColor(LIGHTGRAY_FONT_COLOR:GetRGB()); + elseif not hotkeyUpdated then + button.HotKey:SetVertexColor(RED_FONT_COLOR:GetRGB()); + end + else + button.HotKey:Hide(); + end + else + if checksRange and not inRange and not hotkeyUpdated then + button.HotKey:SetVertexColor(RED_FONT_COLOR:GetRGB()); + else + button.HotKey:SetVertexColor(LIGHTGRAY_FONT_COLOR:GetRGB()); + end + end +end + +local function UpdateActionBar(button, macroId) + local data = MegaMacroIconEvaluator.GetCachedData(macroId) + local functions = MegaMacroInfoFunctions.Unknown + + if data then + if data.Type == "spell" then + functions = MegaMacroInfoFunctions.Spell + elseif data.Type == "item" then + functions = MegaMacroInfoFunctions.Item + elseif data.Type == "fallback" then + functions = MegaMacroInfoFunctions.Fallback + end + + UpdateCurrentActionState(button, functions, data.Id) + UpdateUsable(button, functions, data.Id) + UpdateCount(button, functions, data.Id) + UpdateEquipped(button, functions, data.Id) + UpdateOverlayGlow(button, functions, data.Id) + button.icon:SetTexture(data.Icon or MegaMacroTexture) + + if LibActionButton then + UpdateCooldownLibActionButton(button, functions, data.Id) + else + UpdateCooldownBlizzard(button, functions, data.Id) + end + + -- Range updates can be expensive, throttle handled by OnUpdate logic + UpdateRange(button, functions, data.Id, data.Target) + end +end + +local function ResetActionBar(button) + button:SetChecked(false) + button.Count:SetText("") + button.Border:Hide() + ActionButton_HideOverlayGlow(button) + if ClearChargeCooldown then ClearChargeCooldown(button) end + UpdateRange(button, MegaMacroInfoFunctions.Unknown) + button.icon:SetVertexColor(1.0, 1.0, 1.0) + + local normalTexture = button.NormalTexture + if normalTexture then + normalTexture:SetVertexColor(1.0, 1.0, 1.0) + end +end + +local function ForEachLibActionButton(func) + if LibActionButton and LibActionButton.buttonRegistry then + for button, _ in pairs(LibActionButton.buttonRegistry) do + func(button) + end + end +end + +local function ForEachDominosButton(func) + for i=1, 120 do + local button = nil + if i <= 12 then + button = _G[('ActionButton%d'):format(i)] + elseif i <= 24 then + button = _G["DominosActionButton"..(i - 12)] + elseif i <= 36 then + button = _G[('MultiBarRightButton%d'):format(i - 24)] + elseif i <= 48 then + button = _G[('MultiBarLeftButton%d'):format(i - 36)] + elseif i <= 60 then + button = _G[('MultiBarBottomRightButton%d'):format(i - 48)] + elseif i <= 72 then + button = _G[('MultiBarBottomLeftButton%d'):format(i - 60)] + else + button = _G["DominosActionButton"..(i - 60)] + end + if button then + func(button) + end + end +end + +local function ForEachBlizzardActionButton(func) + for actionBarIndex=1, #BlizzardActionBars do + for i=1, 12 do + local button = _G[BlizzardActionBars[actionBarIndex].."Button"..i] + if button then + func(button) + end + end + end +end + +MegaMacroActionBarEngine = {} + +function MegaMacroActionBarEngine.Initialize() + if _G["BT4Button1"] then + LibActionButton = LibStub("LibActionButton-1.0") + ActionBarSystem = "LAB" + elseif _G["ElvUI_Bar1Button1"] then + LibActionButton = LibStub("LibActionButton-1.0-ElvUI") + ActionBarSystem = "LAB" + elseif Dominos then + ActionBarSystem = "Dominos" + else + ActionBarSystem = "Blizzard" + end + + MegaMacroIconEvaluator.OnIconUpdated(function() + rangeTimer = -1 + end) +end + +-- Returns a short string that uniquely represents the current modifier key state. +local function GetCurrentModifierSignature() + local sig = "" + if IsShiftKeyDown() then sig = sig .. "S" end + if IsControlKeyDown() then sig = sig .. "C" end + if IsAltKeyDown() then sig = sig .. "A" end + return sig +end + +function MegaMacroActionBarEngine.OnUpdate(elapsed) + UpdateRangeTimer(elapsed) + + local currentGlobalMods = GetCurrentModifierSignature() + if currentGlobalMods ~= previousGlobalMods then + for btn, _ in pairs(buttonCache) do + buttonCache[btn] = nil + end + previousGlobalMods = currentGlobalMods + end + + local focus = GetMouseFoci and GetMouseFoci()[1] or GetMouseFocus and GetMouseFocus() + local iterator = ForEachBlizzardActionButton + + if ActionBarSystem == "LAB" then + iterator = ForEachLibActionButton + elseif ActionBarSystem == "Dominos" then + iterator = ForEachDominosButton + end + + iterator(function(button) + local action = button:GetAttribute("action") or button.action + -- 12.0: C_Macro namespace usage + local macroName = GetActionText(action) + local macroCode = nil + + -- 12.0 Safety: Only fetch macro body if we have a name, to avoid errors + if macroName then + macroCode = C_Macro.GetMacroBody(macroName) + end + + local type, arg1 = GetActionInfo(action) + + -- 12.0 Safety: Ensure macroCode is a string before parsing to avoid Taint errors + local macroId = nil + if type == "macro" and macroCode and _G.type(macroCode) == "string" and #macroCode >= 4 then + macroId = tonumber(string.sub(macroCode, 2, 4)) + end + + local needRefresh = false + local curMods = GetCurrentModifierSignature() + + if macroId then + local cache = buttonCache[button] + if not cache then + cache = { macroId = nil, mods = nil } + buttonCache[button] = cache + end + + if cache.macroId ~= macroId or cache.mods ~= curMods then + needRefresh = true + cache.macroId = macroId + cache.mods = curMods + end + else + if buttonCache[button] then + buttonCache[button] = nil + end + needRefresh = false + end + + if macroId and needRefresh then + ActionsBoundToMegaMacros[button] = true + UpdateActionBar(button, macroId) + + if button == focus then + ShowToolTipForMegaMacro(macroId) + end + elseif ActionsBoundToMegaMacros[button] then + ActionsBoundToMegaMacros[button] = nil + if not arg1 then + ResetActionBar(button) + end + end + end) +end + +function MegaMacroActionBarEngine.OnTargetChanged() + rangeTimer = -1 +end + +if hooksecurefunc then + hooksecurefunc("ActionButton_UpdateRangeIndicator", function() + rangeTimer = -1 + end) +end \ No newline at end of file diff --git a/src/engine/mega-macro-code-info.lua b/src/engine/mega-macro-code-info.lua index abb3fa7..6531be6 100644 --- a/src/engine/mega-macro-code-info.lua +++ b/src/engine/mega-macro-code-info.lua @@ -1,336 +1,350 @@ -local CodeInfoCache = {} ---[[ -{ - "macroid": { - { - type: "cast", cast/stopmacro/use/castsequence/showtooltip/fallback - body: "[mod:alt] X; Y" - }, - ... - } -} ---]] - -local function trim(s) - return s:gsub("^%s*(.-)%s*$", "%1") -end - -local function lastIndexOf(str, match, maxIndex) - local index = string.find(str, match) - - if index ~= nil then - local nextIndex = string.find(str, match, index + 1) - while nextIndex and (not maxIndex or nextIndex < maxIndex) do - index = nextIndex - nextIndex = string.find(str, match, index + 1) - end - end - - return index -end - -local function Char(str, index) - if string.len(str) >= index then - return string.sub(str, index, index) - end -end - -local function ParseSpaces(parsingContext) - local result = false - - while Char(parsingContext.Code, parsingContext.Index) == " " do - result = true - parsingContext.Index = parsingContext.Index + 1 - end - - return result -end - -local function ParseEndOfLine(parsingContext) - local result = false - - while Char(parsingContext.Code, parsingContext.Index) == "\n" do - result = true - parsingContext.Index = parsingContext.Index + 1 - ParseSpaces(parsingContext) - end - - return result -end - -local function ParseWord(parsingContext) - local result = false - local word = "" - - local character = Char(parsingContext.Code, parsingContext.Index) - while character and (string.match(character, "[a-z]") or string.match(character, "[A-Z]") or string.match(character, "[0-9]") or character == "_") do - word = word .. character - result = true - parsingContext.Index = parsingContext.Index + 1 - character = Char(parsingContext.Code, parsingContext.Index) - end - - return result, word -end - -local function GrabRemainingLineCode(parsingContext) - local code = "" - local character = Char(parsingContext.Code, parsingContext.Index) - local appendToCode = true -- ignore comments trailing lines of code - - while character ~= nil and character ~= "\n" do - if character == "#" then - appendToCode = false - end - - if appendToCode then - code = code .. character - end - - parsingContext.Index = parsingContext.Index + 1 - character = Char(parsingContext.Code, parsingContext.Index) - end - - return code -end - -local function ParseCastCommand(parsingContext) - local castCode = trim(GrabRemainingLineCode(parsingContext)) - table.insert( - CodeInfoCache[parsingContext.MacroId], - { - Type = "cast", - Body = castCode - }) -end - -local function ParseCastsequenceCommand(parsingContext) - local body = trim(GrabRemainingLineCode(parsingContext)) - table.insert( - CodeInfoCache[parsingContext.MacroId], - { - Type = "castsequence", - Body = body - }) -end - -local function ParseStopmacroCommand(parsingContext) - local condition = trim(GrabRemainingLineCode(parsingContext)) - table.insert( - CodeInfoCache[parsingContext.MacroId], - { - Type = "stopmacro", - Body = condition.."TRUE" - }) -end - -local function ParsePetCommand(parsingContext, command) - local condition = trim(GrabRemainingLineCode(parsingContext)) - table.insert( - CodeInfoCache[parsingContext.MacroId], - { - Type = "petcommand", - Body = condition.."TRUE", - Command = command - }) -end - -local function ParseEquipSetCommand(parsingContext) - local equipCode = trim(GrabRemainingLineCode(parsingContext)) - table.insert( - CodeInfoCache[parsingContext.MacroId], - { - Type = "equipset", - Body = equipCode - }) -end - -local function ParseClickCommand(parsingContext) - local clickCode = trim(GrabRemainingLineCode(parsingContext)) - - table.insert( - CodeInfoCache[parsingContext.MacroId], - { - Type = "click", - Body = clickCode - }) -end - -local function ParseCommand(parsingContext) - local result = false - - if Char(parsingContext.Code, parsingContext.Index) == "/" then - parsingContext.Index = parsingContext.Index + 1 - local wordResult, word = ParseWord(parsingContext) - - if wordResult then - word = string.lower(word) - result = true - ParseSpaces(parsingContext) - - if word == "cast" or word == "use" then - ParseCastCommand(parsingContext) - elseif word == "castsequence" then - ParseCastsequenceCommand(parsingContext) - elseif word == "stopmacro" then - ParseStopmacroCommand(parsingContext) - elseif word == "petattack" then - ParsePetCommand(parsingContext, "attack") - elseif word == "petassist" then - ParsePetCommand(parsingContext, "assist") - elseif word == "petpassive" then - ParsePetCommand(parsingContext, "passive") - elseif word == "petdefensive" then - ParsePetCommand(parsingContext, "defensive") - elseif word == "petfollow" then - ParsePetCommand(parsingContext, "follow") - elseif word == "petmoveto" then - ParsePetCommand(parsingContext, "moveto") - elseif word == "petstay" then - ParsePetCommand(parsingContext, "stay") - elseif word == "petdismiss" then - ParsePetCommand(parsingContext, "dismiss") - elseif word == "equipset" then - ParseEquipSetCommand(parsingContext) - elseif word == "click" then - ParseClickCommand(parsingContext) - end - end - end - - GrabRemainingLineCode(parsingContext) - ParseEndOfLine(parsingContext) - - return result -end - -local function ParseShowtooltip(parsingContext) - if Char(parsingContext.Code, parsingContext.Index) == "#" then - parsingContext.Index = parsingContext.Index + 1 - local wordResult, word = ParseWord(parsingContext) - - if wordResult and string.lower(word) == "showtooltip" then - local body = trim(GrabRemainingLineCode(parsingContext)) - if string.len(body) > 0 then - table.insert( - CodeInfoCache[parsingContext.MacroId], - { - Type = "showtooltip", - Body = body - }) - return true - end - end - end - - return false -end - -local function AddFallbackAbility(macroId) - local codeInfo = CodeInfoCache[macroId] - local codeInfoLength = #codeInfo - - for i=1, codeInfoLength do - local type = codeInfo[i].Type - - if type == "showtooltip" then - break - elseif type == "cast" or type == "use" then - local endOfAbility = string.find(codeInfo[i].Body, ";") - endOfAbility = endOfAbility and (endOfAbility - 1) - local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]", endOfAbility) or 0) + 1 - - local abilityName = trim(string.sub(codeInfo[i].Body, endOfConditions, endOfAbility)) - - table.insert( - codeInfo, - { - Type = "fallbackAbility", - Body = abilityName - }) - break - elseif type == "castsequence" then - local endOfSequence = string.find(codeInfo[i].Body, ";") - endOfSequence = endOfSequence and (endOfSequence - 1) - local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]", endOfSequence) or 0) + 1 - - local sequence = trim(string.sub(codeInfo[i].Body, endOfConditions, endOfSequence)) - - table.insert( - codeInfo, - { - Type = "fallbackSequence", - Body = sequence - }) - break - elseif type == "petcommand" then - table.insert( - codeInfo, - { - Type = "fallbackPetCommand", - Body = codeInfo[i].Command - }) - break - elseif type == "equipset" then - local endOfAbility = string.find(codeInfo[i].Body, ";") - endOfAbility = endOfAbility and (endOfAbility - 1) - local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]", endOfAbility) or 0) + 1 - - local firstSetMentioned = trim(string.sub(codeInfo[i].Body, endOfConditions, endOfAbility)) - - table.insert( - codeInfo, - { - Type = "fallbackEquipSet", - Body = firstSetMentioned - }) - elseif type == "click" then - local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]") or 0) + 1 - local buttonName = trim(string.sub(codeInfo[i].Body, endOfConditions)) - - table.insert( - codeInfo, - { - Type = "fallbackClick", - Body = buttonName - }) - elseif type == "stopmacro" then - -- ignore - end - end -end - -local function CalculateMacroInfo(macro) - local parsingContext = { MacroId = macro.Id, Index = 1, Code = macro.Code } - CodeInfoCache[macro.Id] = {} - - -- skip to start of code - ParseSpaces(parsingContext) - ParseEndOfLine(parsingContext) - - if parsingContext.Index < string.len(parsingContext.Code) then - if not ParseShowtooltip(parsingContext) then - while parsingContext.Index <= string.len(parsingContext.Code) do - ParseCommand(parsingContext) - end - end - end - - AddFallbackAbility(macro.Id) - - return CodeInfoCache[macro.Id] -end - -MegaMacroCodeInfo = {} - -function MegaMacroCodeInfo.Get(macro) - return CodeInfoCache[macro.Id] or CalculateMacroInfo(macro) -end - -function MegaMacroCodeInfo.Clear(macroId) - CodeInfoCache[macroId] = nil -end - -function MegaMacroCodeInfo.ClearAll() - CodeInfoCache = {} +local CodeInfoCache = {} +--[[ +{ + "macroid": { + { + type: "cast", cast/stopmacro/use/castsequence/showtooltip/fallback + body: "[mod:alt] X; Y" + }, + ... + } +} +--]] + +local function trim(s) + if not s then return "" end + return s:gsub("^%s*(.-)%s*$", "%1") +end + +local function lastIndexOf(str, match, maxIndex) + if not str then return nil end + local index = string.find(str, match) + + if index ~= nil then + local nextIndex = string.find(str, match, index + 1) + while nextIndex and (not maxIndex or nextIndex < maxIndex) do + index = nextIndex + nextIndex = string.find(str, match, index + 1) + end + end + + return index +end + +local function Char(str, index) + if not str then return nil end + if #str >= index then + return string.sub(str, index, index) + end +end + +local function ParseSpaces(parsingContext) + local result = false + while Char(parsingContext.Code, parsingContext.Index) == " " do + result = true + parsingContext.Index = parsingContext.Index + 1 + end + return result +end + +local function ParseEndOfLine(parsingContext) + local result = false + while Char(parsingContext.Code, parsingContext.Index) == "\n" do + result = true + parsingContext.Index = parsingContext.Index + 1 + ParseSpaces(parsingContext) + end + return result +end + +local function ParseWord(parsingContext) + local result = false + local word = "" + + local character = Char(parsingContext.Code, parsingContext.Index) + while character and (string.match(character, "[a-z]") or string.match(character, "[A-Z]") or string.match(character, "[0-9]") or character == "_") do + word = word .. character + result = true + parsingContext.Index = parsingContext.Index + 1 + character = Char(parsingContext.Code, parsingContext.Index) + end + + return result, word +end + +local function GrabRemainingLineCode(parsingContext) + local code = "" + local character = Char(parsingContext.Code, parsingContext.Index) + local appendToCode = true -- ignore comments trailing lines of code + + while character ~= nil and character ~= "\n" do + if character == "#" then + appendToCode = false + end + + if appendToCode then + code = code .. character + end + + parsingContext.Index = parsingContext.Index + 1 + character = Char(parsingContext.Code, parsingContext.Index) + end + + return code +end + +local function ParseCastCommand(parsingContext) + local castCode = trim(GrabRemainingLineCode(parsingContext)) + table.insert( + CodeInfoCache[parsingContext.MacroId], + { + Type = "cast", + Body = castCode + }) +end + +local function ParseCastsequenceCommand(parsingContext) + local body = trim(GrabRemainingLineCode(parsingContext)) + table.insert( + CodeInfoCache[parsingContext.MacroId], + { + Type = "castsequence", + Body = body + }) +end + +local function ParseStopmacroCommand(parsingContext) + local condition = trim(GrabRemainingLineCode(parsingContext)) + table.insert( + CodeInfoCache[parsingContext.MacroId], + { + Type = "stopmacro", + Body = condition.."TRUE" + }) +end + +local function ParsePetCommand(parsingContext, command) + local condition = trim(GrabRemainingLineCode(parsingContext)) + table.insert( + CodeInfoCache[parsingContext.MacroId], + { + Type = "petcommand", + Body = condition.."TRUE", + Command = command + }) +end + +local function ParseEquipSetCommand(parsingContext) + local equipCode = trim(GrabRemainingLineCode(parsingContext)) + table.insert( + CodeInfoCache[parsingContext.MacroId], + { + Type = "equipset", + Body = equipCode + }) +end + +local function ParseClickCommand(parsingContext) + local clickCode = trim(GrabRemainingLineCode(parsingContext)) + + table.insert( + CodeInfoCache[parsingContext.MacroId], + { + Type = "click", + Body = clickCode + }) +end + +local function ParseCommand(parsingContext) + local result = false + + if Char(parsingContext.Code, parsingContext.Index) == "/" then + parsingContext.Index = parsingContext.Index + 1 + local wordResult, word = ParseWord(parsingContext) + + if wordResult then + word = string.lower(word) + result = true + ParseSpaces(parsingContext) + + if word == "cast" or word == "use" then + ParseCastCommand(parsingContext) + elseif word == "castsequence" then + ParseCastsequenceCommand(parsingContext) + elseif word == "stopmacro" then + ParseStopmacroCommand(parsingContext) + elseif word == "petattack" then + ParsePetCommand(parsingContext, "attack") + elseif word == "petassist" then + ParsePetCommand(parsingContext, "assist") + elseif word == "petpassive" then + ParsePetCommand(parsingContext, "passive") + elseif word == "petdefensive" then + ParsePetCommand(parsingContext, "defensive") + elseif word == "petfollow" then + ParsePetCommand(parsingContext, "follow") + elseif word == "petmoveto" then + ParsePetCommand(parsingContext, "moveto") + elseif word == "petstay" then + ParsePetCommand(parsingContext, "stay") + elseif word == "petdismiss" then + ParsePetCommand(parsingContext, "dismiss") + elseif word == "equipset" then + ParseEquipSetCommand(parsingContext) + elseif word == "click" then + ParseClickCommand(parsingContext) + end + end + end + + GrabRemainingLineCode(parsingContext) + ParseEndOfLine(parsingContext) + + return result +end + +local function ParseShowtooltip(parsingContext) + if Char(parsingContext.Code, parsingContext.Index) == "#" then + parsingContext.Index = parsingContext.Index + 1 + local wordResult, word = ParseWord(parsingContext) + + if wordResult and string.lower(word) == "showtooltip" then + local body = trim(GrabRemainingLineCode(parsingContext)) + if #body > 0 then + table.insert( + CodeInfoCache[parsingContext.MacroId], + { + Type = "showtooltip", + Body = body + }) + return true + end + end + end + + return false +end + +local function AddFallbackAbility(macroId) + local codeInfo = CodeInfoCache[macroId] + if not codeInfo then return end + + local codeInfoLength = #codeInfo + + for i=1, codeInfoLength do + local type = codeInfo[i].Type + + if type == "showtooltip" then + break + elseif type == "cast" or type == "use" then + local endOfAbility = string.find(codeInfo[i].Body, ";") + endOfAbility = endOfAbility and (endOfAbility - 1) + local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]", endOfAbility) or 0) + 1 + + local abilityName = trim(string.sub(codeInfo[i].Body, endOfConditions, endOfAbility)) + + table.insert( + codeInfo, + { + Type = "fallbackAbility", + Body = abilityName + }) + break + elseif type == "castsequence" then + local endOfSequence = string.find(codeInfo[i].Body, ";") + endOfSequence = endOfSequence and (endOfSequence - 1) + local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]", endOfSequence) or 0) + 1 + + local sequence = trim(string.sub(codeInfo[i].Body, endOfConditions, endOfSequence)) + + table.insert( + codeInfo, + { + Type = "fallbackSequence", + Body = sequence + }) + break + elseif type == "petcommand" then + table.insert( + codeInfo, + { + Type = "fallbackPetCommand", + Body = codeInfo[i].Command + }) + break + elseif type == "equipset" then + local endOfAbility = string.find(codeInfo[i].Body, ";") + endOfAbility = endOfAbility and (endOfAbility - 1) + local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]", endOfAbility) or 0) + 1 + + local firstSetMentioned = trim(string.sub(codeInfo[i].Body, endOfConditions, endOfAbility)) + + table.insert( + codeInfo, + { + Type = "fallbackEquipSet", + Body = firstSetMentioned + }) + elseif type == "click" then + local endOfConditions = (lastIndexOf(codeInfo[i].Body, "%]") or 0) + 1 + local buttonName = trim(string.sub(codeInfo[i].Body, endOfConditions)) + + table.insert( + codeInfo, + { + Type = "fallbackClick", + Body = buttonName + }) + elseif type == "stopmacro" then + -- ignore + end + end +end + +local function CalculateMacroInfo(macro) + -- 12.0 Safety: Guard against nil macro code to prevent crashes + if not macro or type(macro.Code) ~= "string" then + if macro and macro.Id then + CodeInfoCache[macro.Id] = {} + return CodeInfoCache[macro.Id] + end + return {} + end + + local parsingContext = { MacroId = macro.Id, Index = 1, Code = macro.Code } + CodeInfoCache[macro.Id] = {} + + -- skip to start of code + ParseSpaces(parsingContext) + ParseEndOfLine(parsingContext) + + -- Optimized loop using # operator + if parsingContext.Index < #parsingContext.Code then + if not ParseShowtooltip(parsingContext) then + while parsingContext.Index <= #parsingContext.Code do + ParseCommand(parsingContext) + end + end + end + + AddFallbackAbility(macro.Id) + + return CodeInfoCache[macro.Id] +end + +MegaMacroCodeInfo = {} + +function MegaMacroCodeInfo.Get(macro) + if not macro or not macro.Id then return nil end + return CodeInfoCache[macro.Id] or CalculateMacroInfo(macro) +end + +function MegaMacroCodeInfo.Clear(macroId) + if macroId then + CodeInfoCache[macroId] = nil + end +end + +function MegaMacroCodeInfo.ClearAll() + CodeInfoCache = {} end \ No newline at end of file diff --git a/src/engine/mega-macro-engine.lua b/src/engine/mega-macro-engine.lua index 5db701f..857b677 100644 --- a/src/engine/mega-macro-engine.lua +++ b/src/engine/mega-macro-engine.lua @@ -1,557 +1,655 @@ -MegaMacroEngine = {} -local ClickyFrameName = "MegaMacroClicky" -local MacroIndexCache = {} -- caches native macro indexes - these change based on macro name so they are not the id we'll use in the addon -local Initialized = false - -local function GenerateIdPrefix(id) - local result = "00"..id - return "#"..string.sub(result, -3) -end - -local function FormatMacroDisplayName(megaMacroDisplayName) - if not megaMacroDisplayName or #megaMacroDisplayName == 0 then - return " " - else - return string.sub(megaMacroDisplayName, 1, 18) - end -end - -local function GetIdFromMacroCode(macroCode) - return macroCode and tonumber(string.sub(macroCode, 2, 4)) -end - -local function InitializeMacroIndexCache() - MacroIndexCache = {} - - for i=1, MacroLimits.MaxGlobalMacros do - local _, _, macroCode, _ = GetMacroInfo(i) - - if macroCode then - local macroId = GetIdFromMacroCode(macroCode) - - if macroId then - MacroIndexCache[macroId] = i - end - end - end - - for i=1 + MacroLimits.MaxGlobalMacros, MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros do - local _, _, macroCode, _ = GetMacroInfo(i) - - if macroCode then - local macroId = GetIdFromMacroCode(macroCode) - - if macroId then - MacroIndexCache[macroId] = i - end - end - end -end - -local function GenerateNativeMacroCode(macro) - -- Check if there is #showtooltip already. If not, add it to the start. - local code = macro.Code - if #code <= MegaMacroCodeMaxLengthForNative - 14 then - if not string.find(code, "#showtooltip") then - code = "#showtooltip\n" .. code - end - end - code = GenerateIdPrefix(macro.Id) .. "\n" .. code - return code -end - -local function getTexture(macro, macroIndex) - local macroIndex = macroIndex or MacroIndexCache[macro.Id] - local iconTexture = macro.StaticTexture - if macroIndex and not iconTexture then - local _, iconTexture, _, _ = GetMacroInfo(macroIndex) - end - return iconTexture -end - -local function BindMacro(macro, macroIndex) - local macroIndex = macroIndex or MacroIndexCache[macro.Id] - - -- Bind code to macro - if macroIndex then - local iconTexture = getTexture(macro, macroIndex) - if #macro.Code <= MegaMacroCodeMaxLengthForNative then - EditMacro(macroIndex, FormatMacroDisplayName(macro.DisplayName), iconTexture, GenerateNativeMacroCode(macro), true, macroIndex > MacroLimits.MaxGlobalMacros) - else - MegaMacroEngine.GetOrCreateClicky(macro.Id):SetAttribute("macrotext", macro.Code) - EditMacro(macroIndex, FormatMacroDisplayName(macro.DisplayName), iconTexture, MegaMacroEngine.GetMacroStubCode(macro.Id), true, macroIndex > MacroLimits.MaxGlobalMacros) - end - InitializeMacroIndexCache() - end -end - -local function BindNewMacro(macro, macroIndex) - local macroIndex = macroIndex or MacroIndexCache[macro.Id] - - if not macroIndex then - -- Find a free slot. Need to know if global or character - local isGlobal = macro.Scope == MegaMacroScopes.Global or macro.Scope == MegaMacroScopes.Class or macro.Scope == MegaMacroScopes.Specialization - macroIndex = isGlobal and MegaMacroEngine.FindAvailableGlobalMacro() or MegaMacroEngine.FindAvailableCharacterMacro() - end - -- Bind code to macro - BindMacro(macro, macroIndex) -end - -local function TryImportGlobalMacros() - local numberOfGlobalMacros = GetNumMacros() - - for i=1, numberOfGlobalMacros do - local name, iconTexture, body, _ = GetMacroInfo(i) - -- First, is it already a Mega Macro? - local macroId = GetIdFromMacroCode(body) - - if not macroId then - local macro = MegaMacro.Create(name, MegaMacroScopes.Global, iconTexture or MegaMacroTexture, true, body, i) - - if macro == nil then - macro = MegaMacro.Create(name, MegaMacroScopes.Inactive, iconTexture or MegaMacroTexture, true, body, i) - if macro == nil then - return false, "Failed to import at macro " .. i .. "(" .. name .. "). Please delete the macro and reload your UI." - end - -- print("Importing Inactive Global macro " .. i .. " ".. name .. " to " .. " #" .. macro.Id) - end - end - end - local newNumberOfGlobalMacros = GetNumMacros() - if newNumberOfGlobalMacros > numberOfGlobalMacros then - print("Mega Macro: Global import created " .. newNumberOfGlobalMacros - numberOfGlobalMacros .. " macros.") - end - - return true -end - -local function TryImportCharacterMacros() - local _, numberOfCharacterMacros = GetNumMacros() - - for i=1 + MacroIndexOffsets.NativeCharacterMacros, numberOfCharacterMacros + MacroIndexOffsets.NativeCharacterMacros do - local name, iconTexture, body, _ = GetMacroInfo(i) - -- First, is it already a Mega Macro? - local macroId = GetIdFromMacroCode(body) - - if not macroId then - local macro = MegaMacro.Create(name, MegaMacroScopes.Character, iconTexture or MegaMacroTexture, true, body, i) - - if macro == nil then - macro = MegaMacro.Create(name, MegaMacroScopes.Inactive, iconTexture or MegaMacroTexture, true, body, i) - if macro == nil then - return false, "Failed to import at macro " .. i .. "(" .. name .. "). Please delete the macro and reload your UI." - end - -- print("Importing Inactive Char macro " .. i .. " ".. name .. " to " .. " #" .. macro.Id) - end - end - end - local _, newNumberOfCharacterMacros = GetNumMacros() - if newNumberOfCharacterMacros > numberOfCharacterMacros then - print("Mega Macro: Character import created " .. newNumberOfCharacterMacros - numberOfCharacterMacros .. " macros.") - end - - return true -end - -local function MergeCharacterSpecializationMacros() - -- Remove the character specialization macros and add them to character macros. - local characterSpecializationMacros = MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros - local characterMacros = MegaMacroCharacterData.Macros - if not characterSpecializationMacros or #characterSpecializationMacros == 0 then - return - end - for i=1, #characterSpecializationMacros do - -- if we don't have room, move to inactive - if #characterMacros >= MacroLimits.MaxCharacterMacros then - local macro = characterSpecializationMacros[i] - macro.Scope = MegaMacroScopes.Inactive - macro.Id = MegaMacro.GetNextAvailableMacroId(MacroIndexOffsets.Inactive, MacroLimits.InactiveCount, MegaMacroGlobalData.InactiveMacros) - table.insert(MegaMacroGlobalData.InactiveMacros, macro) - else - local macro = characterSpecializationMacros[i] - macro.Scope = MegaMacroScopes.Character - macro.Id = MegaMacro.GetNextAvailableMacroId(MacroIndexOffsets.NativeCharacterMacros, MacroLimits.MaxCharacterMacros, MegaMacroCharacterData.Macros) - table.insert(characterMacros, macro) - end - end - MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros = {} -end - - -function MegaMacroEngine.GetMacroStubCode(macroId) - -- Fix a bug that causes click events not to register only when CVar ActionButtonUseKeyDown is set to 1. - local keyDownOrUp = GetCVar("ActionButtonUseKeyDown") - local primaryMacroButtonClickValue = keyDownOrUp == "1" and " LeftButton" or "" - return - GenerateIdPrefix(macroId).."\n".. - "/click [btn:1] "..ClickyFrameName..macroId..primaryMacroButtonClickValue.." "..keyDownOrUp.."\n".. - "/click [btn:2] "..ClickyFrameName..macroId.." RightButton "..keyDownOrUp.."\n".. - "/click [btn:3] "..ClickyFrameName..macroId.." MiddleButton "..keyDownOrUp.."\n".. - "/click [btn:4] "..ClickyFrameName..macroId.." Button4 "..keyDownOrUp.."\n".. - "/click [btn:5] "..ClickyFrameName..macroId.." Button5 "..keyDownOrUp.."\n" -end - -function MegaMacroEngine.FindAvailableGlobalMacro() - if not InCombatLockdown() then - local globalCount, characterCount = GetNumMacros() - - -- Find used indexes from MacroIndexCache - local usedMacroIndexes = {} - for _, index in pairs(MacroIndexCache) do - usedMacroIndexes[index] = true - end - - local startIndex = 1 - local endIndex = MacroLimits.MaxGlobalMacros - - -- If there is a free slot, use that first. - local hasFreeSlot = globalCount < MacroLimits.MaxGlobalMacros - if hasFreeSlot then - return CreateMacro(" ", MegaMacroTexture, " ", false) - end - - for i=startIndex, endIndex do - if not usedMacroIndexes[i] then - return i - end - end - -- Didn't find a free slot. Try to find an inactive one. - for i=startIndex, endIndex do - if usedMacroIndexes[i] and MegaMacroEngine.GetMacroIdFromIndex(i) > MacroIndexOffsets.Inactive then - return i - end - end - print("Mega Macro: Failed to find available global macro slot.") - return nil - end -end - -function MegaMacroEngine.FindAvailableCharacterMacro() - if not InCombatLockdown() then - local globalCount, characterCount = GetNumMacros() - - -- Find used indexes from MacroIndexCache - local usedMacroIndexes = {} - for _, index in pairs(MacroIndexCache) do - usedMacroIndexes[index] = true - end - - local startIndex = 1 + MacroIndexOffsets.NativeCharacterMacros - local endIndex = MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros - - -- If there is a free slot, use that first. Otherwise, return the first one that isn't indexed. - local hasFreeSlot = characterCount < MacroLimits.MaxCharacterMacros - if hasFreeSlot then - local index = CreateMacro(" ", MegaMacroTexture, " ", true) - -- print("Mega Macro: Created new character macro at index " .. index .. ".") - return index - end - - for i=startIndex, endIndex do - if not usedMacroIndexes[i] then - return i - end - end - - return nil - end -end - -function MegaMacroEngine.GetOrCreateClicky(macroId) - local name = ClickyFrameName..macroId - local clicky = _G[name] - - if not clicky then - clicky = CreateFrame("Button", name, UIParent, "SecureActionButtonTemplate") - clicky:SetAttribute("type", "macro") - clicky:SetAttribute("macrotext", "") - end - - return clicky -end - -local function UnbindMacro(macro) - if Initialized then - local macroIndex = MacroIndexCache[macro.Id] - - if macroIndex then - MegaMacroEngine.GetOrCreateClicky(macro.Id):SetAttribute("macrotext", "") - EditMacro(macroIndex, " ", nil, GenerateIdPrefix(macro.Id), true, macroIndex > MacroLimits.MaxGlobalMacros) - InitializeMacroIndexCache() - end - end -end - -local function BindMacrosList(macroList) - local count = #macroList - for i=1, count do - BindMacro(macroList[i]) - end -end - -local function UnbindMacrosList(macroList) - local count = #macroList - for i=1, count do - UnbindMacro(macroList[i]) - end -end - -local function BindMacros() - BindMacrosList(MegaMacroGlobalData.Macros) - - if MegaMacroGlobalData.Classes[MegaMacroCachedClass] then - BindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros) - - if MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] then - BindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros) - end - end - - BindMacrosList(MegaMacroCharacterData.Macros) - - if MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] then - BindMacrosList(MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros) - end -end - -local function PickupMacroWrapper(original, macroIndex) - if InCombatLockdown() then - return - end - if MegaMacroConfig['UseNativeActionBar'] then - original(macroIndex) - return - end - - local macroId = macroIndex and MegaMacroEngine.GetMacroIdFromIndex(macroIndex) - - if macroId then - local data = MegaMacroIconEvaluator.GetCachedData(macroId) - EditMacro(macroIndex, nil, data and data.Icon, nil, true, macroIndex > MacroLimits.MaxGlobalMacros) - end - - original(macroIndex) - - -- revert icon so that if a macro is dragged during combat, it will show the blank icon instead of an out-of-date macro icon - if macroId then - EditMacro(macroIndex, nil, MegaMacroTexture, nil, true, macroIndex > MacroLimits.MaxGlobalMacros) - end -end - - - -function MegaMacroEngine.SafeInitialize() - if InCombatLockdown() then - return false - end - - MergeCharacterSpecializationMacros() - - InitializeMacroIndexCache() - Initialized = true - - BindMacros() - - local originalPickupMacro = PickupMacro - PickupMacro = function(macroIndex) PickupMacroWrapper(originalPickupMacro, macroIndex) end - - return true -end - -function MegaMacroEngine.VerifyMacros() - -- Verify all is well. Check for macros in wrong space, or duplicate macros. - local numberOfGlobalMacros, numberOfCharacterMacros = GetNumMacros() - - for i=1, numberOfGlobalMacros do - local name, _, body, _ = GetMacroInfo(i) - local macroId = GetIdFromMacroCode(body) - - if macroId then - if macroId > MacroLimits.MaxGlobalMacros and macroId < MacroIndexOffsets.Inactive then - print("Mega Macro: Found character macro in global space! " .. i .. " " .. name .. " #" .. macroId) - DeleteMacro(i) - end - end - end - - - for i=1 + MacroIndexOffsets.NativeCharacterMacros, numberOfCharacterMacros + MacroIndexOffsets.NativeCharacterMacros do - local name, _, body, _ = GetMacroInfo(i) - local macroId = GetIdFromMacroCode(body) - - if macroId then - if macroId < MacroIndexOffsets.NativeCharacterMacros then - print("Mega Macro: Found global macro in character space! " .. i .. " " .. name .. " #" .. macroId) - DeleteMacro(i) - end - end - end - - InitializeMacroIndexCache() - -- Verify that every macro in the addon is in the macro index cache. If not, we need to make a new macro. Also check duplicates - local macroIds = {} - local function VerifyMacro(macro, i) - local macroId = macro.Id - local macroIndex = MacroIndexCache[macro.Id] - -- Check for duplicate macroIds - if macroIds[macroId] then - -- print("Mega Macro: Found duplicate macroId " .. macroId .. " " .. macro.DisplayName) - else - macroIds[macroId] = true - end - -- Check for missing macroIndex - if not macroIndex then - print("Mega Macro: Fixing missing macro " .. macro.Id .. " " .. macro.DisplayName) - BindNewMacro(macro) - end - - return true - end - - for i=1, #MegaMacroGlobalData.Macros do - VerifyMacro(MegaMacroGlobalData.Macros[i], i) - end - - for i=1, #MegaMacroCharacterData.Macros do - VerifyMacro(MegaMacroCharacterData.Macros[i], i) - end - - if MegaMacroGlobalData.Classes[MegaMacroCachedClass] then - for i=1, #MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros do - VerifyMacro(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros[i], i) - end - - if MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] then - for i=1, #MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros do - VerifyMacro(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros[i], i) - end - end - end - - InitializeMacroIndexCache() - -end - -function MegaMacroEngine.ImportMacros() - if InCombatLockdown() then - return false - end - - local importSuccessful, errorMessage = TryImportGlobalMacros() - if importSuccessful then - MegaMacroGlobalData.Activated = true - else - message(errorMessage) - end - - local importSuccessful, errorMessage = TryImportCharacterMacros() - if importSuccessful then - MegaMacroCharacterData.Activated = true - else - message(errorMessage) - end - - InitializeMacroIndexCache() -end - -function MegaMacroEngine.GetMacroIdFromIndex(macroIndex) - for id, index in pairs(MacroIndexCache) do - if index == macroIndex then - return id - end - end - - return nil -end - -function MegaMacroEngine.GetMacroIndexFromId(macroId) - return MacroIndexCache[macroId] -end - -function MegaMacroEngine.OnMacroCreated(macro, macroIndex) - BindNewMacro(macro, macroIndex) -end - -function MegaMacroEngine.OnMacroRenamed(macro) - BindMacro(macro) -end - -function MegaMacroEngine.OnMacroUpdated(macro) - BindMacro(macro) -end - -function MegaMacroEngine.OnMacroDeleted(macro) - -- unbind the macro from any action bar slots its bound to - if not InCombatLockdown() then - for i=1, 120 do - local type, id = GetActionInfo(i) - if type == "macro" and MegaMacroEngine.GetMacroIdFromIndex(id) == macro.Id then - PickupAction(i) - ClearCursor() - end - end - end - - UnbindMacro(macro) -end - -function MegaMacroEngine.OnMacroMoved(oldMacro, newMacro) - -- update binding from old macro to new macro (move is actually a create+delete) - if not InCombatLockdown() then - for i=1, 120 do - local type, id = GetActionInfo(i) - if type == "macro" and MegaMacroEngine.GetMacroIdFromIndex(id) == oldMacro.Id then - PickupMacro(MacroIndexCache[newMacro.Id]) - PlaceAction(i) - ClearCursor() - end - end - end -end - -function MegaMacroEngine.OnSpecializationChanged(oldValue, newValue) - UnbindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[oldValue].Macros) - UnbindMacrosList(MegaMacroCharacterData.Specializations[oldValue].Macros) - - BindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[newValue].Macros) - BindMacrosList(MegaMacroCharacterData.Specializations[newValue].Macros) -end - -function MegaMacroEngine.Uninstall() - if InCombatLockdown() then - return false - end - - MegaMacroGlobalData.Activated = false - MegaMacroCharacterData.Activated = false - - -- Loop every macro and remove the prefix - for i=1, MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros do - local _, _, code, _ = GetMacroInfo(i) - local macroId = GetIdFromMacroCode(code) - local macroName, _, _, _ = GetMacroInfo(i) - - - if macroId then - local cleanCode = string.sub(code, 5) - --If it is stubcode, replace with what we can. - local macro = MegaMacro.GetById(macroId) - if macro and #macro.Code > MegaMacroCodeMaxLengthForNative then - cleanCode = macro.Code - end - - local iconTexture = getTexture(macro, i) - EditMacro(i, macroName, iconTexture, cleanCode, true, i > MacroLimits.MaxGlobalMacros) - end - end - -- Now clear MegaMacro Global, Character, and Spec data - MegaMacroGlobalData.Macros = {} - MegaMacroCharacterData.Macros = {} - MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros = {} - MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros = {} - MegaMacroGlobalData.InactiveMacros = {} - - InitializeMacroIndexCache() - message("Mega Macro: Uninstalled. Disabled MegaMacro and reload your UI.") - return true +MegaMacroEngine = {} +local ClickyFrameName = "MegaMacroClicky" +local MacroIndexCache = {} -- caches native macro indexes +local Initialized = false + +-- 12.0 Compatibility Helpers +local function GetMacroInfo(index) + -- Try C_Macro first (12.0 standard) + if C_Macro and C_Macro.GetMacroInfo then + local info = C_Macro.GetMacroInfo(index) + if info then + return info.name, info.icon, info.body, info.isLocal + end + end + -- Fallback for legacy/global + if _G.GetMacroInfo then + return _G.GetMacroInfo(index) + end +end + +local function GetNumMacros() + if C_Macro and C_Macro.GetNumMacros then + return C_Macro.GetNumMacros() + end + return _G.GetNumMacros() +end + +local function EditMacro(...) + if C_Macro and C_Macro.EditMacro then + return C_Macro.EditMacro(...) + end + return _G.EditMacro(...) +end + +local function CreateMacro(...) + if C_Macro and C_Macro.CreateMacro then + return C_Macro.CreateMacro(...) + end + return _G.CreateMacro(...) +end + +local function DeleteMacro(...) + if C_Macro and C_Macro.DeleteMacro then + return C_Macro.DeleteMacro(...) + end + return _G.DeleteMacro(...) +end + +local function PickupMacro(...) + if C_Macro and C_Macro.PickupMacro then + return C_Macro.PickupMacro(...) + end + return _G.PickupMacro(...) +end + +local function GetActionInfo(slot) + if C_ActionBar and C_ActionBar.GetActionInfo then + return C_ActionBar.GetActionInfo(slot) + end + return _G.GetActionInfo(slot) +end + +local function PickupAction(slot) + if C_ActionBar and C_ActionBar.PickupAction then + return C_ActionBar.PickupAction(slot) + end + return _G.PickupAction(slot) +end + +local function PlaceAction(slot) + if C_ActionBar and C_ActionBar.PlaceAction then + return C_ActionBar.PlaceAction(slot) + end + return _G.PlaceAction(slot) +end + + +local function GenerateIdPrefix(id) + local result = "00"..id + return "#"..string.sub(result, -3) +end + +local function FormatMacroDisplayName(megaMacroDisplayName) + if not megaMacroDisplayName or #megaMacroDisplayName == 0 then + return " " + else + return string.sub(megaMacroDisplayName, 1, 18) + end +end + +local function GetIdFromMacroCode(macroCode) + if not macroCode or type(macroCode) ~= "string" then return nil end + return tonumber(string.sub(macroCode, 2, 4)) +end + +local function InitializeMacroIndexCache() + MacroIndexCache = {} + + -- 12.0: C_Macro.GetMacroInfo might return nil for empty slots, handled by helper + for i=1, MacroLimits.MaxGlobalMacros do + local _, _, macroCode = GetMacroInfo(i) + if macroCode then + local macroId = GetIdFromMacroCode(macroCode) + if macroId then + MacroIndexCache[macroId] = i + end + end + end + + for i=1 + MacroLimits.MaxGlobalMacros, MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros do + local _, _, macroCode = GetMacroInfo(i) + if macroCode then + local macroId = GetIdFromMacroCode(macroCode) + if macroId then + MacroIndexCache[macroId] = i + end + end + end +end + +local function GenerateNativeMacroCode(macro) + -- Check if there is #showtooltip already. If not, add it to the start. + local code = macro.Code or "" + if #code <= MegaMacroCodeMaxLengthForNative - 14 then + if not string.find(code, "#showtooltip") then + code = "#showtooltip\n" .. code + end + end + code = GenerateIdPrefix(macro.Id) .. "\n" .. code + return code +end + +local function getTexture(macro, macroIndex) + local macroIndex = macroIndex or MacroIndexCache[macro.Id] + local iconTexture = macro.StaticTexture + + if macroIndex and not iconTexture then + local _, indexIcon = GetMacroInfo(macroIndex) + iconTexture = indexIcon + end + return iconTexture +end + +function MegaMacroEngine.GetOrCreateClicky(macroId) + local name = ClickyFrameName..macroId + local clicky = _G[name] + + if not clicky then + -- 12.0: SecureActionButtonTemplate requires strict inheritance + clicky = CreateFrame("Button", name, UIParent, "SecureActionButtonTemplate") + clicky:SetAttribute("type", "macro") + clicky:SetAttribute("macrotext", "") + end + + return clicky +end + +function MegaMacroEngine.GetMacroStubCode(macroId) + -- Fix a bug that causes click events not to register only when CVar ActionButtonUseKeyDown is set to 1. + local keyDownOrUp = GetCVar("ActionButtonUseKeyDown") + local primaryMacroButtonClickValue = keyDownOrUp == "1" and " LeftButton" or "" + return + GenerateIdPrefix(macroId).."\n".. + "/click [btn:1] "..ClickyFrameName..macroId..primaryMacroButtonClickValue.." "..keyDownOrUp.."\n".. + "/click [btn:2] "..ClickyFrameName..macroId.." RightButton "..keyDownOrUp.."\n".. + "/click [btn:3] "..ClickyFrameName..macroId.." MiddleButton "..keyDownOrUp.."\n".. + "/click [btn:4] "..ClickyFrameName..macroId.." Button4 "..keyDownOrUp.."\n".. + "/click [btn:5] "..ClickyFrameName..macroId.." Button5 "..keyDownOrUp.."\n" +end + +local function BindMacro(macro, macroIndex) + -- 12.0 Safety: Cannot edit macros or set attributes in combat + if InCombatLockdown() then return end + + local macroIndex = macroIndex or MacroIndexCache[macro.Id] + + -- Bind code to macro + if macroIndex then + local iconTexture = getTexture(macro, macroIndex) + local macroCode = macro.Code or "" + + if #macroCode <= MegaMacroCodeMaxLengthForNative then + EditMacro(macroIndex, FormatMacroDisplayName(macro.DisplayName), iconTexture, GenerateNativeMacroCode(macro), true, macroIndex > MacroLimits.MaxGlobalMacros) + -- Clean up clicky if it existed previously + local clicky = _G[ClickyFrameName..macro.Id] + if clicky then clicky:SetAttribute("macrotext", "") end + else + -- Extended Macro logic (>255 chars) + local clicky = MegaMacroEngine.GetOrCreateClicky(macro.Id) + clicky:SetAttribute("macrotext", macroCode) + EditMacro(macroIndex, FormatMacroDisplayName(macro.DisplayName), iconTexture, MegaMacroEngine.GetMacroStubCode(macro.Id), true, macroIndex > MacroLimits.MaxGlobalMacros) + end + InitializeMacroIndexCache() + end +end + +local function BindNewMacro(macro, macroIndex) + if InCombatLockdown() then return end + + local macroIndex = macroIndex or MacroIndexCache[macro.Id] + + if not macroIndex then + -- Find a free slot. Need to know if global or character + local isGlobal = macro.Scope == MegaMacroScopes.Global or macro.Scope == MegaMacroScopes.Class or macro.Scope == MegaMacroScopes.Specialization + macroIndex = isGlobal and MegaMacroEngine.FindAvailableGlobalMacro() or MegaMacroEngine.FindAvailableCharacterMacro() + end + -- Bind code to macro + if macroIndex then + BindMacro(macro, macroIndex) + end +end + +-- Import Logic Wrappers +local function TryImportGlobalMacros() + local numberOfGlobalMacros = GetNumMacros() + + for i=1, numberOfGlobalMacros do + local name, iconTexture, body = GetMacroInfo(i) + -- First, is it already a Mega Macro? + local macroId = GetIdFromMacroCode(body) + + if not macroId and body then + local macro = MegaMacro.Create(name, MegaMacroScopes.Global, iconTexture or MegaMacroTexture, true, body, i) + + if macro == nil then + macro = MegaMacro.Create(name, MegaMacroScopes.Inactive, iconTexture or MegaMacroTexture, true, body, i) + if macro == nil then + return false, "Failed to import at macro " .. i .. "(" .. (name or "?") .. "). Please delete the macro and reload your UI." + end + end + end + end + + local newNumberOfGlobalMacros = GetNumMacros() + if newNumberOfGlobalMacros > numberOfGlobalMacros then + print("Mega Macro: Global import created " .. newNumberOfGlobalMacros - numberOfGlobalMacros .. " macros.") + end + + return true +end + +local function TryImportCharacterMacros() + local _, numberOfCharacterMacros = GetNumMacros() + + for i=1 + MacroIndexOffsets.NativeCharacterMacros, numberOfCharacterMacros + MacroIndexOffsets.NativeCharacterMacros do + local name, iconTexture, body = GetMacroInfo(i) + + local macroId = GetIdFromMacroCode(body) + + if not macroId and body then + local macro = MegaMacro.Create(name, MegaMacroScopes.Character, iconTexture or MegaMacroTexture, true, body, i) + + if macro == nil then + macro = MegaMacro.Create(name, MegaMacroScopes.Inactive, iconTexture or MegaMacroTexture, true, body, i) + if macro == nil then + return false, "Failed to import at macro " .. i .. "(" .. (name or "?") .. "). Please delete the macro and reload your UI." + end + end + end + end + + local _, newNumberOfCharacterMacros = GetNumMacros() + if newNumberOfCharacterMacros > numberOfCharacterMacros then + print("Mega Macro: Character import created " .. newNumberOfCharacterMacros - numberOfCharacterMacros .. " macros.") + end + + return true +end + +local function MergeCharacterSpecializationMacros() + local characterSpecializationMacros = MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros + local characterMacros = MegaMacroCharacterData.Macros + + if not characterSpecializationMacros or #characterSpecializationMacros == 0 then + return + end + + for i=1, #characterSpecializationMacros do + local macro = characterSpecializationMacros[i] + + if #characterMacros >= MacroLimits.MaxCharacterMacros then + macro.Scope = MegaMacroScopes.Inactive + macro.Id = MegaMacro.GetNextAvailableMacroId(MacroIndexOffsets.Inactive, MacroLimits.InactiveCount, MegaMacroGlobalData.InactiveMacros) + table.insert(MegaMacroGlobalData.InactiveMacros, macro) + else + macro.Scope = MegaMacroScopes.Character + macro.Id = MegaMacro.GetNextAvailableMacroId(MacroIndexOffsets.NativeCharacterMacros, MacroLimits.MaxCharacterMacros, MegaMacroCharacterData.Macros) + table.insert(characterMacros, macro) + end + end + MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros = {} +end + +function MegaMacroEngine.FindAvailableGlobalMacro() + if InCombatLockdown() then return nil end + + local globalCount = GetNumMacros() -- returns (global, perChar) + + local usedMacroIndexes = {} + for _, index in pairs(MacroIndexCache) do + usedMacroIndexes[index] = true + end + + local startIndex = 1 + local endIndex = MacroLimits.MaxGlobalMacros + + local hasFreeSlot = globalCount < MacroLimits.MaxGlobalMacros + if hasFreeSlot then + return CreateMacro(" ", MegaMacroTexture, " ", false) + end + + for i=startIndex, endIndex do + if not usedMacroIndexes[i] then + return i + end + end + + for i=startIndex, endIndex do + if usedMacroIndexes[i] and MegaMacroEngine.GetMacroIdFromIndex(i) > MacroIndexOffsets.Inactive then + return i + end + end + + print("Mega Macro: Failed to find available global macro slot.") + return nil +end + +function MegaMacroEngine.FindAvailableCharacterMacro() + if InCombatLockdown() then return nil end + + local _, characterCount = GetNumMacros() + + local usedMacroIndexes = {} + for _, index in pairs(MacroIndexCache) do + usedMacroIndexes[index] = true + end + + local startIndex = 1 + MacroIndexOffsets.NativeCharacterMacros + local endIndex = MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros + + local hasFreeSlot = characterCount < MacroLimits.MaxCharacterMacros + if hasFreeSlot then + local index = CreateMacro(" ", MegaMacroTexture, " ", true) + return index + end + + for i=startIndex, endIndex do + if not usedMacroIndexes[i] then + return i + end + end + + return nil +end + +local function UnbindMacro(macro) + if Initialized and not InCombatLockdown() then + local macroIndex = MacroIndexCache[macro.Id] + + if macroIndex then + local clicky = _G[ClickyFrameName..macro.Id] + if clicky then + clicky:SetAttribute("macrotext", "") + end + EditMacro(macroIndex, " ", nil, GenerateIdPrefix(macro.Id), true, macroIndex > MacroLimits.MaxGlobalMacros) + InitializeMacroIndexCache() + end + end +end + +local function BindMacrosList(macroList) + if not macroList then return end + local count = #macroList + for i=1, count do + BindMacro(macroList[i]) + end +end + +local function UnbindMacrosList(macroList) + if not macroList then return end + local count = #macroList + for i=1, count do + UnbindMacro(macroList[i]) + end +end + +local function BindMacros() + if InCombatLockdown() then return end + + BindMacrosList(MegaMacroGlobalData.Macros) + + if MegaMacroCachedClass and MegaMacroGlobalData.Classes[MegaMacroCachedClass] then + BindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros) + + if MegaMacroCachedSpecialization and MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] then + BindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros) + end + end + + BindMacrosList(MegaMacroCharacterData.Macros) + + if MegaMacroCachedSpecialization and MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] then + BindMacrosList(MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros) + end +end + +local function PickupMacroWrapper(original, macroIndex) + if InCombatLockdown() then return end + + if MegaMacroConfig and MegaMacroConfig['UseNativeActionBar'] then + original(macroIndex) + return + end + + local macroId = macroIndex and MegaMacroEngine.GetMacroIdFromIndex(macroIndex) + + if macroId then + local data = MegaMacroIconEvaluator.GetCachedData(macroId) + EditMacro(macroIndex, nil, data and data.Icon, nil, true, macroIndex > MacroLimits.MaxGlobalMacros) + end + + original(macroIndex) + + -- revert icon so that if a macro is dragged during combat, it will show the blank icon instead of an out-of-date macro icon + if macroId then + EditMacro(macroIndex, nil, MegaMacroTexture, nil, true, macroIndex > MacroLimits.MaxGlobalMacros) + end +end + +function MegaMacroEngine.SafeInitialize() + if InCombatLockdown() then return false end + + MergeCharacterSpecializationMacros() + InitializeMacroIndexCache() + Initialized = true + BindMacros() + + -- Hook PickupMacro + local originalPickupMacro = PickupMacro + -- Reassign the global to our wrapper? No, hook it or replace the pointer in this scope? + -- The original code tried to overwrite the Global. In 12.0 this is risky but standard for this type of addon. + if _G.PickupMacro then + _G.PickupMacro = function(macroIndex) PickupMacroWrapper(originalPickupMacro, macroIndex) end + end + + -- Also hook C_Macro if it exists + if C_Macro and C_Macro.PickupMacro then + local originalCPickup = C_Macro.PickupMacro + C_Macro.PickupMacro = function(macroIndex) PickupMacroWrapper(originalCPickup, macroIndex) end + end + + return true +end + +function MegaMacroEngine.VerifyMacros() + local numberOfGlobalMacros = GetNumMacros() + + for i=1, numberOfGlobalMacros do + local name, _, body = GetMacroInfo(i) + local macroId = GetIdFromMacroCode(body) + + if macroId then + if macroId > MacroLimits.MaxGlobalMacros and macroId < MacroIndexOffsets.Inactive then + print("Mega Macro: Found character macro in global space! " .. i .. " " .. (name or "") .. " #" .. macroId) + DeleteMacro(i) + end + end + end + + local _, numberOfCharacterMacros = GetNumMacros() + for i=1 + MacroIndexOffsets.NativeCharacterMacros, numberOfCharacterMacros + MacroIndexOffsets.NativeCharacterMacros do + local name, _, body = GetMacroInfo(i) + local macroId = GetIdFromMacroCode(body) + + if macroId then + if macroId < MacroIndexOffsets.NativeCharacterMacros then + print("Mega Macro: Found global macro in character space! " .. i .. " " .. (name or "") .. " #" .. macroId) + DeleteMacro(i) + end + end + end + + InitializeMacroIndexCache() + + local macroIds = {} + local function VerifyMacro(macro) + if not macro then return end + local macroId = macro.Id + local macroIndex = MacroIndexCache[macro.Id] + + if macroIds[macroId] then + -- Duplicate detected + else + macroIds[macroId] = true + end + + if not macroIndex then + print("Mega Macro: Fixing missing macro " .. macro.Id .. " " .. (macro.DisplayName or "")) + BindNewMacro(macro) + end + return true + end + + for i=1, #MegaMacroGlobalData.Macros do VerifyMacro(MegaMacroGlobalData.Macros[i]) end + for i=1, #MegaMacroCharacterData.Macros do VerifyMacro(MegaMacroCharacterData.Macros[i]) end + + if MegaMacroCachedClass and MegaMacroGlobalData.Classes[MegaMacroCachedClass] then + for i=1, #MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros do + VerifyMacro(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros[i]) + end + if MegaMacroCachedSpecialization and MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] then + for i=1, #MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros do + VerifyMacro(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros[i]) + end + end + end + + InitializeMacroIndexCache() +end + +function MegaMacroEngine.ImportMacros() + if InCombatLockdown() then return false end + + local importSuccessful, errorMessage = TryImportGlobalMacros() + if importSuccessful then + MegaMacroGlobalData.Activated = true + elseif errorMessage then + message(errorMessage) + end + + importSuccessful, errorMessage = TryImportCharacterMacros() + if importSuccessful then + MegaMacroCharacterData.Activated = true + elseif errorMessage then + message(errorMessage) + end + + InitializeMacroIndexCache() +end + +function MegaMacroEngine.GetMacroIdFromIndex(macroIndex) + for id, index in pairs(MacroIndexCache) do + if index == macroIndex then + return id + end + end + return nil +end + +function MegaMacroEngine.GetMacroIndexFromId(macroId) + return MacroIndexCache[macroId] +end + +function MegaMacroEngine.OnMacroCreated(macro, macroIndex) + BindNewMacro(macro, macroIndex) +end + +function MegaMacroEngine.OnMacroRenamed(macro) + BindMacro(macro) +end + +function MegaMacroEngine.OnMacroUpdated(macro) + BindMacro(macro) +end + +function MegaMacroEngine.OnMacroDeleted(macro) + if InCombatLockdown() then + print("Mega Macro: Cannot delete macros in combat.") + return + end + + for i=1, 120 do + local type, id = GetActionInfo(i) + if type == "macro" and MegaMacroEngine.GetMacroIdFromIndex(id) == macro.Id then + PickupAction(i) + ClearCursor() + end + end + + UnbindMacro(macro) +end + +function MegaMacroEngine.OnMacroMoved(oldMacro, newMacro) + if InCombatLockdown() then return end + + for i=1, 120 do + local type, id = GetActionInfo(i) + if type == "macro" and MegaMacroEngine.GetMacroIdFromIndex(id) == oldMacro.Id then + PickupMacro(MacroIndexCache[newMacro.Id]) + PlaceAction(i) + ClearCursor() + end + end +end + +function MegaMacroEngine.OnSpecializationChanged(oldValue, newValue) + if InCombatLockdown() then return end + if not MegaMacroCachedClass then return end + + if oldValue and MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[oldValue] then + UnbindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[oldValue].Macros) + end + if oldValue and MegaMacroCharacterData.Specializations[oldValue] then + UnbindMacrosList(MegaMacroCharacterData.Specializations[oldValue].Macros) + end + + if newValue and MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[newValue] then + BindMacrosList(MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[newValue].Macros) + end + if newValue and MegaMacroCharacterData.Specializations[newValue] then + BindMacrosList(MegaMacroCharacterData.Specializations[newValue].Macros) + end +end + +function MegaMacroEngine.Uninstall() + if InCombatLockdown() then return false end + + MegaMacroGlobalData.Activated = false + MegaMacroCharacterData.Activated = false + + -- Loop every macro and remove the prefix + for i=1, MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros do + local _, _, code = GetMacroInfo(i) + local macroName = GetMacroInfo(i) -- get name + + local macroId = GetIdFromMacroCode(code) + + if macroId then + local cleanCode = string.sub(code or "", 5) + local macro = MegaMacro.GetById(macroId) + + if macro and macro.Code and #macro.Code > MegaMacroCodeMaxLengthForNative then + cleanCode = macro.Code + end + + local iconTexture = getTexture(macro, i) + EditMacro(i, macroName, iconTexture, cleanCode, true, i > MacroLimits.MaxGlobalMacros) + end + end + + -- Now clear data + MegaMacroGlobalData.Macros = {} + MegaMacroCharacterData.Macros = {} + if MegaMacroCachedClass then + MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros = {} + if MegaMacroCachedSpecialization then + MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros = {} + end + end + MegaMacroGlobalData.InactiveMacros = {} + + InitializeMacroIndexCache() + message("Mega Macro: Uninstalled. Disabled MegaMacro and reload your UI.") + return true end \ No newline at end of file diff --git a/src/engine/mega-macro-icon-evaluator.lua b/src/engine/mega-macro-icon-evaluator.lua index 517fdb0..ab9cf42 100644 --- a/src/engine/mega-macro-icon-evaluator.lua +++ b/src/engine/mega-macro-icon-evaluator.lua @@ -1,356 +1,381 @@ -local MacrosToUpdatePerMs = 2 -local LastMacroScope = MegaMacroScopes.Global -local LastMacroList = nil -local LastMacroIndex = 0 - -local IconUpdatedCallbacks = {} -local MacroEffectData = {} -- { Type = "spell" or "item" or "equipment set" or other, Name = "", Icon = 0, Target = "" } - -local function GetTextureFromPetCommand(command) - if command == "dismiss" then - return PetActionTextures.Dismiss - elseif command == "attack" then - return PetActionTextures.Attack - elseif command == "assist" then - return PetActionTextures.Assist - elseif command == "passive" then - return PetActionTextures.Passive - elseif command == "defensive" then - return PetActionTextures.Defensive - elseif command == "follow" then - return PetActionTextures.Follow - elseif command == "moveto" then - return PetActionTextures.MoveTo - elseif command == "stay" then - return PetActionTextures.Stay - end -end - -local function IterateNextMacroInternal(nextScopeAttempts) - LastMacroIndex = LastMacroIndex + 1 - - if LastMacroIndex > #LastMacroList then - -- limit the recursive iteration to going through each scope once - if nextScopeAttempts > 5 then - return false - end - - if LastMacroScope == MegaMacroScopes.Global then - LastMacroScope = MegaMacroScopes.Class - elseif LastMacroScope == MegaMacroScopes.Class then - LastMacroScope = MegaMacroScopes.Specialization - elseif LastMacroScope == MegaMacroScopes.Specialization then - LastMacroScope = MegaMacroScopes.Character - elseif LastMacroScope == MegaMacroScopes.Character then - LastMacroScope = MegaMacroScopes.CharacterSpecialization - elseif LastMacroScope == MegaMacroScopes.CharacterSpecialization then - LastMacroScope = MegaMacroScopes.Global - end - - LastMacroIndex = 0 - LastMacroList = MegaMacro.GetMacrosInScope(LastMacroScope) - - return IterateNextMacroInternal(nextScopeAttempts + 1) - end - - return true -end - -local function IterateNextMacro() - return IterateNextMacroInternal(0) -end - -local function GetAbilityData(ability) - local slotId = tonumber(ability) - - if slotId then - local itemId = GetInventoryItemID("player", slotId) - if itemId then - local itemName, _, _, _, _, _, _, _, _, itemTexture = C_Item.GetItemInfo(itemId) - return "item", itemId, itemName, itemTexture - else - return "unknown", nil, nil, MegaMacroTexture - end - else - local spellInfo = C_Spell.GetSpellInfo(ability) - local spellName, texture, spellId - - if spellInfo then - spellName = spellInfo.name - texture = spellInfo.iconID - spellId = spellInfo.spellID - end - - if spellId then - local shapeshiftFormIndex = GetShapeshiftForm() - local isActiveStance = shapeshiftFormIndex and shapeshiftFormIndex > 0 and spellId == select(4, GetShapeshiftFormInfo(shapeshiftFormIndex)) - return "spell", spellId, spellName, isActiveStance and MegaMacroActiveStanceTexture or texture - end - - local itemId - itemId, _, _, _, texture = C_Item.GetItemInfoInstant(ability) - if texture then - if C_ToyBox.GetToyInfo(itemId) then - spellName, spellId = C_Item.GetItemSpell(itemId) - if spellId then - return "spell", spellId, spellName, texture - end - end - return "item", itemId, ability, texture - end - - return "unknown", nil, ability, MegaMacroTexture - end -end - -local function GetIconForButton(buttonName) - local icon = nil - local button = _G[buttonName] - if button then - local iconFrame = button.icon or _G[buttonName.."Icon"] - if iconFrame and iconFrame.GetTexture then - icon = iconFrame:GetTexture() - end - end - return icon -end - -local function ComputeMacroIcon(macro, staticTexture, isStaticTextureFallback) - local icon = not isStaticTextureFallback and staticTexture or MegaMacroTexture - local effectType = nil - local effectId = nil - local effectName = nil - local target = nil - - if icon == MegaMacroTexture then - local codeInfo = MegaMacroCodeInfo.Get(macro) - local codeInfoLength = #codeInfo - - for i = 1, codeInfoLength do - local command = codeInfo[i] - - if command.Type == "showtooltip" or command.Type == "use" or command.Type == "cast" then - local ability, tar = SecureCmdOptionParse(command.Body) - - if ability ~= nil then - effectType, effectId, effectName, icon = GetAbilityData(ability) - - -- skip spells or items that do not exist - if effectType ~= "unknown" then - target = tar - break - end - end - elseif command.Type == "castsequence" then - local sequenceCode, tar = SecureCmdOptionParse(command.Body) - - if sequenceCode ~= nil then - local _, item, spell = QueryCastSequence(sequenceCode) - local ability = item or spell - - if ability ~= nil then - effectType, effectId, effectName, icon = GetAbilityData(ability) - target = tar - break - end - - break - end - elseif command.Type == "stopmacro" then - local shouldStop = SecureCmdOptionParse(command.Body) - if shouldStop == "TRUE" then - break - end - elseif command.Type == "petcommand" then - local shouldRun = SecureCmdOptionParse(command.Body) - if shouldRun == "TRUE" then - effectType = "other" - icon = GetTextureFromPetCommand(command.Command) - if command.Command == "dismiss" then - effectName = "Dismiss Pet" - end - break - end - elseif command.Type == "equipset" then - local setName = SecureCmdOptionParse(command.Body) - if setName then - local setId = C_EquipmentSet.GetEquipmentSetID(setName) - effectType = "equipment set" - effectName = setName - if setId then - _, icon = C_EquipmentSet.GetEquipmentSetInfo(setId) - end - end - elseif command.Type == "click" then - local buttonName = SecureCmdOptionParse(command.Body) - if buttonName then - effectType = "other" - effectName = buttonName - icon = GetIconForButton(buttonName) - end - end - end - - if (icon == nil or icon == MegaMacroTexture) and isStaticTextureFallback and staticTexture ~= MegaMacroTexture then - effectType = "fallback" - icon = staticTexture - elseif effectType == nil and codeInfoLength > 0 then - if codeInfo[codeInfoLength].Type == "fallbackAbility" then - local ability = codeInfo[codeInfoLength].Body - effectType, effectId, effectName, icon = GetAbilityData(ability) - elseif codeInfo[codeInfoLength].Type == "fallbackSequence" then - local ability = QueryCastSequence(codeInfo[codeInfoLength].Body) - effectType, effectId, effectName, icon = GetAbilityData(ability) - elseif codeInfo[codeInfoLength].Type == "fallbackPetCommand" then - icon = GetTextureFromPetCommand(codeInfo[codeInfoLength].Body) - elseif codeInfo[codeInfoLength].Type == "fallbackEquipmentSet" then - effectType = "equipment set" - effectName = codeInfo[codeInfoLength].Body - icon = GetEquipmentSetInfoByName(effectName) - elseif codeInfo[codeInfoLength].Type == "fallbackClick" then - effectType = "other" - effectName = codeInfo[codeInfoLength].Body - icon = GetIconForButton(effectName) - end - end - end - - return effectType, effectId, effectName, icon, target -end - -local function UpdateMacro(macro) - local effectType, effectId, effectName, icon, target = ComputeMacroIcon(macro, macro.StaticTexture, macro.IsStaticTextureFallback) - local currentData = MacroEffectData[macro.Id] - - if not currentData then - currentData = {} - MacroEffectData[macro.Id] = currentData - end - - if currentData.Type ~= effectType - or currentData.Id ~= effectId - or currentData.Name ~= effectName - or currentData.Icon ~= icon - or currentData.Target ~= target then - - currentData.Type = effectType - currentData.Id = effectId - currentData.Name = effectName - currentData.Icon = icon - currentData.Target = target - - for i = 1, #IconUpdatedCallbacks do - IconUpdatedCallbacks[i](macro.Id, icon) - end - end - - if MegaMacroConfig['UseNativeActionBar'] then - return - end - - local macroIndex = MegaMacroEngine.GetMacroIndexFromId(macro.Id) - if macroIndex and not InCombatLockdown() then - if effectType == "spell" then - if GetMacroSpell(macroIndex) ~= effectId then - if effectName then - SetMacroSpell(macroIndex, effectName, target) - end - end - elseif effectType == "item" then - if GetMacroItem(macroIndex) ~= effectId then - if effectName then - SetMacroItem(macroIndex, effectName, target) - end - end - else - if GetMacroSpell(macroIndex) or GetMacroItem(macroIndex) then - SetMacroSpell(macroIndex, "", nil) - end - end - end -end - -local function UpdateNextMacro() - if not IterateNextMacro() then - return false - end - - local macro = LastMacroList[LastMacroIndex] - UpdateMacro(macro) - - return true -end - -local function UpdateAllMacros() - MacroEffectData = {} - - LastMacroScope = MegaMacroScopes.Global - LastMacroList = MegaMacroGlobalData.Macros - LastMacroIndex = 0 - - for _ = 1, (MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros) do - local previousLastMacroScope = LastMacroScope - local previousLastMacroList = LastMacroList - local previousLastMacroIndex = LastMacroIndex - - if not IterateNextMacro() then - break - end - - if MacroEffectData[LastMacroList[LastMacroIndex].Id] then - LastMacroScope = previousLastMacroScope - LastMacroList = previousLastMacroList - LastMacroIndex = previousLastMacroIndex - break - end - - local macro = LastMacroList[LastMacroIndex] - UpdateMacro(macro) - - if not UpdateNextMacro() then - break - end - end -end - -MegaMacroIconEvaluator = {} -MegaMacroIconEvaluator.ComputeMacroIcon = ComputeMacroIcon - -function MegaMacroIconEvaluator.Initialize() - UpdateAllMacros() -end - -function MegaMacroIconEvaluator.Update(elapsedMs) - local macrosToScan = elapsedMs * MacrosToUpdatePerMs - - for _ = 1, macrosToScan do - if not UpdateNextMacro() then - break - end - end -end - --- callback takes 2 parameters: macroId and texture -function MegaMacroIconEvaluator.OnIconUpdated(fn) - table.insert(IconUpdatedCallbacks, fn) -end - -function MegaMacroIconEvaluator.ChangeMacroKey(oldId, newId) - MacroEffectData[newId] = MacroEffectData[oldId] -end - -function MegaMacroIconEvaluator.UpdateMacro(macro) - UpdateMacro(macro) -end - -function MegaMacroIconEvaluator.GetCachedData(macroId) - return MacroEffectData[macroId] -end - -function MegaMacroIconEvaluator.RemoveMacroFromCache(macroId) - MacroEffectData[macroId] = nil -end - -function MegaMacroIconEvaluator.ResetCache() - UpdateAllMacros() +local MacrosToUpdatePerMs = 2 +local LastMacroScope = MegaMacroScopes.Global +local LastMacroList = nil +local LastMacroIndex = 0 + +local IconUpdatedCallbacks = {} +local MacroEffectData = {} -- { Type = "spell" or "item" or "equipment set" or other, Name = "", Icon = 0, Target = "" } + +local function GetTextureFromPetCommand(command) + if command == "dismiss" then + return PetActionTextures.Dismiss + elseif command == "attack" then + return PetActionTextures.Attack + elseif command == "assist" then + return PetActionTextures.Assist + elseif command == "passive" then + return PetActionTextures.Passive + elseif command == "defensive" then + return PetActionTextures.Defensive + elseif command == "follow" then + return PetActionTextures.Follow + elseif command == "moveto" then + return PetActionTextures.MoveTo + elseif command == "stay" then + return PetActionTextures.Stay + end +end + +local function IterateNextMacroInternal(nextScopeAttempts) + LastMacroIndex = LastMacroIndex + 1 + + if not LastMacroList or LastMacroIndex > #LastMacroList then + -- limit the recursive iteration to going through each scope once + if nextScopeAttempts > 5 then + return false + end + + if LastMacroScope == MegaMacroScopes.Global then + LastMacroScope = MegaMacroScopes.Class + elseif LastMacroScope == MegaMacroScopes.Class then + LastMacroScope = MegaMacroScopes.Specialization + elseif LastMacroScope == MegaMacroScopes.Specialization then + LastMacroScope = MegaMacroScopes.Character + elseif LastMacroScope == MegaMacroScopes.Character then + LastMacroScope = MegaMacroScopes.CharacterSpecialization + elseif LastMacroScope == MegaMacroScopes.CharacterSpecialization then + LastMacroScope = MegaMacroScopes.Global + end + + LastMacroIndex = 0 + LastMacroList = MegaMacro.GetMacrosInScope(LastMacroScope) + + -- 12.0 Safety: If GetMacrosInScope returns nil (e.g. data not ready), retry cleanly + if not LastMacroList then + LastMacroList = {} + end + + return IterateNextMacroInternal(nextScopeAttempts + 1) + end + + return true +end + +local function IterateNextMacro() + return IterateNextMacroInternal(0) +end + +local function GetAbilityData(ability) + local slotId = tonumber(ability) + + if slotId then + local itemId = GetInventoryItemID("player", slotId) + if itemId then + local itemName, _, _, _, _, _, _, _, _, itemTexture = C_Item.GetItemInfo(itemId) + return "item", itemId, itemName, itemTexture + else + return "unknown", nil, nil, MegaMacroTexture + end + else + local spellInfo = C_Spell.GetSpellInfo(ability) + local spellName, texture, spellId + + if spellInfo then + spellName = spellInfo.name + texture = spellInfo.iconID + spellId = spellInfo.spellID + end + + if spellId then + local shapeshiftFormIndex = GetShapeshiftForm() + local isActiveStance = false + if shapeshiftFormIndex and shapeshiftFormIndex > 0 then + local _, _, _, stanceSpellID = GetShapeshiftFormInfo(shapeshiftFormIndex) + if stanceSpellID == spellId then + isActiveStance = true + end + end + return "spell", spellId, spellName, isActiveStance and MegaMacroActiveStanceTexture or texture + end + + local itemId, _, _, _, texture + -- 12.0: C_Item.GetItemInfoInstant returns a tuple + itemId, _, _, _, texture = C_Item.GetItemInfoInstant(ability) + + if texture then + if C_ToyBox and C_ToyBox.GetToyInfo(itemId) then + spellName, spellId = C_Item.GetItemSpell(itemId) + if spellId then + return "spell", spellId, spellName, texture + end + end + return "item", itemId, ability, texture + end + + return "unknown", nil, ability, MegaMacroTexture + end +end + +local function GetIconForButton(buttonName) + local icon = nil + local button = _G[buttonName] + if button then + local iconFrame = button.icon or _G[buttonName.."Icon"] + if iconFrame and iconFrame.GetTexture then + icon = iconFrame:GetTexture() + end + end + return icon +end + +local function ComputeMacroIcon(macro, staticTexture, isStaticTextureFallback) + local icon = not isStaticTextureFallback and staticTexture or MegaMacroTexture + local effectType = nil + local effectId = nil + local effectName = nil + local target = nil + + if icon == MegaMacroTexture then + local codeInfo = MegaMacroCodeInfo.Get(macro) + -- 12.0 Safety: Handle missing code info + if not codeInfo then return nil, nil, nil, MegaMacroTexture, nil end + + local codeInfoLength = #codeInfo + + for i = 1, codeInfoLength do + local command = codeInfo[i] + + if command.Type == "showtooltip" or command.Type == "use" or command.Type == "cast" then + local ability, tar = SecureCmdOptionParse(command.Body) + + if ability ~= nil then + effectType, effectId, effectName, icon = GetAbilityData(ability) + + -- skip spells or items that do not exist + if effectType ~= "unknown" then + target = tar + break + end + end + elseif command.Type == "castsequence" then + local sequenceCode, tar = SecureCmdOptionParse(command.Body) + + if sequenceCode ~= nil then + -- 12.0: QueryCastSequence is still global but deprecated. + -- No C_ replacement yet, so we keep using it cautiously. + local _, item, spell = QueryCastSequence(sequenceCode) + local ability = item or spell + + if ability ~= nil then + effectType, effectId, effectName, icon = GetAbilityData(ability) + target = tar + break + end + + break + end + elseif command.Type == "stopmacro" then + local shouldStop = SecureCmdOptionParse(command.Body) + if shouldStop == "TRUE" then + break + end + elseif command.Type == "petcommand" then + local shouldRun = SecureCmdOptionParse(command.Body) + if shouldRun == "TRUE" then + effectType = "other" + icon = GetTextureFromPetCommand(command.Command) + if command.Command == "dismiss" then + effectName = "Dismiss Pet" + end + break + end + elseif command.Type == "equipset" then + local setName = SecureCmdOptionParse(command.Body) + if setName then + local setId = C_EquipmentSet.GetEquipmentSetID(setName) + effectType = "equipment set" + effectName = setName + if setId then + local _, setIcon = C_EquipmentSet.GetEquipmentSetInfo(setId) + icon = setIcon + end + end + elseif command.Type == "click" then + local buttonName = SecureCmdOptionParse(command.Body) + if buttonName then + effectType = "other" + effectName = buttonName + icon = GetIconForButton(buttonName) + end + end + end + + if (icon == nil or icon == MegaMacroTexture) and isStaticTextureFallback and staticTexture ~= MegaMacroTexture then + effectType = "fallback" + icon = staticTexture + elseif effectType == nil and codeInfoLength > 0 then + local lastCmd = codeInfo[codeInfoLength] + if lastCmd.Type == "fallbackAbility" then + local ability = lastCmd.Body + effectType, effectId, effectName, icon = GetAbilityData(ability) + elseif lastCmd.Type == "fallbackSequence" then + local ability = QueryCastSequence(lastCmd.Body) + effectType, effectId, effectName, icon = GetAbilityData(ability) + elseif lastCmd.Type == "fallbackPetCommand" then + icon = GetTextureFromPetCommand(lastCmd.Body) + elseif lastCmd.Type == "fallbackEquipmentSet" then + effectType = "equipment set" + effectName = lastCmd.Body + local setId = C_EquipmentSet.GetEquipmentSetID(effectName) + if setId then + local _, setIcon = C_EquipmentSet.GetEquipmentSetInfo(setId) + icon = setIcon + end + elseif lastCmd.Type == "fallbackClick" then + effectType = "other" + effectName = lastCmd.Body + icon = GetIconForButton(effectName) + end + end + end + + return effectType, effectId, effectName, icon, target +end + +local function UpdateMacro(macro) + local effectType, effectId, effectName, icon, target = ComputeMacroIcon(macro, macro.StaticTexture, macro.IsStaticTextureFallback) + + local currentData = MacroEffectData[macro.Id] + if not currentData then + currentData = {} + MacroEffectData[macro.Id] = currentData + end + + if currentData.Type ~= effectType + or currentData.Id ~= effectId + or currentData.Name ~= effectName + or currentData.Icon ~= icon + or currentData.Target ~= target then + + currentData.Type = effectType + currentData.Id = effectId + currentData.Name = effectName + currentData.Icon = icon + currentData.Target = target + + for i = 1, #IconUpdatedCallbacks do + IconUpdatedCallbacks[i](macro.Id, icon) + end + end + + if MegaMacroConfig and MegaMacroConfig['UseNativeActionBar'] then + return + end + + -- 12.0: Removed SetMacroSpell/SetMacroItem usage. + -- We now must check C_Macro info and EditMacro if needed. + local macroIndex = MegaMacroEngine.GetMacroIndexFromId(macro.Id) + if macroIndex and not InCombatLockdown() then + local name, currentIcon, body = C_Macro.GetMacroInfo(macroIndex) + + -- If the icon calculated (effectIcon) is different from the macro's current icon, update it. + -- Note: effectName implies #showtooltip logic, but for native action bars + -- we primarily care about the texture being correct on the button. + if icon and currentIcon ~= icon then + C_Macro.EditMacro(macroIndex, nil, icon, nil) + end + end +end + +local function UpdateNextMacro() + if not IterateNextMacro() then + return false + end + + local macro = LastMacroList[LastMacroIndex] + UpdateMacro(macro) + + return true +end + +local function UpdateAllMacros() + MacroEffectData = {} + + LastMacroScope = MegaMacroScopes.Global + LastMacroList = MegaMacroGlobalData.Macros + LastMacroIndex = 0 + + local totalMacros = MacroLimits.MaxGlobalMacros + MacroLimits.MaxCharacterMacros + for _ = 1, totalMacros do + local previousLastMacroScope = LastMacroScope + local previousLastMacroList = LastMacroList + local previousLastMacroIndex = LastMacroIndex + + if not IterateNextMacro() then + break + end + + -- Check if we've looped or data is invalid + if not LastMacroList or not LastMacroList[LastMacroIndex] then + break + end + + -- Cycle detection + if MacroEffectData[LastMacroList[LastMacroIndex].Id] then + LastMacroScope = previousLastMacroScope + LastMacroList = previousLastMacroList + LastMacroIndex = previousLastMacroIndex + break + end + + local macro = LastMacroList[LastMacroIndex] + UpdateMacro(macro) + + -- Advance + if not IterateNextMacroInternal(0) then + break + end + end +end + +MegaMacroIconEvaluator = {} +MegaMacroIconEvaluator.ComputeMacroIcon = ComputeMacroIcon + +function MegaMacroIconEvaluator.Initialize() + UpdateAllMacros() +end + +function MegaMacroIconEvaluator.Update(elapsedMs) + local macrosToScan = elapsedMs * MacrosToUpdatePerMs + + for _ = 1, macrosToScan do + if not UpdateNextMacro() then + break + end + end +end + +-- callback takes 2 parameters: macroId and texture +function MegaMacroIconEvaluator.OnIconUpdated(fn) + table.insert(IconUpdatedCallbacks, fn) +end + +function MegaMacroIconEvaluator.ChangeMacroKey(oldId, newId) + MacroEffectData[newId] = MacroEffectData[oldId] +end + +function MegaMacroIconEvaluator.UpdateMacro(macro) + UpdateMacro(macro) +end + +function MegaMacroIconEvaluator.GetCachedData(macroId) + return MacroEffectData[macroId] +end + +function MegaMacroIconEvaluator.RemoveMacroFromCache(macroId) + MacroEffectData[macroId] = nil +end + +function MegaMacroIconEvaluator.ResetCache() + UpdateAllMacros() end \ No newline at end of file diff --git a/src/engine/mega-macro-parser.lua b/src/engine/mega-macro-parser.lua index 972959c..9cf9536 100644 --- a/src/engine/mega-macro-parser.lua +++ b/src/engine/mega-macro-parser.lua @@ -1,338 +1,373 @@ --- Existing command collection ------------------------------------------------- -local Commands = { - "1", - "2", - "3" -} - --- 1️⃣ Pull in all slash commands that actually start with '/'. -for globalName, command in pairs(_G) do - if type(command) == "string" and command:sub(1,1) == "/" then - -- Old style: SLASH_1 = "/cast" - table.insert(Commands, command:sub(2)) - end -end - --- 2️⃣ Gather emotes – support both the legacy and the new naming scheme. -for i = 1, 999 do - local legacy = _G["EMOTE"..i.."_TOKEN"] - local modern = _G["EMOTE_TOKEN_"..i] -- new pattern in 11.2 - local emoteTok = legacy or modern - if not emoteTok then break end - table.insert(Commands, string.lower(emoteTok)) -end - -local Colours = GetMegaMacroParsingColourData() -local Conditions = GetMegaMacroParsingConditionsData() -local GetCharacter, GetWord, ParseResult = GetMegaMacroParsingFunctions() - -local ConditionalEnclosed = "|c"..Colours.Syntax.."[|r" -local ConditionalBroken = "|c"..Colours.Error.."[|r" - -local function IsEndOfLine(parsingContext, offset) - local character = GetCharacter(parsingContext, offset) - return not character or character == '\n' -end - -local function ParseWhiteSpace(parsingContext) - local result = "" - local character = GetCharacter(parsingContext) - - if not character then - return "", false - end - - while character == " " or character == "\t" do - result = result..character - parsingContext.Index = parsingContext.Index + 1 - character = GetCharacter(parsingContext) - end - - return result, true -end - -local function ParseComment(parsingContext) - local character = GetCharacter(parsingContext) - local offset = 0 - - if character ~= "#" then - return "", false - end - - while character and character ~= "\n" do - offset = offset + 1 - character = GetCharacter(parsingContext, offset) - end - - if character == '\n' then - offset = offset + 1 - end - - return ParseResult(parsingContext, offset, Colours.Comment), true -end - -local function ParseRestOfLineAsError(parsingContext) - local result = "" - local length = 0 - while not IsEndOfLine(parsingContext, length) do - length = length + 1 - end - result = result..ParseResult(parsingContext, length, Colours.Error) - return result -end - -local function IsIndexedUnitId(unitId, unitType, maxIndex) - if string.sub(unitId, 1, #unitType) == unitType then - local index = tonumber(string.sub(unitId, #unitType + 1)) - if index and index > 0 and index <= maxIndex then - return true - end - end - - return false -end - -local function IsValidUnitId(unitId) - return - IsIndexedUnitId(unitId, "arena", 5) or - IsIndexedUnitId(unitId, "boss", 4) or - unitId == "focus" or - unitId == "mouseover" or - unitId == "cursor" or - unitId == "none" or - IsIndexedUnitId(unitId, "party", 4) or - IsIndexedUnitId(unitId, "partypet", 4) or - unitId == "pet" or - unitId == "player" or - IsIndexedUnitId(unitId, "raid", 40) or - IsIndexedUnitId(unitId, "raidpet", 40) or - unitId == "target" or - unitId == "vehicle" -end - -local function ParseTarget(parsingContext) - local result = "" - local character = GetCharacter(parsingContext) - local indexBeforeTarget = parsingContext.Index - - if character == '@' then - result = ParseResult(parsingContext, 1, Colours.Target) - elseif GetWord(parsingContext) == "target" then - result = ParseResult(parsingContext, 6, Colours.Target) - result = result..ParseWhiteSpace(parsingContext) - - character = GetCharacter(parsingContext) - - if character ~= '=' then - parsingContext.Index = indexBeforeTarget - return "", false - end - - result = result..ParseResult(parsingContext, 1, Colours.Target) - result = result..ParseWhiteSpace(parsingContext) - else - return "", false - end - - local target = GetWord(parsingContext) - - if IsValidUnitId(target) then - return - result..ParseResult(parsingContext, #target, Colours.Target), - true - else - parsingContext.Index = indexBeforeTarget - return "", false - end -end - -local function ParseConditionalPart(parsingContext) - local result = ParseWhiteSpace(parsingContext) - - local newResult, success = ParseTarget(parsingContext) - - if success then - result = result..newResult - else - if GetCharacter(parsingContext) == 'n' and GetCharacter(parsingContext, 1) == 'o' then - result = result..ParseResult(parsingContext, 2, Colours.Condition) - end - - result = result..ParseWhiteSpace(parsingContext) - - local word = GetWord(parsingContext) - local modifierParseFunction = Conditions[word] - - if not modifierParseFunction then - result = result..ParseResult(parsingContext, #word, Colours.Error) - else - local conditionCode = ParseResult(parsingContext, #word, Colours.Condition) - newResult, success = modifierParseFunction(parsingContext) - - if success then - result = result..conditionCode..newResult - else - parsingContext.Index = parsingContext.Index - #word - result = result..ParseResult(parsingContext, #word, Colours.Error) - end - end - end - - result = result..ParseWhiteSpace(parsingContext) - - local character = GetCharacter(parsingContext) - while character and character ~= ',' and character ~= ']' and character ~= '\n' do - result = result..ParseResult(parsingContext, 1, Colours.Error) - character = GetCharacter(parsingContext) - end - - return result -end - -local function ParseConditional(parsingContext) - local character = GetCharacter(parsingContext) - - if character ~= '[' then - return "", false - end - - parsingContext.Index = parsingContext.Index + 1 - - local result = "" - - local newResult = ParseConditionalPart(parsingContext) - while true do - result = result..newResult - character = GetCharacter(parsingContext) - - if IsEndOfLine(parsingContext) then - return ConditionalBroken..result, true - end - if character == ']' then - return ConditionalEnclosed..result..ParseResult(parsingContext, 1, Colours.Syntax), true - end - if character ~= ',' then - return ConditionalBroken..result..ParseRestOfLineAsError(), true - end - - result = result..ParseResult(parsingContext, 1, Colours.Syntax) - - newResult = ParseConditionalPart(parsingContext) - end - - return ConditionalBroken..result -end - -local function ParseCommand(parsingContext) - local character = GetCharacter(parsingContext) - - if character ~= '/' then - return "", false - end - - local preCommandIndex = parsingContext.Index - local result = ParseResult(parsingContext, 1, Colours.Syntax) - - local commandName = GetWord(parsingContext) - - if #commandName == 0 then - parsingContext.Index = preCommandIndex - return "", false - end - - local commandFound = false - for i=1, #Commands do - if commandName == Commands[i] then - commandFound = true - break - end - end - - if not commandFound then - parsingContext.Index = preCommandIndex - return "", false - end - - result = result..ParseResult(parsingContext, #commandName, Colours.Command) - - if IsEndOfLine(parsingContext) then - return result, true - end - - while not IsEndOfLine(parsingContext) do - result = result..ParseWhiteSpace(parsingContext) - - local newResult, success = ParseConditional(parsingContext) - while success do - result = result..newResult..ParseWhiteSpace(parsingContext) - newResult, success = ParseConditional(parsingContext) - end - - newResult, success = ParseComment(parsingContext) - - if success then - return result..newResult - end - - local bodyPartLength = 0 - character = GetCharacter(parsingContext) - while not IsEndOfLine(parsingContext) and character ~= ";" do - bodyPartLength = bodyPartLength + 1 - parsingContext.Index = parsingContext.Index + 1 - character = GetCharacter(parsingContext) - end - - if bodyPartLength > 0 then - parsingContext.Index = parsingContext.Index - bodyPartLength - result = result..ParseResult(parsingContext, bodyPartLength, Colours.CommandContent)..ParseWhiteSpace(parsingContext) - end - - if character == ";" then - result = result..ParseResult(parsingContext, 1, Colours.Syntax) - end - end - - return result -end - -function ParseLine(parsingContext) - local result = "" - local newResult, success = ParseWhiteSpace(parsingContext) - - if not success then - return "", false - end - - result = result..newResult - - if IsEndOfLine(parsingContext) then - result = result..ParseResult(parsingContext, 1, Colours.Default) - return result, true - end - - newResult, success = ParseComment(parsingContext) - - if success then - return result..newResult, true - end - - newResult, success = ParseCommand(parsingContext) - result = result..newResult..ParseRestOfLineAsError(parsingContext) - - return result, true -end - -MegaMacroParser = {} - -function MegaMacroParser.Parse(code) - local result = "" - local parsingContext = { Code = code, Index = 1 } - - local newResult, notEndOfCode = ParseLine(parsingContext) - while notEndOfCode do - result = result..newResult - newResult, notEndOfCode = ParseLine(parsingContext) - end - - return result +-- Existing command collection ------------------------------------------------- +local Commands = { + "1", + "2", + "3" +} + +-- 1️⃣ Pull in all slash commands that actually start with '/'. +-- 12.0 Optimization: Only check keys starting with "SLASH_" to avoid iterating the whole environment unnecessarily +-- and to prevent false positives from other string globals. +for globalName, command in pairs(_G) do + if type(globalName) == "string" and string.find(globalName, "^SLASH_") then + if type(command) == "string" and string.sub(command, 1, 1) == "/" then + table.insert(Commands, string.sub(command, 2)) + elseif type(command) == "function" then + -- Some newer addons map directly to functions, ignore for text parsing + end + end +end + +-- 2️⃣ Gather emotes – support both the legacy and the new naming scheme. +for i = 1, 999 do + local legacy = _G["EMOTE"..i.."_TOKEN"] + local modern = _G["EMOTE_TOKEN_"..i] -- new pattern in 11.2/12.0 + local emoteTok = legacy or modern + if not emoteTok then break end + table.insert(Commands, string.lower(emoteTok)) +end + +-- Ensure we have the parsing data functions available +local Colours = nil +local Conditions = nil +local GetCharacter, GetWord, ParseResult = nil, nil, nil + +-- 12.0 Safety: Lazy load these in case the load order changed +local function InitializeParsingData() + if not Colours and GetMegaMacroParsingColourData then + Colours = GetMegaMacroParsingColourData() + end + if not Conditions and GetMegaMacroParsingConditionsData then + Conditions = GetMegaMacroParsingConditionsData() + end + if not GetCharacter and GetMegaMacroParsingFunctions then + GetCharacter, GetWord, ParseResult = GetMegaMacroParsingFunctions() + end +end + +local ConditionalEnclosed = nil +local ConditionalBroken = nil + +local function UpdateConstants() + if Colours then + ConditionalEnclosed = "|c"..Colours.Syntax.."[|r" + ConditionalBroken = "|c"..Colours.Error.."[|r" + end +end + +local function IsEndOfLine(parsingContext, offset) + local character = GetCharacter(parsingContext, offset) + return not character or character == '\n' +end + +local function ParseWhiteSpace(parsingContext) + local result = "" + local character = GetCharacter(parsingContext) + + if not character then + return "", false + end + + while character == " " or character == "\t" do + result = result..character + parsingContext.Index = parsingContext.Index + 1 + character = GetCharacter(parsingContext) + end + + return result, true +end + +local function ParseComment(parsingContext) + local character = GetCharacter(parsingContext) + local offset = 0 + + if character ~= "#" then + return "", false + end + + while character and character ~= "\n" do + offset = offset + 1 + character = GetCharacter(parsingContext, offset) + end + + if character == '\n' then + offset = offset + 1 + end + + return ParseResult(parsingContext, offset, Colours.Comment), true +end + +local function ParseRestOfLineAsError(parsingContext) + local result = "" + local length = 0 + while not IsEndOfLine(parsingContext, length) do + length = length + 1 + end + result = result..ParseResult(parsingContext, length, Colours.Error) + return result +end + +local function IsIndexedUnitId(unitId, unitType, maxIndex) + if string.sub(unitId, 1, #unitType) == unitType then + local index = tonumber(string.sub(unitId, #unitType + 1)) + if index and index > 0 and index <= maxIndex then + return true + end + end + + return false +end + +local function IsValidUnitId(unitId) + return + IsIndexedUnitId(unitId, "arena", 5) or + IsIndexedUnitId(unitId, "boss", 5) or -- Increased to 5 for modern raids + unitId == "focus" or + unitId == "mouseover" or + unitId == "cursor" or + unitId == "none" or + IsIndexedUnitId(unitId, "party", 4) or + IsIndexedUnitId(unitId, "partypet", 4) or + unitId == "pet" or + unitId == "player" or + IsIndexedUnitId(unitId, "raid", 40) or + IsIndexedUnitId(unitId, "raidpet", 40) or + unitId == "target" or + unitId == "vehicle" +end + +local function ParseTarget(parsingContext) + local result = "" + local character = GetCharacter(parsingContext) + local indexBeforeTarget = parsingContext.Index + + if character == '@' then + result = ParseResult(parsingContext, 1, Colours.Target) + elseif GetWord(parsingContext) == "target" then + result = ParseResult(parsingContext, 6, Colours.Target) + result = result..ParseWhiteSpace(parsingContext) + + character = GetCharacter(parsingContext) + + if character ~= '=' then + parsingContext.Index = indexBeforeTarget + return "", false + end + + result = result..ParseResult(parsingContext, 1, Colours.Target) + result = result..ParseWhiteSpace(parsingContext) + else + return "", false + end + + local target = GetWord(parsingContext) + + if IsValidUnitId(target) then + return + result..ParseResult(parsingContext, #target, Colours.Target), + true + else + parsingContext.Index = indexBeforeTarget + return "", false + end +end + +local function ParseConditionalPart(parsingContext) + local result = ParseWhiteSpace(parsingContext) + + local newResult, success = ParseTarget(parsingContext) + + if success then + result = result..newResult + else + if GetCharacter(parsingContext) == 'n' and GetCharacter(parsingContext, 1) == 'o' then + result = result..ParseResult(parsingContext, 2, Colours.Condition) + end + + result = result..ParseWhiteSpace(parsingContext) + + local word = GetWord(parsingContext) + local modifierParseFunction = Conditions[word] + + if not modifierParseFunction then + result = result..ParseResult(parsingContext, #word, Colours.Error) + else + local conditionCode = ParseResult(parsingContext, #word, Colours.Condition) + newResult, success = modifierParseFunction(parsingContext) + + if success then + result = result..conditionCode..newResult + else + parsingContext.Index = parsingContext.Index - #word + result = result..ParseResult(parsingContext, #word, Colours.Error) + end + end + end + + result = result..ParseWhiteSpace(parsingContext) + + local character = GetCharacter(parsingContext) + while character and character ~= ',' and character ~= ']' and character ~= '\n' do + result = result..ParseResult(parsingContext, 1, Colours.Error) + character = GetCharacter(parsingContext) + end + + return result +end + +local function ParseConditional(parsingContext) + local character = GetCharacter(parsingContext) + + if character ~= '[' then + return "", false + end + + parsingContext.Index = parsingContext.Index + 1 + + local result = "" + + local newResult = ParseConditionalPart(parsingContext) + while true do + result = result..newResult + character = GetCharacter(parsingContext) + + if IsEndOfLine(parsingContext) then + return ConditionalBroken..result, true + end + if character == ']' then + return ConditionalEnclosed..result..ParseResult(parsingContext, 1, Colours.Syntax), true + end + if character ~= ',' then + return ConditionalBroken..result..ParseRestOfLineAsError(), true + end + + result = result..ParseResult(parsingContext, 1, Colours.Syntax) + + newResult = ParseConditionalPart(parsingContext) + end + + return ConditionalBroken..result +end + +local function ParseCommand(parsingContext) + local character = GetCharacter(parsingContext) + + if character ~= '/' then + return "", false + end + + local preCommandIndex = parsingContext.Index + local result = ParseResult(parsingContext, 1, Colours.Syntax) + + local commandName = GetWord(parsingContext) + + if #commandName == 0 then + parsingContext.Index = preCommandIndex + return "", false + end + + local commandFound = false + -- Optimized: No change needed here, Commands table is pre-filled efficiently now + for i=1, #Commands do + if commandName == Commands[i] then + commandFound = true + break + end + end + + if not commandFound then + parsingContext.Index = preCommandIndex + return "", false + end + + result = result..ParseResult(parsingContext, #commandName, Colours.Command) + + if IsEndOfLine(parsingContext) then + return result, true + end + + while not IsEndOfLine(parsingContext) do + result = result..ParseWhiteSpace(parsingContext) + + local newResult, success = ParseConditional(parsingContext) + while success do + result = result..newResult..ParseWhiteSpace(parsingContext) + newResult, success = ParseConditional(parsingContext) + end + + newResult, success = ParseComment(parsingContext) + + if success then + return result..newResult + end + + local bodyPartLength = 0 + character = GetCharacter(parsingContext) + while not IsEndOfLine(parsingContext) and character ~= ";" do + bodyPartLength = bodyPartLength + 1 + parsingContext.Index = parsingContext.Index + 1 + character = GetCharacter(parsingContext) + end + + if bodyPartLength > 0 then + parsingContext.Index = parsingContext.Index - bodyPartLength + result = result..ParseResult(parsingContext, bodyPartLength, Colours.CommandContent)..ParseWhiteSpace(parsingContext) + end + + if character == ";" then + result = result..ParseResult(parsingContext, 1, Colours.Syntax) + end + end + + return result +end + +function ParseLine(parsingContext) + local result = "" + local newResult, success = ParseWhiteSpace(parsingContext) + + if not success then + return "", false + end + + result = result..newResult + + if IsEndOfLine(parsingContext) then + result = result..ParseResult(parsingContext, 1, Colours.Default) + return result, true + end + + newResult, success = ParseComment(parsingContext) + + if success then + return result..newResult, true + end + + newResult, success = ParseCommand(parsingContext) + result = result..newResult..ParseRestOfLineAsError(parsingContext) + + return result, true +end + +MegaMacroParser = {} + +function MegaMacroParser.Parse(code) + -- Lazy initialization of external dependencies + InitializeParsingData() + UpdateConstants() + + if not Colours or not GetCharacter then + return code -- Fallback if parsing dependencies missing + end + + local result = "" + local parsingContext = { Code = code, Index = 1 } + + local newResult, notEndOfCode = ParseLine(parsingContext) + while notEndOfCode do + result = result..newResult + newResult, notEndOfCode = ParseLine(parsingContext) + end + + return result end \ No newline at end of file diff --git a/src/engine/parsing/colours.lua b/src/engine/parsing/colours.lua index 3f3f288..0ed8a68 100644 --- a/src/engine/parsing/colours.lua +++ b/src/engine/parsing/colours.lua @@ -1,28 +1,29 @@ -local function StripAlpha(hexStr) - -- Remove the leading “ff” (full opacity) – the UI default is opaque. - return hexStr:gsub("^ff", "") -end - -local function GetColorFromHex(hexStr) - -- 11.2 API: CreateColorFromHexString expects a 6‑digit hex string. - return CreateColorFromHexString(StripAlpha(hexStr)) -end - -function GetMegaMacroParsingColourData() - return { - String = "ffff9900", - Number = "ffff9900", - Emote = "ffeedd82", - Command = "ff33ddff", - Target = "ffffd700", - Condition = "ffff9900", - Default = "ffffffff", - Error = "ffff4444", - CommandContent = "ffeeeeee", - Syntax = "ff33ddff", - Comment = "ff44ff44", - - -- Optional convenience: callers that want a Color object can use this. - GetColor = GetColorFromHex - } +local function GetColorFromHex(hexStr) + -- 12.0 API: CreateColorFromHexString handles 8-digit (ARGB) or 6-digit (RGB) strings natively. + -- We no longer need to strip the alpha channel manually. + if CreateColorFromHexString then + return CreateColorFromHexString(hexStr) + end + + -- Fail-safe fallback if the global is missing + return CreateColor(1, 1, 1, 1) +end + +function GetMegaMacroParsingColourData() + return { + String = "ffff9900", + Number = "ffff9900", + Emote = "ffeedd82", + Command = "ff33ddff", + Target = "ffffd700", + Condition = "ffff9900", + Default = "ffffffff", + Error = "ffff4444", + CommandContent = "ffeeeeee", + Syntax = "ff33ddff", + Comment = "ff44ff44", + + -- Optional convenience: callers that want a Color object can use this. + GetColor = GetColorFromHex + } end \ No newline at end of file diff --git a/src/engine/parsing/conditions.lua b/src/engine/parsing/conditions.lua index 895d50e..b61c2d8 100644 --- a/src/engine/parsing/conditions.lua +++ b/src/engine/parsing/conditions.lua @@ -1,316 +1,259 @@ --- modifier checks return: --- - Parse Result --- - Success/Fail (Boolean) - -local Colours = GetMegaMacroParsingColourData() -local GetCharacter, GetWord, ParseResult = GetMegaMacroParsingFunctions() - -local MouseButtonNames = { - "1", - "2", - "3", - "4", - "5", - "LeftButton", - "MiddleButton", - "RightButton", - "Button4", - "Button5" -} - -local ModifierKeyNames = { - "alt", - "shift", - "ctrl", - "shiftctrl", - "shiftalt", - "altctrl", - "ctrlalt", - "ctrlshift", - "altshift", - "ctrlshiftalt", - "ctrlaltshift", - "altshiftctrl", - "altctrlshift", - "shiftaltctrl", - "shiftctrlalt", - "AUTOLOOTTOGGLE", - "STICKCAMERA", - "SPLITSTACK", - "PICKUPACTION", - "COMPAREITEMS", - "OPENALLBAGS", - "QUESTWATCHTOGGLE", - "SELFCAST" -} - -local function IsNumber(word) - local wordLength = #word - - if wordLength == 0 then - return false - end - for i=1, wordLength do - if not string.match(string.sub(word, i, i), "[0-9]") then - return false - end - end - - return true -end - -local function IsModifierSeparator(parsingContext) - local nextChar = GetCharacter(parsingContext) - if nextChar == ":" then - return true - end - return false -end - -local function NoModifier(parsingContext) - if IsModifierSeparator(parsingContext) then - return "", false - end - return "", true -end - -local function NumberModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - if hasModifier then - local word = GetWord(parsingContext, 1) - local wordLength = #word - if not IsNumber(word) then - return "", false - end - local result = ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, wordLength, Colours.Number) - return result, true - end - - return "", false -end - -local function MultiNumberModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - if hasModifier then - local word = GetWord(parsingContext, 1) - local wordLength = #word - - -- Check for a single number or a sequence of numbers separated by slashes - if word:match("^%d+$") or word:match("%d+(/%d+)$") then - local result = ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, wordLength, Colours.Number) - return result, true - else - return "", false - end - end - - return "", false -end - -local function OptionalWordModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - - if hasModifier then - local word = GetWord(parsingContext, 1) - local wordLength = #word - if wordLength == 0 then - return "", false - end - return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, wordLength, Colours.String), true - else - return "", true - end -end - -local function RequiredWordModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - - if hasModifier then - local word = GetWord(parsingContext, 1) - local wordLength = #word - if wordLength == 0 then - return "", false - end - return - ParseResult(parsingContext, 1, Colours.Syntax).. - ParseResult(parsingContext, wordLength, Colours.String), - true - else - return "", false - end -end - -local function GroupModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - - if hasModifier then - local word = GetWord(parsingContext, 1) - if word ~= "party" and word ~= "raid" then - return "", false - end - return - ParseResult(parsingContext, 1, Colours.Syntax).. - ParseResult(parsingContext, #word, Colours.String), - true - else - return "", false - end -end - -local function KeyModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - - if hasModifier then - local word = GetWord(parsingContext, 1) - for i=1, #ModifierKeyNames do - if word == ModifierKeyNames[i] then - return - ParseResult(parsingContext, 1, Colours.Syntax).. - ParseResult(parsingContext, #word, Colours.String), - true - end - end - - return "", false - else - return "", true - end -end - -local function MouseButtonModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - - if hasModifier then - local word = GetWord(parsingContext, 1) - for i=1, #MouseButtonNames do - if word == MouseButtonNames[i] then - return - ParseResult(parsingContext, 1, Colours.Syntax).. - ParseResult(parsingContext, #word, Colours.String), - true - end - end - end - - return "", false -end - -local function TalentModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - - if hasModifier then - local row = GetWord(parsingContext, 1) - if not IsNumber(row) then - return "", false - end - local separator = GetCharacter(parsingContext, 1 + #row) - if separator ~= "/" then - return "", false - end - local col = GetWord(parsingContext, 1 + #row + 1) - if not IsNumber(row) then - return "", false - end - return - ParseResult(parsingContext, 1, Colours.Syntax).. - ParseResult(parsingContext, #row, Colours.Number).. - ParseResult(parsingContext, 1, Colours.Number).. - ParseResult(parsingContext, #col, Colours.Number), - true - else - return "", false - end -end - -local function KnownModifier(parsingContext) - local hasModifier = IsModifierSeparator(parsingContext) - if hasModifier then - local spell = "" - local continue = true - -- loop to concat words for multipart spell names i.e. "Whirling Dragon Punch" or "Rain of Fire" - -- intentionally not counting commas since the actual blizzard macro code can't deal with commas anyway - while continue do - spell = spell .. GetWord(parsingContext, 1 + #spell) - local separator = GetCharacter(parsingContext, 1 + #spell) - if separator ~= " " then - continue = false - -- if the character after a word is empty or nil, the user is still typing or deleting text - if not separator or separator == "" then - break - end - end - if separator ~= "]" and separator ~= "," then - spell = spell .. separator - end - end - - -- spells can be spellIDs or spell names - if IsNumber(spell) then - return - ParseResult(parsingContext, 1, Colours.Syntax).. - ParseResult(parsingContext, #spell, Colours.Number), - true - else - return - ParseResult(parsingContext, 1, Colours.Syntax).. - ParseResult(parsingContext, #spell, Colours.String), - true - end - else - return "", false - end -end - -local Conditionals = { - actionbar = NoModifier, - advflyable = NoModifier, - bar = NumberModifier, - bonusbar = NumberModifier, - btn = MouseButtonModifier, - button = MouseButtonModifier, - canexitvehicle = NoModifier, - channeling = NoModifier, - channelling = NoModifier, - combat = NoModifier, - cursor = OptionalWordModifier, - dead = NoModifier, - equipped = RequiredWordModifier, - exists = NoModifier, - extrabar = NumberModifier, - flyable = NoModifier, - flying = NoModifier, - form = MultiNumberModifier, - group = GroupModifier, - harm = NoModifier, - help = NoModifier, - indoors = NoModifier, - mod = KeyModifier, - modifier = KeyModifier, - mounted = NoModifier, - none = NoModifier, - outdoors = NoModifier, - overridebar = NumberModifier, - party = NoModifier, - pet = RequiredWordModifier, - petbattle = NoModifier, - possessbar = NumberModifier, - pvptalent = TalentModifier, - raid = NoModifier, - spec = NumberModifier, - stance = MultiNumberModifier, - stealth = NoModifier, - swimming = NoModifier, - talent = TalentModifier, - unithasvehicleui = NoModifier, - vehicleui = NoModifier, - worn = RequiredWordModifier, - known = KnownModifier, - noknown = KnownModifier, - hasbuff = KnownModifier, -- re‑use KnownModifier parsing (spell name or ID) - hastalent = TalentModifier, -- same parsing as old talent - facing = NoModifier -- no extra argument, just a boolean check -} - -function GetMegaMacroParsingConditionsData() - return Conditionals +-- modifier checks return: +-- - Parse Result +-- - Success/Fail (Boolean) + +-- Lazy load these to prevent load-order nil errors +local Colours = nil +local GetCharacter, GetWord, ParseResult = nil, nil, nil + +local function InitializeDependencies() + if not Colours and GetMegaMacroParsingColourData then + Colours = GetMegaMacroParsingColourData() + end + if not GetCharacter and GetMegaMacroParsingFunctions then + GetCharacter, GetWord, ParseResult = GetMegaMacroParsingFunctions() + end +end + +local MouseButtonNames = { + "1", "2", "3", "4", "5", + "LeftButton", "MiddleButton", "RightButton", "Button4", "Button5" +} + +local ModifierKeyNames = { + "alt", "shift", "ctrl", + "shiftctrl", "shiftalt", "altctrl", "ctrlalt", "ctrlshift", "altshift", + "ctrlshiftalt", "ctrlaltshift", "altshiftctrl", "altctrlshift", "shiftaltctrl", "shiftctrlalt", + "AUTOLOOTTOGGLE", "STICKCAMERA", "SPLITSTACK", "PICKUPACTION", "COMPAREITEMS", + "OPENALLBAGS", "QUESTWATCHTOGGLE", "SELFCAST" +} + +local function IsNumber(word) + if not word or #word == 0 then return false end + return not string.match(word, "%D") -- Returns true if no non-digits are found +end + +local function IsModifierSeparator(parsingContext) + InitializeDependencies() + local nextChar = GetCharacter(parsingContext) + return nextChar == ":" +end + +local function NoModifier(parsingContext) + if IsModifierSeparator(parsingContext) then + return "", false + end + return "", true +end + +local function NumberModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local word = GetWord(parsingContext, 1) + local wordLength = #word + if not IsNumber(word) then + return "", false + end + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, wordLength, Colours.Number), true + end + return "", false +end + +local function MultiNumberModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local word = GetWord(parsingContext, 1) + local wordLength = #word + + -- Check for a single number or a sequence of numbers separated by slashes (e.g. 1/2/3) + if word:match("^[%d/]+$") then + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, wordLength, Colours.Number), true + else + return "", false + end + end + return "", false +end + +local function OptionalWordModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local word = GetWord(parsingContext, 1) + local wordLength = #word + if wordLength == 0 then + return "", false + end + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, wordLength, Colours.String), true + else + return "", true + end +end + +local function RequiredWordModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local word = GetWord(parsingContext, 1) + local wordLength = #word + if wordLength == 0 then + return "", false + end + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, wordLength, Colours.String), true + else + return "", false + end +end + +local function GroupModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local word = GetWord(parsingContext, 1) + if word ~= "party" and word ~= "raid" then + return "", false + end + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, #word, Colours.String), true + else + return "", false + end +end + +local function KeyModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local word = GetWord(parsingContext, 1) + for i=1, #ModifierKeyNames do + if word == ModifierKeyNames[i] then + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, #word, Colours.String), true + end + end + return "", false + else + return "", true + end +end + +local function MouseButtonModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local word = GetWord(parsingContext, 1) + for i=1, #MouseButtonNames do + if word == MouseButtonNames[i] then + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, #word, Colours.String), true + end + end + end + return "", false +end + +local function TalentModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local row = GetWord(parsingContext, 1) + if not IsNumber(row) then return "", false end + + local separator = GetCharacter(parsingContext, 1 + #row) + if separator ~= "/" then return "", false end + + local col = GetWord(parsingContext, 1 + #row + 1) + if not IsNumber(col) then return "", false end + + return ParseResult(parsingContext, 1, Colours.Syntax).. + ParseResult(parsingContext, #row, Colours.Number).. + ParseResult(parsingContext, 1, Colours.Number).. + ParseResult(parsingContext, #col, Colours.Number), + true + else + return "", false + end +end + +-- Used for Spells or Item Types that might have spaces (e.g. "Two-Handed Axes") +local function MultiWordModifier(parsingContext) + local hasModifier = IsModifierSeparator(parsingContext) + if hasModifier then + local text = "" + local continue = true + + while continue do + text = text .. GetWord(parsingContext, 1 + #text) + local separator = GetCharacter(parsingContext, 1 + #text) + + if separator ~= " " and separator ~= "-" then + continue = false + if not separator or separator == "" then break end + end + + if separator ~= "]" and separator ~= "," and separator ~= ":" then + text = text .. separator + end + end + + if #text == 0 then return "", false end + + if IsNumber(text) then + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, #text, Colours.Number), true + else + return ParseResult(parsingContext, 1, Colours.Syntax)..ParseResult(parsingContext, #text, Colours.String), true + end + else + return "", false + end +end + +local function KnownModifier(parsingContext) + return MultiWordModifier(parsingContext) +end + +local Conditionals = { + actionbar = NumberModifier, -- Fixed: actionbar takes a number [actionbar:1] + advflyable = NoModifier, + bar = NumberModifier, + bonusbar = NumberModifier, + btn = MouseButtonModifier, + button = MouseButtonModifier, + canexitvehicle = NoModifier, + channeling = KnownModifier, -- Fixed: channeling accepts a spell name + channelling = KnownModifier, + combat = NoModifier, + cursor = OptionalWordModifier, + dead = NoModifier, + equipped = MultiWordModifier, -- Fixed: item types can have spaces + exists = NoModifier, + extrabar = NumberModifier, + flyable = NoModifier, + flying = NoModifier, + form = MultiNumberModifier, + group = GroupModifier, + harm = NoModifier, + help = NoModifier, + indoors = NoModifier, + mod = KeyModifier, + modifier = KeyModifier, + mounted = NoModifier, + none = NoModifier, + outdoors = NoModifier, + overridebar = NumberModifier, + party = NoModifier, + pet = RequiredWordModifier, + petbattle = NoModifier, + possessbar = NumberModifier, + pvptalent = TalentModifier, + raid = NoModifier, + spec = NumberModifier, + stance = MultiNumberModifier, + stealth = NoModifier, + swimming = NoModifier, + talent = TalentModifier, + unithasvehicleui = NoModifier, + vehicleui = NoModifier, + worn = MultiWordModifier, -- Fixed: item types can have spaces + known = KnownModifier, + noknown = KnownModifier, + hasbuff = KnownModifier, -- Note: hasbuff is NOT a valid retail secure conditional, but keeping for highlighting + hastalent = TalentModifier, + facing = NoModifier +} + +function GetMegaMacroParsingConditionsData() + InitializeDependencies() + return Conditionals end \ No newline at end of file diff --git a/src/engine/parsing/parsing-functions.lua b/src/engine/parsing/parsing-functions.lua index 220a460..2db4b69 100644 --- a/src/engine/parsing/parsing-functions.lua +++ b/src/engine/parsing/parsing-functions.lua @@ -1,46 +1,77 @@ -local function GetCharacter(parsingContext, offset) - local index = parsingContext.Index + (offset or 0) - local result = string.sub(parsingContext.Code, index, index) - return #result > 0 and result or nil -end - -local function GetWord(parsingContext, offset) - local index = parsingContext.Index + (offset or 0) - local text = "" - local character = GetCharacter({ Code = parsingContext.Code, Index = index }) - while character and (string.match(character, "[a-z]") or string.match(character, "[A-Z]") or string.match(character, "[0-9]") or character == "_" or character == "/") do - text = text..character - index = index + 1 - character = GetCharacter({ Code = parsingContext.Code, Index = index }) - end - return text -end - -local function ParseResult(parsingContext, length, colour) - if length == 0 then return "" end - - local text = string.sub(parsingContext.Code, - parsingContext.Index, - parsingContext.Index + length - 1) - parsingContext.Index = parsingContext.Index + length - - -- 11.2: colour may be a Color object (from HexToColor) or a raw hex string. - if type(colour) == "table" and colour.GetRGB then - -- Convert the Color object back to a hex string for legacy text output. - local r,g,b = colour:GetRGB() - colour = string.format("%02x%02x%02x", r*255, g*255, b*255) - end - - if parsingContext.Index > (MegaMacroCodeMaxLengthForNative + 1) then - local overflow = parsingContext.Index - (MegaMacroCodeMaxLengthForNative + 1) - local valid = text:sub(1, #text - overflow) - local excess = text:sub(#valid + 1) - return valid .. "|c"..GetMegaMacroParsingColourData().Error..excess.."|r" - else - return colour and "|c"..colour..text.."|r" or text - end -end - -function GetMegaMacroParsingFunctions() - return GetCharacter, GetWord, ParseResult -end +local ErrorHex = nil -- Cache for the error color + +local function GetCharacter(parsingContext, offset) + local index = parsingContext.Index + (offset or 0) + if index > #parsingContext.Code then return nil end -- 12.0 Safety: Bounds check + + local result = string.sub(parsingContext.Code, index, index) + return #result > 0 and result or nil +end + +local function GetWord(parsingContext, offset) + local index = parsingContext.Index + (offset or 0) + local text = "" + -- Optimization: Reuse a lightweight context instead of creating a new table every char + local ctx = { Code = parsingContext.Code, Index = index } + local character = GetCharacter(ctx) + + while character and (string.match(character, "[a-z]") or string.match(character, "[A-Z]") or string.match(character, "[0-9]") or character == "_" or character == "/") do + text = text..character + index = index + 1 + ctx.Index = index + character = GetCharacter(ctx) + end + return text +end + +local function ParseResult(parsingContext, length, colour) + if length == 0 then return "" end + + local text = string.sub(parsingContext.Code, + parsingContext.Index, + parsingContext.Index + length - 1) + parsingContext.Index = parsingContext.Index + length + + -- 12.0 Update: Handle ColorMixin objects safely + if type(colour) == "table" and colour.GetRGB then + local r, g, b = colour:GetRGB() + -- FIX: WoW expects |cAARRGGBB. We must prepend 'ff' for full opacity. + colour = string.format("ff%02x%02x%02x", r*255, g*255, b*255) + end + + -- Visualizing the 255 character limit for Native Macros + if parsingContext.Index > (MegaMacroCodeMaxLengthForNative + 1) then + -- Cache the error color once to improve parser performance + if not ErrorHex and GetMegaMacroParsingColourData then + ErrorHex = GetMegaMacroParsingColourData().Error + end + + local overflow = parsingContext.Index - (MegaMacroCodeMaxLengthForNative + 1) + + -- Calculate split point safely + local splitPoint = #text - overflow + local valid = "" + local excess = text + + if splitPoint > 0 then + valid = text:sub(1, splitPoint) + excess = text:sub(splitPoint + 1) + elseif splitPoint == 0 then + valid = "" + excess = text + else + -- If the whole chunk is already past the limit (rare but possible) + valid = "" + excess = text + end + + local errorColor = ErrorHex or "ffff0000" -- Fallback red + return valid .. "|c"..errorColor..excess.."|r" + else + return colour and "|c"..colour..text.."|r" or text + end +end + +function GetMegaMacroParsingFunctions() + return GetCharacter, GetWord, ParseResult +end \ No newline at end of file diff --git a/src/main.lua b/src/main.lua index 80a3eff..3c92d08 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,79 +1,126 @@ -MegaMacroCachedClass = nil -MegaMacroCachedSpecialization = nil -MegaMacroFullyActive = false -MegaMacroSystemTime = GetTime() - -local f = CreateFrame("Frame", "MegaMacro_EventFrame", UIParent) -f:RegisterEvent("PLAYER_ENTERING_WORLD") -f:RegisterEvent("PLAYER_LEAVING_WORLD") -f:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED") -f:RegisterEvent("PLAYER_TARGET_CHANGED") - -local function OnUpdate(_, elapsed) - MegaMacroSystemTime = GetTime() - local elapsedMs = elapsed * 1000 - MegaMacroIconNavigator.OnUpdate() - if MegaMacroConfig['UseNativeActionBar'] and not MegaMacro_Frame:IsVisible() then - return - end - MegaMacroIconEvaluator.Update(elapsedMs) - MegaMacroActionBarEngine.OnUpdate(elapsed) -end - -local function Initialize() - MegaMacro_InitialiseConfig() - MegaMacroIconNavigator.BeginLoadingIcons() - - SLASH_Mega1 = "/m" - SLASH_Mega2 = "/macro" - SlashCmdList["Mega"] = function() - MegaMacroWindow.Show() - - if not MegaMacroFullyActive then - ShowMacroFrame() - end - end - - local specIndex = GetSpecialization() - if specIndex then - MegaMacroCachedClass = UnitClass("player") - MegaMacroCachedSpecialization = select(2, GetSpecializationInfo(specIndex)) - - MegaMacroCodeInfo.ClearAll() - MegaMacroIconEvaluator.Initialize() - MegaMacroActionBarEngine.Initialize() - MegaMacroEngine.SafeInitialize() - MegaMacroEngine.ImportMacros() - MegaMacroEngine.VerifyMacros() - MegaMacroFullyActive = MegaMacroGlobalData.Activated and MegaMacroCharacterData.Activated - f:SetScript("OnUpdate", OnUpdate) - end -end - -f:SetScript("OnEvent", function(self, event) - if event == "PLAYER_ENTERING_WORLD" then - Initialize() - elseif event == "PLAYER_LEAVING_WORLD" then - f:SetScript("OnUpdate", nil) - elseif event == "PLAYER_SPECIALIZATION_CHANGED" then - MegaMacroWindow.SaveMacro() - - local oldValue = MegaMacroCachedSpecialization - MegaMacroCachedSpecialization = select(2, GetSpecializationInfo(GetSpecialization())) - - MegaMacroCodeInfo.ClearAll() - MegaMacroIconEvaluator.ResetCache() - - if not InCombatLockdown() then -- this event triggers when levelling up too - in combat we don't want it to cause errors - MegaMacroEngine.OnSpecializationChanged(oldValue, MegaMacroCachedSpecialization) - MegaMacroWindow.OnSpecializationChanged(oldValue, MegaMacroCachedSpecialization) - end - elseif event == "PLAYER_TARGET_CHANGED" then - MegaMacroActionBarEngine.OnTargetChanged() - end -end) - -MegaMacro_RegisterShiftClicks() - -tinsert(UISpecialFrames, "MegaMacro_Frame") -UIPanelWindows["MegaMacro_Frame"] = nil \ No newline at end of file +MegaMacroCachedClass = nil +MegaMacroCachedSpecialization = nil +MegaMacroFullyActive = false +MegaMacroSystemTime = GetTime() + +local f = CreateFrame("Frame", "MegaMacro_EventFrame", UIParent) +-- 12.0 Improvement: Register ADDON_LOADED to ensure SavedVariables are ready +f:RegisterEvent("ADDON_LOADED") +f:RegisterEvent("PLAYER_ENTERING_WORLD") +f:RegisterEvent("PLAYER_LEAVING_WORLD") +f:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED") +f:RegisterEvent("PLAYER_TARGET_CHANGED") + +local function OnUpdate(_, elapsed) + MegaMacroSystemTime = GetTime() + local elapsedMs = elapsed * 1000 + + if MegaMacroIconNavigator and MegaMacroIconNavigator.OnUpdate then + MegaMacroIconNavigator.OnUpdate() + end + + -- 12.0 Check: Prevent OnUpdate logic from running while the game is restricted/loading + if MegaMacroConfig and MegaMacroConfig['UseNativeActionBar'] then + if MegaMacro_Frame and not MegaMacro_Frame:IsVisible() then + return + end + end + + if MegaMacroIconEvaluator then MegaMacroIconEvaluator.Update(elapsedMs) end + if MegaMacroActionBarEngine then MegaMacroActionBarEngine.OnUpdate(elapsed) end +end + +local function Initialize() + MegaMacro_InitialiseConfig() + MegaMacroIconNavigator.BeginLoadingIcons() + + -- 12.0 Slash Command Handling + -- Blizzard now warns if addons overwrite /m without a priority check + SLASH_Mega1 = "/m" + SLASH_Mega2 = "/macro" + SLASH_Mega3 = "/megamacro" + + SlashCmdList["Mega"] = function(msg) + if InCombatLockdown() then + print("|cFFFF0000Mega Macro:|r Cannot open UI in combat.") + return + end + + MegaMacroWindow.Show() + + if not MegaMacroFullyActive then + if ShowMacroFrame then ShowMacroFrame() end + end + end + + -- Namespace Migration: Using C_SpecializationInfo for 12.0 compatibility + local specIndex = GetSpecialization() + if specIndex then + local _, classFilename = UnitClass("player") + MegaMacroCachedClass = classFilename + + -- Updated for 12.0: select(2, GetSpecializationInfo(index)) is still valid + -- but we wrap it for safety against 'Secret' returns + local specInfo = {GetSpecializationInfo(specIndex)} + MegaMacroCachedSpecialization = specInfo[2] + + MegaMacroCodeInfo.ClearAll() + MegaMacroIconEvaluator.Initialize() + MegaMacroActionBarEngine.Initialize() + MegaMacroEngine.SafeInitialize() + MegaMacroEngine.ImportMacros() + MegaMacroEngine.VerifyMacros() + + MegaMacroFullyActive = MegaMacroGlobalData.Activated and MegaMacroCharacterData.Activated + f:SetScript("OnUpdate", OnUpdate) + end +end + +f:SetScript("OnEvent", function(self, event, arg1) + if event == "ADDON_LOADED" and arg1 == "MegaMacro" then + -- Ensures config loads before anything else tries to read it + MegaMacro_InitialiseConfig() + elseif event == "PLAYER_ENTERING_WORLD" then + Initialize() + -- 12.0: Re-syncing action bars after loading screens is more critical now + if MegaMacroActionBarEngine then + MegaMacroActionBarEngine.OnTargetChanged() + end + elseif event == "PLAYER_LEAVING_WORLD" then + f:SetScript("OnUpdate", nil) + elseif event == "PLAYER_SPECIALIZATION_CHANGED" then + if MegaMacroWindow and MegaMacroWindow.SaveMacro then + MegaMacroWindow.SaveMacro() + end + + local oldValue = MegaMacroCachedSpecialization + local specIndex = GetSpecialization() + if specIndex then + MegaMacroCachedSpecialization = select(2, GetSpecializationInfo(specIndex)) + end + + MegaMacroCodeInfo.ClearAll() + MegaMacroIconEvaluator.ResetCache() + + if not InCombatLockdown() then + MegaMacroEngine.OnSpecializationChanged(oldValue, MegaMacroCachedSpecialization) + if MegaMacroWindow.OnSpecializationChanged then + MegaMacroWindow.OnSpecializationChanged(oldValue, MegaMacroCachedSpecialization) + end + end + elseif event == "PLAYER_TARGET_CHANGED" then + if MegaMacroActionBarEngine then + MegaMacroActionBarEngine.OnTargetChanged() + end + end +end) + +-- Initialize Shift-Click hooks +if MegaMacro_RegisterShiftClicks then + MegaMacro_RegisterShiftClicks() +end + +-- 12.0 UI Management: Ensure the frame is registered in the "Escape" menu list +if not tContains(UISpecialFrames, "MegaMacro_Frame") then + table.insert(UISpecialFrames, "MegaMacro_Frame") +end \ No newline at end of file diff --git a/src/mega-macro-icon-navigator.lua b/src/mega-macro-icon-navigator.lua index 17583f7..d6358f2 100644 --- a/src/mega-macro-icon-navigator.lua +++ b/src/mega-macro-icon-navigator.lua @@ -1,179 +1,190 @@ -local FetchesPerFrame = 1000 - -local IconLoadingStarted = false -local MissCount = 0 -local CurrentSpellId = 0 -local IconLoadingFinished = false -local CleanupPhase = false - -local IconCache = {} -IconCacheKeys = {} - -local function AddRange(self, otherTable) - local selfLength = #self - for i=1, #otherTable do - self[selfLength + i] = otherTable[i] - end -end - -local function GetDefaultIconList() - local icons = {} - - -- We need to avoid adding duplicate spellIDs from the spellbook tabs for your other specs. - local activeIcons = {}; - - for i = 1, C_SpellBook.GetNumSpellBookSkillLines() do - local skillLine = C_SpellBook.GetSpellBookSkillLineInfo(i) - local tab = skillLine.name - local tabTex = skillLine.iconID - local offset = skillLine.itemIndexOffset - local numSpells = skillLine.numSpellBookItems - offset = offset + 1; - local tabEnd = offset + numSpells; - for j = offset, tabEnd - 1 do - --to get spell info by slot, you have to pass in a pet argument - local spellType, ID = C_SpellBook.GetSpellBookItemType(j, Enum.SpellBookSpellBank.Player); - if (spellType ~= "FUTURESPELL") then - local fileID = C_SpellBook.GetSpellBookItemTexture(j, Enum.SpellBookSpellBank.Player); - if (fileID) then - activeIcons[fileID] = true; - end - end - if (spellType == "FLYOUT") then - local _, _, numSlots, isKnown = GetFlyoutInfo(ID); - if (isKnown and numSlots > 0) then - for k = 1, numSlots do - local spellID - spellID, _, isKnown = GetFlyoutSlotInfo(ID, k) - if (isKnown) then - local fileID = GetSpellTexture(spellID); - if (fileID) then - activeIcons[fileID] = true; - end - end - end - end - end - end - end - - for fileDataID in pairs(activeIcons) do - icons[#icons + 1] = fileDataID; - end - - GetLooseMacroIcons(icons); - GetLooseMacroItemIcons(icons); - GetMacroIcons(icons); - GetMacroItemIcons(icons); - - local iconListLength = #icons - for i=1, iconListLength do - if type(icons[i]) ~= "number" then - icons[i] = "INTERFACE\\ICONS\\"..icons[i] - end - end - - for i=1, #icons do - icons[i] = { Icon = icons[i], SpellId = nil } - end - - return icons -end - -MegaMacroIconNavigator = {} - -function MegaMacroIconNavigator.BeginLoadingIcons() - IconLoadingStarted = true -end - -function MegaMacroIconNavigator.OnUpdate() - if IconLoadingStarted and not IconLoadingFinished then - for _=1, FetchesPerFrame do - CurrentSpellId = CurrentSpellId + 1 - local spellInfo = C_Spell.GetSpellInfo(CurrentSpellId) - if (spellInfo == nil) then - return - end - local name, _, icon, _, _, _, spellId = spellInfo.name, nil, spellInfo.iconID, spellInfo.castTime, spellInfo.minRange, spellInfo.maxRange, spellInfo.spellID - - if icon == 136243 then - -- 136243 is the a gear icon, we can ignore those spells (courtesy of WeakAuras) - MissCount = 0 - elseif name then - if #name > 0 and icon then - name = string.lower(name) - MissCount = 0 - local cachedIconList = IconCache[name] - if not cachedIconList then - table.insert(IconCacheKeys, name) - cachedIconList = {} - IconCache[name] = cachedIconList - end - local hasIcon = false - for i=1, #cachedIconList do - if cachedIconList[i] == icon then - hasIcon = true - break - end - end - if not hasIcon then - table.insert(cachedIconList, { SpellId = spellId, Icon = icon }) - end - end - else - MissCount = MissCount + 1 - - if MissCount > 400 then - table.sort(IconCacheKeys) - IconLoadingFinished = true - CleanupPhase = true - break - end - end - end - elseif CleanupPhase then - CleanupPhase = false - end -end - -function MegaMacroIconNavigator.Search(searchText) - local priorityResults = {} - local otherResults = {} - local resultCount = 0 - local presentIcons = {} - - if searchText and #searchText > 2 then - searchText = string.lower(searchText) - local escapedSearch = string.gsub(searchText, "([%(%)%.%+%-%*%?%[%]%^%$%%])", "%%%1") - - for _, key in ipairs(IconCacheKeys) do - local index = string.find(key, escapedSearch) - - if index then - local itemList = IconCache[key] - for _, item in ipairs(itemList) do - if not presentIcons[item.Icon] then - presentIcons[item.Icon] = true - if index == 1 then - table.insert(priorityResults, item) - else - table.insert(otherResults, item) - end - - resultCount = resultCount + 1 - - if resultCount > 300 then - break - end - end - end - end - end - - AddRange(priorityResults, otherResults) - return priorityResults - else - return GetDefaultIconList() - end - +local FetchesPerFrame = 1000 + +local IconLoadingStarted = false +local MissCount = 0 +local CurrentSpellId = 0 +local IconLoadingFinished = false +local CleanupPhase = false + +local IconCache = {} +IconCacheKeys = {} + +local function AddRange(self, otherTable) + local selfLength = #self + for i=1, #otherTable do + self[selfLength + i] = otherTable[i] + end +end + +local function GetDefaultIconList() + local icons = {} + local activeIcons = {}; + + -- 12.0 uses C_SpellBook for all spellbook-related queries + local numSkillLines = C_SpellBook.GetNumSpellBookSkillLines() + for i = 1, numSkillLines do + local skillLine = C_SpellBook.GetSpellBookSkillLineInfo(i) + if skillLine then + local offset = skillLine.itemIndexOffset + 1 + local numSpells = skillLine.numSpellBookItems + local tabEnd = offset + numSpells + + for j = offset, tabEnd - 1 do + local spellType, ID = C_SpellBook.GetSpellBookItemType(j, Enum.SpellBookSpellBank.Player) + if (spellType ~= "FUTURESPELL") then + local fileID = C_SpellBook.GetSpellBookItemTexture(j, Enum.SpellBookSpellBank.Player) + if (fileID) then + activeIcons[fileID] = true + end + end + + if (spellType == "FLYOUT") then + -- 12.0 Migration: Flyout info moved to C_Spell + local _, _, numSlots, isKnown = C_Spell.GetFlyoutInfo(ID) + if (isKnown and numSlots > 0) then + for k = 1, numSlots do + local spellID, _ + spellID, _, isKnown = C_Spell.GetFlyoutSlotInfo(ID, k) + if (isKnown) then + local fileID = C_Spell.GetSpellTexture(spellID) + if (fileID) then + activeIcons[fileID] = true + end + end + end + end + end + end + end + end + + for fileDataID in pairs(activeIcons) do + icons[#icons + 1] = fileDataID + end + + -- 12.0 Migration: All Macro Icon functions moved to C_Macro namespace + C_Macro.GetLooseMacroIcons(icons) + C_Macro.GetLooseMacroItemIcons(icons) + C_Macro.GetMacroIcons(icons) + C_Macro.GetMacroItemIcons(icons) + + -- 12.0 Clean up: Blizzard prefers FileDataIDs (numbers) + for i=1, #icons do + local iconVal = icons[i] + if type(iconVal) == "string" and not iconVal:find("\\") then + -- If it's a legacy string name without a path, format it correctly + icons[i] = "INTERFACE\\ICONS\\" .. iconVal + end + end + + for i=1, #icons do + icons[i] = { Icon = icons[i], SpellId = nil } + end + + return icons +end + +MegaMacroIconNavigator = {} + +function MegaMacroIconNavigator.BeginLoadingIcons() + IconLoadingStarted = true +end + +function MegaMacroIconNavigator.OnUpdate() + if IconLoadingStarted and not IconLoadingFinished then + for _=1, FetchesPerFrame do + CurrentSpellId = CurrentSpellId + 1 + + -- 12.0: C_Spell.GetSpellInfo returns a table. + local spellInfo = C_Spell.GetSpellInfo(CurrentSpellId) + + if spellInfo then + local name = spellInfo.name + local icon = spellInfo.iconID + local spellId = spellInfo.spellID + + if icon == 136243 then + -- Ignore generic gear icons + MissCount = 0 + elseif name and #name > 0 and icon then + name = string.lower(name) + MissCount = 0 + local cachedIconList = IconCache[name] + if not cachedIconList then + table.insert(IconCacheKeys, name) + cachedIconList = {} + IconCache[name] = cachedIconList + end + + local hasIcon = false + for i=1, #cachedIconList do + if cachedIconList[i].Icon == icon then + hasIcon = true + break + end + end + + if not hasIcon then + table.insert(cachedIconList, { SpellId = spellId, Icon = icon }) + end + else + MissCount = MissCount + 1 + end + else + MissCount = MissCount + 1 + end + + -- 12.0: Spell IDs now exceed 500,000. + -- We increase the MissCount threshold to ensure we don't stop too early. + if MissCount > 2000 then + table.sort(IconCacheKeys) + IconLoadingFinished = true + CleanupPhase = true + break + end + end + elseif CleanupPhase then + CleanupPhase = false + end +end + +function MegaMacroIconNavigator.Search(searchText) + local priorityResults = {} + local otherResults = {} + local resultCount = 0 + local presentIcons = {} + + if searchText and #searchText > 2 then + searchText = string.lower(searchText) + -- Escape special characters for Lua pattern matching + local escapedSearch = string.gsub(searchText, "([%(%)%.%+%-%*%?%[%]%^%$%%])", "%%%1") + + for _, key in ipairs(IconCacheKeys) do + local index = string.find(key, escapedSearch) + + if index then + local itemList = IconCache[key] + for _, item in ipairs(itemList) do + if not presentIcons[item.Icon] then + presentIcons[item.Icon] = true + if index == 1 then + table.insert(priorityResults, item) + else + table.insert(otherResults, item) + end + + resultCount = resultCount + 1 + if resultCount > 500 then -- Increased limit for 12.0 UI + break + end + end + end + end + end + + AddRange(priorityResults, otherResults) + return priorityResults + else + return GetDefaultIconList() + end end \ No newline at end of file diff --git a/src/mega-macro.lua b/src/mega-macro.lua index 30932fa..05e2f3c 100644 --- a/src/mega-macro.lua +++ b/src/mega-macro.lua @@ -1,262 +1,250 @@ -local MacroIdDisplayNamePartLength = 8 - -local function RemoveItemFromArray(t, item) - local length = #t - - for i=0, length do - if t[i] == item then - table.remove(t, i) - break - end - end -end - -local function GetNextAvailableMacroId(startOffset, count, existingMacros) - for i=1 + startOffset, startOffset + count do - local isMatched = false - - for _, existingMacro in ipairs(existingMacros) do - if existingMacro.Id == i then - isMatched = true - break - end - end - - if not isMatched then - return i - end - end - - return nil -end - -MegaMacro = {} -MegaMacro.GetNextAvailableMacroId = GetNextAvailableMacroId - -function MegaMacro.Create(displayName, scope, staticTexture, isStaticTextureFallback, code, macroIndex) - local result = {} - - local id - local scopedIndex - local macroList - - if scope == MegaMacroScopes.Global then - macroList = MegaMacroGlobalData.Macros - scopedIndex = #macroList + 1 - - if scopedIndex > MacroLimits.GlobalCount then - return nil - end - - id = GetNextAvailableMacroId(MacroIndexOffsets.Global, MacroLimits.GlobalCount, macroList) - elseif scope == MegaMacroScopes.Class then - if MegaMacroGlobalData.Classes[MegaMacroCachedClass] == nil then - MegaMacroGlobalData.Classes[MegaMacroCachedClass] = { Macros = {}, Specializations = {} } - end - - macroList = MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros - scopedIndex = #macroList + 1 - - if scopedIndex > MacroLimits.PerClassCount then - return nil - end - - id = GetNextAvailableMacroId(MacroIndexOffsets.PerClass, MacroLimits.PerClassCount, macroList) - result.Class = MegaMacroCachedClass - elseif scope == MegaMacroScopes.Specialization then - if MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] == nil then - MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] = { Macros = {} } - end - - macroList = MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros - scopedIndex = #macroList + 1 - - if scopedIndex > MacroLimits.PerSpecializationCount then - return nil - end - - id = GetNextAvailableMacroId(MacroIndexOffsets.PerSpecialization, MacroLimits.PerSpecializationCount, macroList) - result.Class = MegaMacroCachedClass - result.Specialization = MegaMacroCachedSpecialization - elseif scope == MegaMacroScopes.Character then - macroList = MegaMacroCharacterData.Macros - scopedIndex = #macroList + 1 - - if scopedIndex > MacroLimits.PerCharacterCount then - return nil - end - - id = GetNextAvailableMacroId(MacroIndexOffsets.PerCharacter, MacroLimits.PerCharacterCount, macroList) - result.Class = MegaMacroCachedClass - elseif scope == MegaMacroScopes.CharacterSpecialization then - if MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] == nil then - MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] = { Macros = {} } - end - - macroList = MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros - scopedIndex = #macroList + 1 - - if scopedIndex > MacroLimits.PerCharacterSpecializationCount then - return nil - end - - id = GetNextAvailableMacroId(MacroIndexOffsets.PerCharacterSpecialization, MacroLimits.PerCharacterSpecializationCount, macroList) - result.Class = MegaMacroCachedClass - result.Specialization = MegaMacroCachedSpecialization - elseif scope == MegaMacroScopes.Inactive then - macroList = MegaMacroGlobalData.InactiveMacros - scopedIndex = #macroList + 1 - - if scopedIndex > MacroLimits.InactiveCount then - return nil - end - - id = GetNextAvailableMacroId(MacroIndexOffsets.Inactive, MacroLimits.InactiveCount, macroList) - else - return nil - end - - if id == nil then - return nil - end - - table.insert(macroList, result) - - result.Id = id - result.Scope = scope - result.ScopedIndex = scopedIndex - result.DisplayName = displayName - result.Code = code or "" - result.StaticTexture = staticTexture - result.IsStaticTextureFallback = isStaticTextureFallback - - MegaMacroEngine.OnMacroCreated(result, macroIndex) - - return result -end - -function MegaMacro.GetSlotCount(scope) - if scope == MegaMacroScopes.Global then - return MacroLimits.GlobalCount - elseif scope == MegaMacroScopes.Class then - return MacroLimits.PerClassCount - elseif scope == MegaMacroScopes.Specialization then - return MacroLimits.PerSpecializationCount - elseif scope == MegaMacroScopes.Character then - return MacroLimits.PerCharacterCount - elseif scope == MegaMacroScopes.CharacterSpecialization then - return MacroLimits.PerCharacterSpecializationCount - elseif scope == MegaMacroScopes.Inactive then - return MacroLimits.InactiveCount - end - - return 0 -end - -function MegaMacro.GetById(macroId) - local scope - - if not macroId then - return nil - elseif macroId <= MacroIndexOffsets.PerClass then - scope = MegaMacroScopes.Global - elseif macroId <= MacroIndexOffsets.PerSpecialization then - scope = MegaMacroScopes.Class - elseif macroId <= MacroIndexOffsets.PerCharacter then - scope = MegaMacroScopes.Specialization - elseif macroId <= MacroIndexOffsets.PerCharacterSpecialization then - scope = MegaMacroScopes.Character - elseif macroId <= MacroIndexOffsets.Inactive then - scope = MegaMacroScopes.CharacterSpecialization - else - scope = MegaMacroScopes.Inactive - end - - local macros = MegaMacro.GetMacrosInScope(scope) - local macroCount = #macros - - for i=1, macroCount do - if macros[i].Id == macroId then - return macros[i] - end - end - - return nil -end - -function MegaMacro.UpdateDetails(self, displayName, staticTexture, isStaticTextureFallback) - self.DisplayName = displayName - self.StaticTexture = staticTexture - self.IsStaticTextureFallback = isStaticTextureFallback - MegaMacroEngine.OnMacroRenamed(self) - MegaMacroIconEvaluator.UpdateMacro(self) -end - -function MegaMacro.UpdateCode(self, code) - self.Code = code - MegaMacroEngine.OnMacroUpdated(self) - MegaMacroCodeInfo.Clear(self.Id) - MegaMacroIconEvaluator.UpdateMacro(self) -end - -function MegaMacro.Delete(self) - if self.Scope == MegaMacroScopes.Global then - RemoveItemFromArray(MegaMacroGlobalData.Macros, self) - elseif self.Scope == MegaMacroScopes.Class then - RemoveItemFromArray(MegaMacroGlobalData.Classes[self.Class].Macros, self) - elseif self.Scope == MegaMacroScopes.Specialization then - RemoveItemFromArray(MegaMacroGlobalData.Classes[self.Class].Specializations[self.Specialization].Macros, self) - elseif self.Scope == MegaMacroScopes.Character then - RemoveItemFromArray(MegaMacroCharacterData.Macros, self) - elseif self.Scope == MegaMacroScopes.CharacterSpecialization then - RemoveItemFromArray(MegaMacroCharacterData.Specializations[self.Specialization].Macros, self) - elseif self.Scope == MegaMacroScopes.Inactive then - RemoveItemFromArray(MegaMacroGlobalData.InactiveMacros, self) - end - - MegaMacroEngine.OnMacroDeleted(self) - MegaMacroCodeInfo.Clear(self.Id) - MegaMacroIconEvaluator.RemoveMacroFromCache(self.Id) -end - -function MegaMacro.Move(self, newScope) - local newMacro = MegaMacro.Create(self.DisplayName, newScope, self.StaticTexture) - MegaMacro.UpdateCode(newMacro, self.Code) - MegaMacroEngine.OnMacroMoved(self, newMacro) - MegaMacro.Delete(self) - return newMacro -end - -function MegaMacro.GetMacrosInScope(scope) - if scope == MegaMacroScopes.Global then - return MegaMacroGlobalData.Macros - elseif scope == MegaMacroScopes.Class then - if MegaMacroGlobalData.Classes[MegaMacroCachedClass] == nil then - MegaMacroGlobalData.Classes[MegaMacroCachedClass] = { Macros = {}, Specializations = {} } - end - return MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros - elseif scope == MegaMacroScopes.Specialization then - if MegaMacroCachedSpecialization == nil then - return {} - end - if MegaMacroGlobalData.Classes[MegaMacroCachedClass] == nil then - MegaMacroGlobalData.Classes[MegaMacroCachedClass] = { Macros = {}, Specializations = {} } - end - if MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] == nil then - MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] = { Macros = {} } - end - return MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros - elseif scope == MegaMacroScopes.Character then - return MegaMacroCharacterData.Macros - elseif scope == MegaMacroScopes.CharacterSpecialization then - if MegaMacroCachedSpecialization == nil then - return {} - end - if MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] == nil then - MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] = { Macros = {} } - end - return MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros - elseif scope == MegaMacroScopes.Inactive then - return MegaMacroGlobalData.InactiveMacros - end +local MacroIdDisplayNamePartLength = 8 + +-- Fixed for 12.0: Lua arrays are 1-indexed. +-- The original 0-index loop could cause table overflows or nil errors. +local function RemoveItemFromArray(t, item) + if not t then return end + for i = #t, 1, -1 do + if t[i] == item then + table.remove(t, i) + break + end + end +end + +local function GetNextAvailableMacroId(startOffset, count, existingMacros) + for i=1 + startOffset, startOffset + count do + local isMatched = false + + for _, existingMacro in ipairs(existingMacros) do + if existingMacro.Id == i then + isMatched = true + break + end + end + + if not isMatched then + return i + end + end + + return nil +end + +MegaMacro = {} +MegaMacro.GetNextAvailableMacroId = GetNextAvailableMacroId + +function MegaMacro.Create(displayName, scope, staticTexture, isStaticTextureFallback, code, macroIndex) + local result = {} + local id + local scopedIndex + local macroList + + -- 12.0 Safety: Ensure we have character/class data before creating + if not MegaMacroCachedClass then + local _, class = UnitClass("player") + MegaMacroCachedClass = class + end + + if scope == MegaMacroScopes.Global then + macroList = MegaMacroGlobalData.Macros + scopedIndex = #macroList + 1 + if scopedIndex > MacroLimits.GlobalCount then return nil end + id = GetNextAvailableMacroId(MacroIndexOffsets.Global, MacroLimits.GlobalCount, macroList) + + elseif scope == MegaMacroScopes.Class then + if MegaMacroGlobalData.Classes[MegaMacroCachedClass] == nil then + MegaMacroGlobalData.Classes[MegaMacroCachedClass] = { Macros = {}, Specializations = {} } + end + macroList = MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros + scopedIndex = #macroList + 1 + if scopedIndex > MacroLimits.PerClassCount then return nil end + id = GetNextAvailableMacroId(MacroIndexOffsets.PerClass, MacroLimits.PerClassCount, macroList) + result.Class = MegaMacroCachedClass + + elseif scope == MegaMacroScopes.Specialization then + if not MegaMacroCachedSpecialization then return nil end -- Safety for 12.0 loading + if MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] == nil then + MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] = { Macros = {} } + end + macroList = MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros + scopedIndex = #macroList + 1 + if scopedIndex > MacroLimits.PerSpecializationCount then return nil end + id = GetNextAvailableMacroId(MacroIndexOffsets.PerSpecialization, MacroLimits.PerSpecializationCount, macroList) + result.Class = MegaMacroCachedClass + result.Specialization = MegaMacroCachedSpecialization + + elseif scope == MegaMacroScopes.Character then + macroList = MegaMacroCharacterData.Macros + scopedIndex = #macroList + 1 + if scopedIndex > MacroLimits.PerCharacterCount then return nil end + id = GetNextAvailableMacroId(MacroIndexOffsets.PerCharacter, MacroLimits.PerCharacterCount, macroList) + result.Class = MegaMacroCachedClass + + elseif scope == MegaMacroScopes.CharacterSpecialization then + if not MegaMacroCachedSpecialization then return nil end + if MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] == nil then + MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] = { Macros = {} } + end + macroList = MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros + scopedIndex = #macroList + 1 + if scopedIndex > MacroLimits.PerCharacterSpecializationCount then return nil end + id = GetNextAvailableMacroId(MacroIndexOffsets.PerCharacterSpecialization, MacroLimits.PerCharacterSpecializationCount, macroList) + result.Class = MegaMacroCachedClass + result.Specialization = MegaMacroCachedSpecialization + + elseif scope == MegaMacroScopes.Inactive then + macroList = MegaMacroGlobalData.InactiveMacros + scopedIndex = #macroList + 1 + if scopedIndex > MacroLimits.InactiveCount then return nil end + id = GetNextAvailableMacroId(MacroIndexOffsets.Inactive, MacroLimits.InactiveCount, macroList) + else + return nil + end + + if id == nil then return nil end + + table.insert(macroList, result) + + result.Id = id + result.Scope = scope + result.ScopedIndex = scopedIndex + result.DisplayName = displayName + result.Code = code or "" + result.StaticTexture = staticTexture + result.IsStaticTextureFallback = isStaticTextureFallback + + -- Trigger the Engine to update the actual Blizzard macro + if MegaMacroEngine and MegaMacroEngine.OnMacroCreated then + MegaMacroEngine.OnMacroCreated(result, macroIndex) + end + + return result +end + +function MegaMacro.GetSlotCount(scope) + if scope == MegaMacroScopes.Global then return MacroLimits.GlobalCount + elseif scope == MegaMacroScopes.Class then return MacroLimits.PerClassCount + elseif scope == MegaMacroScopes.Specialization then return MacroLimits.PerSpecializationCount + elseif scope == MegaMacroScopes.Character then return MacroLimits.PerCharacterCount + elseif scope == MegaMacroScopes.CharacterSpecialization then return MacroLimits.PerCharacterSpecializationCount + elseif scope == MegaMacroScopes.Inactive then return MacroLimits.InactiveCount + end + return 0 +end + +function MegaMacro.GetById(macroId) + if not macroId then return nil end + local scope + + if macroId <= MacroIndexOffsets.PerClass then + scope = MegaMacroScopes.Global + elseif macroId <= MacroIndexOffsets.PerSpecialization then + scope = MegaMacroScopes.Class + elseif macroId <= MacroIndexOffsets.PerCharacter then + scope = MegaMacroScopes.Specialization + elseif macroId <= MacroIndexOffsets.PerCharacterSpecialization then + scope = MegaMacroScopes.Character + elseif macroId <= MacroIndexOffsets.Inactive then + scope = MegaMacroScopes.CharacterSpecialization + else + scope = MegaMacroScopes.Inactive + end + + local macros = MegaMacro.GetMacrosInScope(scope) + if not macros then return nil end + + for i=1, #macros do + if macros[i].Id == macroId then + return macros[i] + end + end + + return nil +end + +function MegaMacro.UpdateDetails(self, displayName, staticTexture, isStaticTextureFallback) + self.DisplayName = displayName + self.StaticTexture = staticTexture + self.IsStaticTextureFallback = isStaticTextureFallback + + if MegaMacroEngine and MegaMacroEngine.OnMacroRenamed then + MegaMacroEngine.OnMacroRenamed(self) + end + + if MegaMacroIconEvaluator then + MegaMacroIconEvaluator.UpdateMacro(self) + end +end + +function MegaMacro.UpdateCode(self, code) + self.Code = code + if MegaMacroEngine and MegaMacroEngine.OnMacroUpdated then + MegaMacroEngine.OnMacroUpdated(self) + end + if MegaMacroCodeInfo then MegaMacroCodeInfo.Clear(self.Id) end + if MegaMacroIconEvaluator then MegaMacroIconEvaluator.UpdateMacro(self) end +end + +function MegaMacro.Delete(self) + -- Fixed: Added safety checks for table existence + if self.Scope == MegaMacroScopes.Global then + RemoveItemFromArray(MegaMacroGlobalData.Macros, self) + elseif self.Scope == MegaMacroScopes.Class and MegaMacroGlobalData.Classes[self.Class] then + RemoveItemFromArray(MegaMacroGlobalData.Classes[self.Class].Macros, self) + elseif self.Scope == MegaMacroScopes.Specialization and MegaMacroGlobalData.Classes[self.Class] then + RemoveItemFromArray(MegaMacroGlobalData.Classes[self.Class].Specializations[self.Specialization].Macros, self) + elseif self.Scope == MegaMacroScopes.Character then + RemoveItemFromArray(MegaMacroCharacterData.Macros, self) + elseif self.Scope == MegaMacroScopes.CharacterSpecialization and MegaMacroCharacterData.Specializations[self.Specialization] then + RemoveItemFromArray(MegaMacroCharacterData.Specializations[self.Specialization].Macros, self) + elseif self.Scope == MegaMacroScopes.Inactive then + RemoveItemFromArray(MegaMacroGlobalData.InactiveMacros, self) + end + + if MegaMacroEngine then MegaMacroEngine.OnMacroDeleted(self) end + if MegaMacroCodeInfo then MegaMacroCodeInfo.Clear(self.Id) end + if MegaMacroIconEvaluator then MegaMacroIconEvaluator.RemoveMacroFromCache(self.Id) end +end + +function MegaMacro.Move(self, newScope) + local newMacro = MegaMacro.Create(self.DisplayName, newScope, self.StaticTexture) + if newMacro then + MegaMacro.UpdateCode(newMacro, self.Code) + if MegaMacroEngine then MegaMacroEngine.OnMacroMoved(self, newMacro) end + MegaMacro.Delete(self) + return newMacro + end +end + +function MegaMacro.GetMacrosInScope(scope) + if scope == MegaMacroScopes.Global then + return MegaMacroGlobalData.Macros + elseif scope == MegaMacroScopes.Class then + if not MegaMacroCachedClass then return {} end + if MegaMacroGlobalData.Classes[MegaMacroCachedClass] == nil then + MegaMacroGlobalData.Classes[MegaMacroCachedClass] = { Macros = {}, Specializations = {} } + end + return MegaMacroGlobalData.Classes[MegaMacroCachedClass].Macros + elseif scope == MegaMacroScopes.Specialization then + if not MegaMacroCachedClass or not MegaMacroCachedSpecialization then return {} end + if MegaMacroGlobalData.Classes[MegaMacroCachedClass] == nil then + MegaMacroGlobalData.Classes[MegaMacroCachedClass] = { Macros = {}, Specializations = {} } + end + if MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] == nil then + MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization] = { Macros = {} } + end + return MegaMacroGlobalData.Classes[MegaMacroCachedClass].Specializations[MegaMacroCachedSpecialization].Macros + elseif scope == MegaMacroScopes.Character then + return MegaMacroCharacterData.Macros + elseif scope == MegaMacroScopes.CharacterSpecialization then + if not MegaMacroCachedSpecialization then return {} end + if MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] == nil then + MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization] = { Macros = {} } + end + return MegaMacroCharacterData.Specializations[MegaMacroCachedSpecialization].Macros + elseif scope == MegaMacroScopes.Inactive then + return MegaMacroGlobalData.InactiveMacros + end + return {} end \ No newline at end of file diff --git a/src/tooltip-functions.lua b/src/tooltip-functions.lua index 3842a5f..d32dbcb 100644 --- a/src/tooltip-functions.lua +++ b/src/tooltip-functions.lua @@ -1,23 +1,41 @@ -function ShowToolTipForMegaMacro(macroId) - GameTooltip:Hide() - GameTooltip_SetDefaultAnchor(GameTooltip, UIParent) - - local data = MegaMacroIconEvaluator.GetCachedData(macroId) - - if data then - if data.Type == "spell" then - GameTooltip:SetSpellByID(data.Id) - -- GameTooltip:SetToyByItemID(itemId) - elseif data.Type == "item" then - -- GameTooltip:SetItemByID(data.Id) - GameTooltip:SetInventoryItemByID(data.Id) - elseif data.Type == "equipment set" then - GameTooltip:SetEquipmentSet(data.Name) - else - local megaMacro = MegaMacro.GetById(macroId) - GameTooltip:SetText(megaMacro.DisplayName or "", 1, 1, 1) - end - - GameTooltip:Show() - end +function ShowToolTipForMegaMacro(macroId) + -- 12.0 Safety: Ensure the tooltip exists + if not GameTooltip then return end + + GameTooltip:Hide() + + -- 12.0 Requirement: Tooltips must have an owner set before content is added. + -- "ANCHOR_CURSOR" puts it near the mouse, which is standard for macros. + -- You can also use "ANCHOR_RIGHT" if you prefer the bottom-right corner. + GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR") + + local data = MegaMacroIconEvaluator.GetCachedData(macroId) + + if data then + if data.Type == "spell" then + -- 12.0: SetSpellByID expects the numeric SpellID + GameTooltip:SetSpellByID(data.Id) + elseif data.Type == "item" then + -- 12.0 Fix: SetInventoryItemByID is deprecated for general item links. + -- Use SetItemByID instead. + GameTooltip:SetItemByID(data.Id) + elseif data.Type == "equipment set" then + GameTooltip:SetEquipmentSet(data.Name) + else + -- Fallback for text-only macros + local megaMacro = MegaMacro.GetById(macroId) + if megaMacro and megaMacro.DisplayName then + GameTooltip:SetText(megaMacro.DisplayName, 1, 1, 1) + end + end + + GameTooltip:Show() + else + -- If data is missing (e.g. during loading), try to show the name at least + local megaMacro = MegaMacro.GetById(macroId) + if megaMacro and megaMacro.DisplayName then + GameTooltip:SetText(megaMacro.DisplayName, 1, 1, 1) + GameTooltip:Show() + end + end end \ No newline at end of file diff --git a/src/windows/mega-macro.window.lua b/src/windows/mega-macro.window.lua index 12fb682..5b8540f 100644 --- a/src/windows/mega-macro.window.lua +++ b/src/windows/mega-macro.window.lua @@ -1,902 +1,931 @@ -local rendering = { - MacrosPerRow = 12, - CharLimitMessageFormat = "%s/%s Characters Used" -} - -local PopupModes = { - New = 0, - Edit = 1 -} - -local PlusTexture = 3192688--135769 - -local IsOpen = false -local SelectedScope = MegaMacroScopes.Global -local MacroItems = {} -local SelectedMacro = nil -local PopupMode = nil -local IconListInitialized = false -local SelectedIcon = nil -local IconList = {} -local SelectedTabIndex = 1 - -NUM_ICONS_PER_ROW = 10 -NUM_ICON_ROWS = 9 -NUM_MACRO_ICONS_SHOWN = NUM_ICONS_PER_ROW * NUM_ICON_ROWS -MACRO_ICON_ROW_HEIGHT = 36 -MegaMacroWindowTogglingMode = false - -local function GetScopeFromTabIndex(tabIndex) - if tabIndex == 1 then - return MegaMacroScopes.Global - elseif tabIndex == 2 then - return MegaMacroScopes.Class - elseif tabIndex == 3 then - return MegaMacroScopes.Specialization - elseif tabIndex == 4 then - return MegaMacroScopes.Character - elseif tabIndex == 5 then - return MegaMacroScopes.Inactive - end -end - -local function GetMacroButtonUI(index) - local buttonName = "MegaMacro_MacroButton" .. index - return _G[buttonName], _G[buttonName].Name, _G[buttonName].Icon -end - --- Creates the button frames for the macro slots -local function CreateMacroSlotFrames() - for i=1, HighestMaxMacroCount do - local button = CreateFrame("CheckButton", "MegaMacro_MacroButton" .. i, MegaMacro_ButtonContainer, "MegaMacro_ButtonTemplate") - button:SetID(i) - if i == 1 then - button:SetPoint("TOPLEFT", MegaMacro_ButtonContainer, "TOPLEFT", 6, -6) - elseif mod(i, rendering.MacrosPerRow) == 1 then - button:SetPoint("TOP", "MegaMacro_MacroButton"..(i-rendering.MacrosPerRow), "BOTTOM", 0, -10) - else - button:SetPoint("LEFT", "MegaMacro_MacroButton"..(i-1), "RIGHT", 13, 0) - end - end -end - -local function FixIconPanelPosition() - local button = MegaMacro_PopupButton1 - local point, relativeTo, relativePoint, x, y = button:GetPoint() - button:SetPoint(point, relativeTo:GetName(), relativePoint, x, y - 40) -end - -local function UpdateIconList() - local searchText = MegaMacro_IconSearchBox:GetText() - local items = MegaMacroIconNavigator.Search(searchText) - local itemCount = #items - - IconList = { { Name = "No Icon", Icon = MegaMacroTexture } } - - for i=1, itemCount do - IconList[i + 1] = items[i] - end -end - -local function InitializeIconListPanel() - if not IconListInitialized then - BuildIconArray(MegaMacro_PopupFrame, "MegaMacro_PopupButton", "MegaMacro_PopupButtonTemplate", NUM_ICONS_PER_ROW, NUM_ICON_ROWS) - FixIconPanelPosition() - IconListInitialized = true - end -end - -local function InitializeTabs() - local playerName = UnitName("player") - MegaMacro_FrameTab2:SetText(MegaMacroCachedClass) - MegaMacro_FrameTab4:SetText(playerName) - MegaMacro_FrameTab5:SetText("Inactive") - MegaMacro_FrameTab6:SetText("Config") - - if MegaMacroCachedSpecialization == '' then - MegaMacro_FrameTab3:SetText("Locked") - MegaMacro_FrameTab3:Disable() - else - MegaMacro_FrameTab3:SetText(MegaMacroCachedSpecialization) - MegaMacro_FrameTab3:Enable() - end -end - --- Shows and hides macro slot buttons based on the number of slots available in the scope -local function InitializeMacroSlots() - local scopeSlotCount = MegaMacro.GetSlotCount(SelectedScope) - - for i=1, scopeSlotCount do - local buttonFrame = _G["MegaMacro_MacroButton" .. i] - - if buttonFrame == nil then - break - end - - buttonFrame:Show() - end - - for i=scopeSlotCount+1, HighestMaxMacroCount do - local buttonFrame = _G["MegaMacro_MacroButton" .. i] - - if buttonFrame == nil then - break - end - - buttonFrame:Hide() - end -end - -local function SaveMacro() - local newCode = MegaMacro_FrameText:GetText() - if SelectedMacro ~= nil and SelectedMacro.Code ~= newCode then - MegaMacro.UpdateCode(SelectedMacro, newCode) - end - - MegaMacro_SaveButton:Disable() - MegaMacro_CancelButton:Disable() -end - -local function RefreshSelectedMacroIcon() - local displayedTexture = "" - - if SelectedMacro then - if SelectedIcon == MegaMacroTexture then - local data = MegaMacroIconEvaluator.GetCachedData(SelectedMacro.Id) - displayedTexture = data and data.Icon or MegaMacroTexture - else - local isStaticTextureFallback = MegaMacro_FallbackTextureCheckBox:GetChecked() - displayedTexture = select(4, MegaMacroIconEvaluator.ComputeMacroIcon(SelectedMacro, SelectedIcon, isStaticTextureFallback)) - end - end - - MegaMacro_FrameSelectedMacroButtonIcon:SetTexture(displayedTexture) -end - -local function SelectIcon(texture) - SelectedIcon = texture or MegaMacroTexture - RefreshSelectedMacroIcon() - - local i = 1 - while true do - local iconButton = _G["MegaMacro_PopupButton"..i] - local iconButtonIcon = _G["MegaMacro_PopupButton"..i.."Icon"] - - if not iconButton then - break - end - - iconButton:SetChecked(SelectedIcon == iconButtonIcon:GetTexture()) - i = i + 1 - end -end - -local function SelectMacro(macro) - SaveMacro() - SelectedMacro = nil - MegaMacro_PopupFrame:Hide() - MegaMacro_FrameSelectedMacroName:SetText("") - MegaMacro_FrameSelectedMacroButtonIcon:SetTexture("") - MegaMacro_FrameText:SetText("") - MegaMacro_EditButton:Disable(); - MegaMacro_DeleteButton:Disable(); - MegaMacro_SaveButton:Disable() - MegaMacro_CancelButton:Disable() - MegaMacro_FrameText:Disable() - - for i=1, HighestMaxMacroCount do - local buttonFrame, _, buttonIcon = GetMacroButtonUI(i) - - if macro and buttonFrame.Macro == macro then - buttonFrame:SetChecked(true) - SelectedMacro = macro - MegaMacro_FrameSelectedMacroName:SetText(macro.DisplayName) - MegaMacro_FrameSelectedMacroButtonIcon:SetTexture(buttonIcon:GetTexture()) - MegaMacro_FrameText:SetText(macro.Code) - MegaMacro_EditButton:Enable(); - MegaMacro_DeleteButton:Enable(); - MegaMacro_FrameText:Enable() - else - buttonFrame:SetChecked(false) - end - end - - SelectIcon(macro and macro.StaticTexture) -end - -local function SetMacroItems() - local items = MegaMacro.GetMacrosInScope(SelectedScope) - MacroItems = items or {} - - table.sort( - MacroItems, - function(left, right) - return (left.DisplayName or "") < (right.DisplayName or "") - end) - - local newMacroButtonCreated = false - - for i=1, HighestMaxMacroCount do - local buttonFrame, buttonName, buttonIcon = GetMacroButtonUI(i) - - local macro = MacroItems[i] - - if macro then - buttonFrame.Macro = macro - buttonFrame.IsNewButton = false - -- buttonFrame:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") - buttonName:SetText(macro.DisplayName) - MegaMacroIconEvaluator.UpdateMacro(macro) - local data = MegaMacroIconEvaluator.GetCachedData(macro.Id) - buttonIcon:SetTexture(data and data.Icon or MegaMacroTexture) - buttonIcon:SetDesaturated(false) - buttonIcon:SetTexCoord(0, 1, 0, 1) - buttonIcon:SetAlpha(1) - if not MegaMacroEngine.GetMacroIndexFromId(macro.Id) then - buttonIcon:SetAlpha(0.5) - buttonIcon:SetDesaturated(true) - end - elseif not newMacroButtonCreated and SelectedScope ~= MegaMacroScopes.Inactive then - buttonFrame.Macro = nil - buttonFrame.IsNewButton = true - -- buttonFrame:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") - buttonName:SetText("") - buttonIcon:SetTexture(PlusTexture) - buttonIcon:SetDesaturated(true) - buttonIcon:SetTexCoord(.08, .92, .08, .92) - buttonIcon:SetAlpha(0.5) - newMacroButtonCreated = true - else - buttonFrame.Macro = nil - buttonFrame.IsNewButton = false - -- buttonFrame:SetHighlightTexture(nil) - buttonName:SetText("") - buttonIcon:SetTexture("") - buttonIcon:SetDesaturated(false) - buttonIcon:SetTexCoord(0, 1, 0, 1) - buttonIcon:SetAlpha(1) - end - end - - SelectMacro(MacroItems[1]) -end - -local function NewMacro() - SelectMacro(nil) - - local button = _G["MegaMacro_MacroButton"..(#MacroItems + 1)] - - if button then - button:SetChecked(true) - end - - PopupMode = PopupModes.New - MegaMacro_PopupEditBox:SetText("") - MegaMacro_FallbackTextureCheckBox:SetChecked(true) - MegaMacro_IconSearchBox:SetText("") - MegaMacro_PopupFrame:Show() -end - -local function DeleteMegaMacro() - if SelectedMacro ~= nil then - MegaMacro.Delete(SelectedMacro) - SetMacroItems() - end -end - -local function UpdateTooltipIfButtonIsHovered(updatedMacroId) - local mouseFocus = GetMouseFoci()[1] - - if mouseFocus then - local focusFrame = mouseFocus:GetName() - - if focusFrame then - if string.find(focusFrame, "^MegaMacro_MacroButton%d+$") then - local macro = _G[focusFrame].Macro - - if macro and macro.Id == updatedMacroId then - ShowToolTipForMegaMacro(macro.Id) - end - elseif focusFrame == "MegaMacro_FrameSelectedMacroButton" and SelectedMacro and SelectedMacro.Id == updatedMacroId then - ShowToolTipForMegaMacro(SelectedMacro.Id) - end - end - end -end - -local function PickupMegaMacro(macro) - local macroIndex = MegaMacroEngine.GetMacroIndexFromId(macro.Id) - -- highlight all tabs to indicate it can be moved to a tab - for i=1, 5 do - if i ~= SelectedTabIndex then - local tab = _G["MegaMacro_FrameTab"..i] - tab:LockHighlight() - end - end - - if macroIndex then - PickupMacro(macroIndex) - end -end - -local function HandleReceiveDrag(targetScope) - local type, macroIndex = GetCursorInfo() - - if type == "macro" then - local macroId = MegaMacroEngine.GetMacroIdFromIndex(macroIndex) - - if macroId then - local macro = MegaMacro.GetById(macroId) - ClearCursor() - - if IsControlKeyDown() then - local newDisplayName = macro.Scope == targetScope and macro.DisplayName.." copy" or macro.DisplayName - local newMacro = MegaMacro.Create(newDisplayName, targetScope, macro.StaticTexture) - MegaMacro.UpdateCode(newMacro, macro.Code) - - if targetScope == SelectedScope then - SetMacroItems() - SelectMacro(newMacro) - end - elseif targetScope == macro.Scope then - -- do nothing - else - local newMacro = MegaMacro.Move(macro, targetScope) - - if targetScope == SelectedScope then - SetMacroItems() - SelectMacro(newMacro) - elseif macro.Scope == SelectedScope then - SetMacroItems() - end - end - else - ClearCursor() - local name, _, body = GetMacroInfo(macroIndex) - local newMacro = MegaMacro.Create(name, targetScope, MegaMacroTexture) - - if newMacro then - MegaMacro.UpdateCode(newMacro, body) - end - - SetMacroItems() - SelectMacro(newMacro) - - if not InCombatLockdown() then - local newMacroIndex = MegaMacroEngine.GetMacroIndexFromId(newMacro.Id) - if newMacroIndex then - for i=1, 120 do - local actionButtonType, actionButtonArg = GetActionInfo(i) - if actionButtonType == "macro" and actionButtonArg == macroIndex then - PickupMacro(newMacroIndex) - PlaceAction(i) - ClearCursor() - end - end - end - - DeleteMacro(macroIndex) - - if MacroFrame:IsVisible() then - MacroFrame_Update() - if MacroFrame.selectedTab == 1 then - MacroFrame_SetAccountMacros() - else - MacroFrame_SetCharacterMacros() - end - end - end - end - end - - return type ~= nil -end - -local function UpdateSearchPlaceholder() - if MegaMacro_IconSearchBox:GetText() == "" then - MegaMacro_IconSearchPlaceholder:SetAlpha(0.4) - else - MegaMacro_IconSearchPlaceholder:SetAlpha(0.0) - end -end - -StaticPopupDialogs["CONFIRM_DELETE_SELECTED_MEGA_MACRO"] = { - text = CONFIRM_DELETE_MACRO, - button1 = OKAY, - button2 = CANCEL, - OnAccept = DeleteMegaMacro, - timeout = 0, - whileDead = 1, - showAlert = 1 -} - -MegaMacroWindow = { - Show = function() - if MegaMacroConfig_IsWindowDialog() then - MegaMacro_Frame:SetMovable(false) - MegaMacro_ToggleWindowModeButton:SetText("Unlock") - ShowUIPanel(MegaMacro_Frame); - else - local relativePoint, x, y = MegaMacroConfig_GetWindowPosition() - MegaMacro_Frame:SetMovable(true) - MegaMacro_Frame:SetSize(640, 524) - MegaMacro_Frame:ClearAllPoints() - MegaMacro_Frame:SetPoint(relativePoint, x, y) - MegaMacro_ToggleWindowModeButton:SetText("Lock") - MegaMacro_Frame:Show() - end - end, - IsOpen = function() - return IsOpen - end, - SaveMacro = function() - SaveMacro() - end, - OnSpecializationChanged = function(oldValue, newValue) - InitializeTabs() - SetMacroItems() - end -} - -function MegaMacro_OnIconUpdated(macroId, texture) - if IsOpen then - if SelectedMacro and SelectedMacro.Id == macroId then - RefreshSelectedMacroIcon() - end - - local macroItemsLength = #MacroItems - - for i=1, macroItemsLength do - if MacroItems[i].Id == macroId then - local _, _, buttonIcon = GetMacroButtonUI(i) - buttonIcon:SetTexture(texture) - end - end - - UpdateTooltipIfButtonIsHovered(macroId) - end -end - -function MegaMacro_Window_OnLoad() - -- Global, Class, ClassSpec, Character, CharacterSpec - PanelTemplates_SetNumTabs(MegaMacro_Frame, 6) - PanelTemplates_SetTab(MegaMacro_Frame, 1) - MegaMacroIconEvaluator.OnIconUpdated(function(macroId, texture) - MegaMacro_OnIconUpdated(macroId, texture) - end) -end - -function MegaMacro_Window_OnShow() - IsOpen = true - InitializeTabs() - InitializeIconListPanel() - UpdateSearchPlaceholder() - MegaMacro_FallbackTextureDescription:SetAlpha(0.6) -end - -function MegaMacro_Window_OnHide() - SaveMacro() - IsOpen = false - MegaMacro_PopupFrame:Hide() -end - -function MegaMacro_Window_OnDragStop() - local _, _, relativePoint, xOfs, yOfs = MegaMacro_Frame:GetPoint() - MegaMacroGlobalData.WindowInfo.RelativePoint = relativePoint - MegaMacroGlobalData.WindowInfo.X = xOfs - MegaMacroGlobalData.WindowInfo.Y = yOfs -end - -function MegaMacro_FrameTab_OnClick(self) - local tabIndex = self:GetID() - local scope = GetScopeFromTabIndex(tabIndex) - - if not HandleReceiveDrag(scope) then - PanelTemplates_SetTab(MegaMacro_Frame, tabIndex); - MegaMacro_ButtonScrollFrame:SetVerticalScroll(0) - MegaMacro_ConfigContainer:Hide() - - if tabIndex == 6 then - SelectedScope = 'config' - MegaMacro_FrameTab_ShowConfig() - return - end - - SelectedScope = scope - SelectedTabIndex = tabIndex - - InitializeMacroSlots() - SetMacroItems() - InitializeTabs() - - end -end - -function MegaMacro_FrameTab_ShowConfig() - -- Initialize Config Options - if not MegaMacro_ConfigContainer.initialized then - MecaMacro_GenerateConfig() - MegaMacro_ConfigContainer.initialized = true - end - - --Clear macro area - InitializeMacroSlots() - SelectMacro(nil) - - MegaMacro_ConfigContainer:Show() -end - -function MegaMacro_FrameTab_OnReceiveDrag(self) - local tabIndex = self:GetID() - local scope = GetScopeFromTabIndex(tabIndex) - HandleReceiveDrag(scope) -end - -function MegaMacro_ButtonContainer_OnLoad() - CreateMacroSlotFrames() -end - -function MegaMacro_ButtonContainer_OnShow() - InitializeMacroSlots() - SetMacroItems() -end - -function MegaMacro_ButtonContainer_OnReceiveDrag() - HandleReceiveDrag(SelectedScope) -end - -function MegaMacro_MacroButton_OnClick(self) - if not HandleReceiveDrag(SelectedScope) then - if self.Macro then - SelectMacro(self.Macro) - elseif self.IsNewButton then - NewMacro() - else - self:SetChecked(false) - end - end -end - -function MegaMacro_MacroButton_OnEnter(self) - if self.Macro then - ShowToolTipForMegaMacro(self.Macro.Id) - end -end - -function MegaMacro_MacroButton_OnLeave(self) - GameTooltip:Hide() -end - -function MegaMacro_MacroButton_OnDragStart(self) - if self.Macro then - PickupMegaMacro(self.Macro) - end -end - -function MegaMacro_MacroButton_OnDragStop(self) - for i=1, 5 do - local tab = _G["MegaMacro_FrameTab"..i] - tab:UnlockHighlight() - end -end - -function MegaMacro_MacroButton_OnReceiveDrag(self) - HandleReceiveDrag(SelectedScope) -end - -function MegaMacro_FrameSelectedMacroButton_OnEnter() - if SelectedMacro then - ShowToolTipForMegaMacro(SelectedMacro.Id) - end -end - -function MegaMacro_FrameSelectedMacroButton_OnLeave() - GameTooltip:Hide() -end - -function MegaMacro_FrameSelectedMacroButton_OnDragStart() - if SelectedMacro then - PickupMegaMacro(SelectedMacro) - end -end - -function MegaMacro_FrameTextButton_OnClick() - if SelectedMacro then - MegaMacro_FrameText:SetFocus(); - end -end - -function MegaMacro_TextBox_OnKeyDown(_, key) - if SelectedMacro then - if key == "S" and IsControlKeyDown() then - MegaMacro_SaveButton_OnClick() - MegaMacro_FrameText:SetFocus() - end - end -end - -function MegaMacro_TextBox_TextChanged(self) - local text = MegaMacro_FrameText:GetText() - if SelectedMacro ~= nil and SelectedMacro.Code ~= text then - MegaMacro_SaveButton:Enable() - MegaMacro_CancelButton:Enable() - else - MegaMacro_SaveButton:Disable() - MegaMacro_CancelButton:Disable() - end - - MegaMacro_FrameCharLimitText:SetFormattedText( - rendering.CharLimitMessageFormat, - MegaMacro_FrameText:GetNumLetters(), - MegaMacroCodeMaxLength) - -- Set color of text based on length - if MegaMacro_FrameText:GetNumLetters() > MegaMacroCodeMaxLength then - MegaMacro_FrameCharLimitText:SetTextColor(1, 0.267, 0.267) - elseif MegaMacro_FrameText:GetNumLetters() > MegaMacroCodeMaxLengthForNative then - MegaMacro_FrameCharLimitText:SetTextColor(1, 0.85, 0) - else - MegaMacro_FrameCharLimitText:SetTextColor(1,1,1) --0.2, 0.867, 1.0 - end - -- Limit. Custom hover tooltip - MegaMacro_FrameCharLimitText:SetScript("OnEnter", function(self) - GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT") - GameTooltip:AddLine("MegaMacro: Character Limit", 1, 1, 1) - GameTooltip:AddLine("\nMacros over 250 characters rely on custom macro logic. They will not have a dynamic icon if you are not using MegaMacros icon rendering. This option can be set in the config tab.", 1, 1, 1, true) - GameTooltip:Show() - end) - MegaMacro_FrameCharLimitText:SetScript("OnLeave", function(self) - GameTooltip:Hide() - end) - - ScrollingEdit_OnTextChanged(self, self:GetParent()) - ScrollingEdit_OnTextChanged(MegaMacro_FormattedFrameText, MegaMacro_FormattedFrameText:GetParent()) - MegaMacro_FormattedFrameText:SetText(MegaMacroParser.Parse(text)) -end - -function MegaMacro_CancelButton_OnClick() - PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON) - - if SelectedMacro ~= nil then - MegaMacro_FrameText:SetText(SelectedMacro.Code) - end - - MegaMacro_PopupFrame:Hide() - MegaMacro_FrameText:ClearFocus() - MegaMacro_CancelButton:Disable() -end - -function MegaMacro_SaveButton_OnClick() - PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON) - SaveMacro() - MegaMacro_PopupFrame:Hide() - MegaMacro_FrameText:ClearFocus() -end - -function MegaMacro_EditButton_OnClick() - if SelectedMacro ~= nil then - MegaMacro_PopupEditBox:SetText(SelectedMacro.DisplayName) - MegaMacro_FallbackTextureCheckBox:SetChecked(SelectedMacro.IsStaticTextureFallback) - MegaMacro_IconSearchBox:SetText("") - MegaMacro_DisplayNameLabel:SetText("(#"..SelectedMacro.Id .. ") Display Name") - MegaMacro_PopupFrame:Show() - PopupMode = PopupModes.Edit - end -end - -function MegaMacro_BlizMacro_Toggle() - -- re-initialize macros - MegaMacroEngine.SafeInitialize() - -- update macro char length limit - MegaMacro_InitialiseConfig() - MegaMacro_FrameCharLimitText:SetFormattedText( - rendering.CharLimitMessageFormat, - MegaMacro_FrameText:GetNumLetters(), - MegaMacroCodeMaxLength) -end - - - -function MegaMacro_EditOkButton_OnClick() - local enteredText = MegaMacro_PopupEditBox:GetText() - local isStaticTextureFallback = MegaMacro_FallbackTextureCheckBox:GetChecked() - - if PopupMode == PopupModes.Edit and SelectedMacro ~= nil then - local selectedMacro = SelectedMacro - MegaMacro.UpdateDetails(SelectedMacro, enteredText, SelectedIcon, isStaticTextureFallback) - SetMacroItems() - SelectMacro(selectedMacro) - elseif PopupMode == PopupModes.New then - local createdMacro = MegaMacro.Create(enteredText, SelectedScope, SelectedIcon, isStaticTextureFallback) - SetMacroItems() - SelectMacro(createdMacro) - MegaMacro_FrameText:SetFocus() - end - - MegaMacro_PopupFrame:Hide() -end - -function MegaMacro_EditOkButton_OnClick_Wrapper() - MegaMacro_EditOkButton_OnClick() -end - -function MegaMacro_EditCancelButton_OnClick() - if SelectedMacro then - SelectIcon(SelectedMacro.StaticTexture) - else - SelectMacro(MacroItems[1]) - end - - MegaMacro_PopupFrame:Hide() -end - -function MegaMacro_EditCancelButton_OnClick_Wrapper() - MegaMacro_EditCancelButton_OnClick() -end - -function MegaMacro_PopupFrame_OnUpdate() - local numMacroIcons = #IconList; - local macroPopupIcon, macroPopupButton; - local macroPopupOffset = FauxScrollFrame_GetOffset(MegaMacro_PopupScrollFrame); - local index; - - -- Icon list - for i=1, NUM_MACRO_ICONS_SHOWN do - macroPopupButton = _G["MegaMacro_PopupButton"..i]; - macroPopupIcon = _G["MegaMacro_PopupButton"..i.."Icon"]; - index = (macroPopupOffset * NUM_ICONS_PER_ROW) + i; - local iconListData = IconList[index] - - if index <= numMacroIcons and iconListData then - macroPopupIcon:SetTexture(iconListData.Icon); - macroPopupButton.SpellId = iconListData.SpellId - macroPopupButton:Show(); - else - macroPopupIcon:SetTexture(""); - macroPopupButton.SpellId = nil - macroPopupButton:Hide(); - end - - macroPopupButton:SetChecked(iconListData and SelectedIcon == iconListData.Icon) - end - - -- Scrollbar stuff - FauxScrollFrame_Update(MegaMacro_PopupScrollFrame, ceil(numMacroIcons / NUM_ICONS_PER_ROW) + 1, NUM_ICON_ROWS, MACRO_ICON_ROW_HEIGHT ); -end - -function MegaMacro_PopupButton_OnClick(self) - local buttonIcon = _G[self:GetName().."Icon"] - SelectIcon(buttonIcon:GetTexture()) -end - -function MegaMacro_PopupButton_OnEnter(self) - if self.SpellId then - GameTooltip:Hide() - GameTooltip_SetDefaultAnchor(GameTooltip, UIParent) - GameTooltip:SetSpellByID(self.SpellId) - GameTooltip:Show() - end -end - -function MegaMacro_PopupButton_OnLeave(self) - GameTooltip:Hide() -end - -function MegaMacro_ToggleWindowModeButton_OnClick() - if MegaMacroConfig_IsWindowDialog() then - local _, _, relativePoint, xOfs, yOfs = MegaMacro_Frame:GetPoint() - MegaMacroGlobalData.WindowInfo = { - IsDialog = false, - RelativePoint = relativePoint, - X = xOfs, - Y = yOfs - } - else - MegaMacroGlobalData.WindowInfo = nil - end - - MegaMacroWindowTogglingMode = true - HideUIPanel(MegaMacro_Frame) - MegaMacroWindow.Show() - MegaMacroWindowTogglingMode = false -end - -function MegaMacro_FallbackTextureCheckBox_OnClick() - RefreshSelectedMacroIcon() -end - -function MegaMacro_IconSearchBox_TextChanged() - UpdateSearchPlaceholder() - UpdateIconList() -end - -function MegaMacro_RegisterShiftClicks() - function shiftClickHookFunction(self) - local slot = SpellBook_GetSpellBookSlot(self); - if ( slot > MAX_SPELLS ) then - return - end - - if IsModifiedClick("CHATLINK") and MegaMacro_FrameText:HasFocus() then - local spellName, subSpellName = GetSpellBookItemName(slot, SpellBookFrame.bookType) - - if spellName and not IsPassiveSpell(slot, SpellBookFrame.bookType) then - if subSpellName and string.len(subSpellName) > 0 then - MegaMacro_FrameText:Insert(spellName.."("..subSpellName..")") - else - MegaMacro_FrameText:Insert(spellName) - end - end - end - end - - -- This is a kind of a hack, but it gets the shift click to work. We loop through all the spellbook buttons and hook their OnClick functions individually, since the generic SpellButton_OnModifiedClick was removed. - for i = 1, 120 do - local buttonName = "SpellButton" .. i - if _G[buttonName] ~= nil then - hooksecurefunc(_G[buttonName], "OnModifiedClick", shiftClickHookFunction) - end - end -end - --- Create the Config options -function MecaMacro_GenerateConfig() - -- Blizzard Action Bar Icons - MecaMacroConfig_GenerateCheckbox('UseBlizzardActionIcons', 'Blizzard Action Bar Icons', 'Disable Mega Macro Action Bar Engine. \nMacro Icons will be decided by blizzard instead of Mega Macro.', 0, -5, MegaMacroConfig.UseNativeActionBar, function(value) - MegaMacroConfig.UseNativeActionBar = value - end) - -- Make button "Uninstall" - MecaMacroConfig_GenerateButton('Uninstall', 'Uninstall', 'Removes MegaMacro information from Macros. Disable MegaMacro before reload to avoid re-importing them. \n\nNote: Current Class and Spec macros will become global. \nMacros for other Classes and Specs will be lost since Blizzard Macros do not have room for them.', 0, -30, function() - -- Warn and get confirmation - StaticPopupDialogs["MEGAMACRO_UNINSTALL_WARNING"] = { - text = "Are you sure you want to uninstall MegaMacro?", - button1 = "Yes", - button2 = "No", - OnAccept = function() - MegaMacroEngine.Uninstall() - print("MegaMacro uninstalled.") - end, - OnCancel = function() - print("Uninstallation cancelled.") - end, - timeout = 0, - whileDead = true, - hideOnEscape = true, - preferredIndex = 3, -- to avoid UI taint - } - - StaticPopup_Show("MEGAMACRO_UNINSTALL_WARNING") - end) -end - -function MecaMacroConfig_GenerateCheckbox(name, text, tooltip, x, y, checked, onClick) - local checkbox = CreateFrame("CheckButton", "MegaMacro_ConfigContainer_" .. name, MegaMacro_ConfigContainer, "ChatConfigCheckButtonTemplate") - checkbox:SetPoint("TOPLEFT", x, y) - checkbox:SetChecked(checked) - checkbox:SetScript("OnClick", function(self) - onClick(checkbox:GetChecked()) - end) - checkbox.tooltip = tooltip - - local textFrame = _G[checkbox:GetName() .. "Text"] - textFrame:SetFontObject("GameFontNormalSmall") - textFrame:SetText(text) - - return checkbox -end - -function MecaMacroConfig_GenerateButton(name, text, tooltip, x, y, onClick) - local button = CreateFrame("Button", "MegaMacro_ConfigContainer_" .. name, MegaMacro_ConfigContainer, "UIPanelButtonTemplate") - button:SetPoint("TOPLEFT", x, y) - -- set width - button:SetWidth(100) - button:SetScript("OnClick", function(self) - onClick() - end) - button.tooltipText = tooltip - - local textFrame = _G[button:GetName() .. "Text"] - textFrame:SetFontObject("GameFontNormalSmall") - textFrame:SetText(text) - - return button -end +local rendering = { + MacrosPerRow = 12, + CharLimitMessageFormat = "%s/%s Characters Used" +} + +local PopupModes = { + New = 0, + Edit = 1 +} + +local PlusTexture = 3192688--135769 + +local IsOpen = false +local SelectedScope = MegaMacroScopes.Global +local MacroItems = {} +local SelectedMacro = nil +local PopupMode = nil +local IconListInitialized = false +local SelectedIcon = nil +local IconList = {} +local SelectedTabIndex = 1 + +NUM_ICONS_PER_ROW = 10 +NUM_ICON_ROWS = 9 +NUM_MACRO_ICONS_SHOWN = NUM_ICONS_PER_ROW * NUM_ICON_ROWS +MACRO_ICON_ROW_HEIGHT = 36 +MegaMacroWindowTogglingMode = false + +local function GetScopeFromTabIndex(tabIndex) + if tabIndex == 1 then + return MegaMacroScopes.Global + elseif tabIndex == 2 then + return MegaMacroScopes.Class + elseif tabIndex == 3 then + return MegaMacroScopes.Specialization + elseif tabIndex == 4 then + return MegaMacroScopes.Character + elseif tabIndex == 5 then + return MegaMacroScopes.Inactive + end +end + +local function GetMacroButtonUI(index) + local buttonName = "MegaMacro_MacroButton" .. index + local btn = _G[buttonName] + if btn then + return btn, _G[buttonName].Name, _G[buttonName].Icon + end +end + +-- Creates the button frames for the macro slots +local function CreateMacroSlotFrames() + for i=1, HighestMaxMacroCount do + -- 12.0: If using CheckButton, BackdropTemplate isn't strictly required unless defining Backdrops, + -- but good practice if you style them later. + local button = CreateFrame("CheckButton", "MegaMacro_MacroButton" .. i, MegaMacro_ButtonContainer, "MegaMacro_ButtonTemplate") + button:SetID(i) + if i == 1 then + button:SetPoint("TOPLEFT", MegaMacro_ButtonContainer, "TOPLEFT", 6, -6) + elseif mod(i, rendering.MacrosPerRow) == 1 then + button:SetPoint("TOP", "MegaMacro_MacroButton"..(i-rendering.MacrosPerRow), "BOTTOM", 0, -10) + else + button:SetPoint("LEFT", "MegaMacro_MacroButton"..(i-1), "RIGHT", 13, 0) + end + end +end + +local function FixIconPanelPosition() + local button = MegaMacro_PopupButton1 + if button then + local point, relativeTo, relativePoint, x, y = button:GetPoint() + if relativeTo then + button:SetPoint(point, relativeTo:GetName(), relativePoint, x, y - 40) + end + end +end + +local function UpdateIconList() + local searchText = MegaMacro_IconSearchBox:GetText() + local items = MegaMacroIconNavigator.Search(searchText) + local itemCount = #items + + IconList = { { Name = "No Icon", Icon = MegaMacroTexture } } + + for i=1, itemCount do + IconList[i + 1] = items[i] + end +end + +local function InitializeIconListPanel() + if not IconListInitialized then + -- Ensure BuildIconArray is defined (usually in Blizzard's UI code or your own utils). + -- If it's a custom global, ensure it exists. + if BuildIconArray then + BuildIconArray(MegaMacro_PopupFrame, "MegaMacro_PopupButton", "MegaMacro_PopupButtonTemplate", NUM_ICONS_PER_ROW, NUM_ICON_ROWS) + FixIconPanelPosition() + IconListInitialized = true + end + end +end + +local function InitializeTabs() + local playerName = UnitName("player") + if MegaMacro_FrameTab2 then MegaMacro_FrameTab2:SetText(MegaMacroCachedClass or "Class") end + if MegaMacro_FrameTab4 then MegaMacro_FrameTab4:SetText(playerName) end + if MegaMacro_FrameTab5 then MegaMacro_FrameTab5:SetText("Inactive") end + if MegaMacro_FrameTab6 then MegaMacro_FrameTab6:SetText("Config") end + + if MegaMacro_FrameTab3 then + if not MegaMacroCachedSpecialization or MegaMacroCachedSpecialization == '' then + MegaMacro_FrameTab3:SetText("Locked") + MegaMacro_FrameTab3:Disable() + else + MegaMacro_FrameTab3:SetText(MegaMacroCachedSpecialization) + MegaMacro_FrameTab3:Enable() + end + end +end + +-- Shows and hides macro slot buttons based on the number of slots available in the scope +local function InitializeMacroSlots() + local scopeSlotCount = MegaMacro.GetSlotCount(SelectedScope) + + for i=1, scopeSlotCount do + local buttonFrame = _G["MegaMacro_MacroButton" .. i] + if buttonFrame == nil then break end + buttonFrame:Show() + end + + for i=scopeSlotCount+1, HighestMaxMacroCount do + local buttonFrame = _G["MegaMacro_MacroButton" .. i] + if buttonFrame == nil then break end + buttonFrame:Hide() + end +end + +local function SaveMacro() + local newCode = MegaMacro_FrameText:GetText() + if SelectedMacro ~= nil and SelectedMacro.Code ~= newCode then + MegaMacro.UpdateCode(SelectedMacro, newCode) + end + + MegaMacro_SaveButton:Disable() + MegaMacro_CancelButton:Disable() +end + +local function RefreshSelectedMacroIcon() + local displayedTexture = "" + + if SelectedMacro then + if SelectedIcon == MegaMacroTexture then + local data = MegaMacroIconEvaluator.GetCachedData(SelectedMacro.Id) + displayedTexture = data and data.Icon or MegaMacroTexture + else + local isStaticTextureFallback = MegaMacro_FallbackTextureCheckBox:GetChecked() + -- 12.0: select(4, ...) gets the icon from the return values + displayedTexture = select(4, MegaMacroIconEvaluator.ComputeMacroIcon(SelectedMacro, SelectedIcon, isStaticTextureFallback)) + end + end + + MegaMacro_FrameSelectedMacroButtonIcon:SetTexture(displayedTexture) +end + +local function SelectIcon(texture) + SelectedIcon = texture or MegaMacroTexture + RefreshSelectedMacroIcon() + + local i = 1 + while true do + local iconButton = _G["MegaMacro_PopupButton"..i] + local iconButtonIcon = _G["MegaMacro_PopupButton"..i.."Icon"] + + if not iconButton then + break + end + + iconButton:SetChecked(SelectedIcon == iconButtonIcon:GetTexture()) + i = i + 1 + end +end + +local function SelectMacro(macro) + SaveMacro() + SelectedMacro = nil + + if MegaMacro_PopupFrame then MegaMacro_PopupFrame:Hide() end + + MegaMacro_FrameSelectedMacroName:SetText("") + MegaMacro_FrameSelectedMacroButtonIcon:SetTexture("") + MegaMacro_FrameText:SetText("") + MegaMacro_EditButton:Disable(); + MegaMacro_DeleteButton:Disable(); + MegaMacro_SaveButton:Disable() + MegaMacro_CancelButton:Disable() + MegaMacro_FrameText:Disable() + + for i=1, HighestMaxMacroCount do + local buttonFrame, _, buttonIcon = GetMacroButtonUI(i) + if buttonFrame then + if macro and buttonFrame.Macro == macro then + buttonFrame:SetChecked(true) + SelectedMacro = macro + MegaMacro_FrameSelectedMacroName:SetText(macro.DisplayName) + MegaMacro_FrameSelectedMacroButtonIcon:SetTexture(buttonIcon:GetTexture()) + MegaMacro_FrameText:SetText(macro.Code) + MegaMacro_EditButton:Enable(); + MegaMacro_DeleteButton:Enable(); + MegaMacro_FrameText:Enable() + else + buttonFrame:SetChecked(false) + end + end + end + + SelectIcon(macro and macro.StaticTexture) +end + +local function SetMacroItems() + local items = MegaMacro.GetMacrosInScope(SelectedScope) + MacroItems = items or {} + + table.sort( + MacroItems, + function(left, right) + return (left.DisplayName or "") < (right.DisplayName or "") + end) + + local newMacroButtonCreated = false + + for i=1, HighestMaxMacroCount do + local buttonFrame, buttonName, buttonIcon = GetMacroButtonUI(i) + + if buttonFrame then + local macro = MacroItems[i] + + if macro then + buttonFrame.Macro = macro + buttonFrame.IsNewButton = false + buttonName:SetText(macro.DisplayName) + MegaMacroIconEvaluator.UpdateMacro(macro) + local data = MegaMacroIconEvaluator.GetCachedData(macro.Id) + buttonIcon:SetTexture(data and data.Icon or MegaMacroTexture) + buttonIcon:SetDesaturated(false) + buttonIcon:SetTexCoord(0, 1, 0, 1) + buttonIcon:SetAlpha(1) + + -- Check if actually synced to native + if not MegaMacroEngine.GetMacroIndexFromId(macro.Id) then + buttonIcon:SetAlpha(0.5) + buttonIcon:SetDesaturated(true) + end + elseif not newMacroButtonCreated and SelectedScope ~= MegaMacroScopes.Inactive then + buttonFrame.Macro = nil + buttonFrame.IsNewButton = true + buttonName:SetText("") + buttonIcon:SetTexture(PlusTexture) + buttonIcon:SetDesaturated(true) + buttonIcon:SetTexCoord(.08, .92, .08, .92) + buttonIcon:SetAlpha(0.5) + newMacroButtonCreated = true + else + buttonFrame.Macro = nil + buttonFrame.IsNewButton = false + buttonName:SetText("") + buttonIcon:SetTexture("") + buttonIcon:SetDesaturated(false) + buttonIcon:SetTexCoord(0, 1, 0, 1) + buttonIcon:SetAlpha(1) + end + end + end + + SelectMacro(MacroItems[1]) +end + +local function NewMacro() + SelectMacro(nil) + + local button = _G["MegaMacro_MacroButton"..(#MacroItems + 1)] + + if button then + button:SetChecked(true) + end + + PopupMode = PopupModes.New + MegaMacro_PopupEditBox:SetText("") + MegaMacro_FallbackTextureCheckBox:SetChecked(true) + MegaMacro_IconSearchBox:SetText("") + MegaMacro_PopupFrame:Show() +end + +local function DeleteMegaMacro() + if SelectedMacro ~= nil then + MegaMacro.Delete(SelectedMacro) + SetMacroItems() + end +end + +local function UpdateTooltipIfButtonIsHovered(updatedMacroId) + -- 12.0: GetMouseFoci is the standard, GetMouseFocus is deprecated + local mouseFocus = GetMouseFoci and GetMouseFoci()[1] or (GetMouseFocus and GetMouseFocus()) + + if mouseFocus then + local focusFrame = mouseFocus:GetName() + + if focusFrame then + if string.find(focusFrame, "^MegaMacro_MacroButton%d+$") then + local macro = _G[focusFrame].Macro + + if macro and macro.Id == updatedMacroId then + ShowToolTipForMegaMacro(macro.Id) + end + elseif focusFrame == "MegaMacro_FrameSelectedMacroButton" and SelectedMacro and SelectedMacro.Id == updatedMacroId then + ShowToolTipForMegaMacro(SelectedMacro.Id) + end + end + end +end + +local function PickupMegaMacro(macro) + local macroIndex = MegaMacroEngine.GetMacroIndexFromId(macro.Id) + -- highlight all tabs to indicate it can be moved to a tab + for i=1, 5 do + if i ~= SelectedTabIndex then + local tab = _G["MegaMacro_FrameTab"..i] + if tab then tab:LockHighlight() end + end + end + + if macroIndex then + PickupMacro(macroIndex) + end +end + +local function HandleReceiveDrag(targetScope) + local type, macroIndex = GetCursorInfo() + + if type == "macro" then + local macroId = MegaMacroEngine.GetMacroIdFromIndex(macroIndex) + + if macroId then + local macro = MegaMacro.GetById(macroId) + ClearCursor() + + if IsControlKeyDown() then + local newDisplayName = macro.Scope == targetScope and macro.DisplayName.." copy" or macro.DisplayName + local newMacro = MegaMacro.Create(newDisplayName, targetScope, macro.StaticTexture) + MegaMacro.UpdateCode(newMacro, macro.Code) + + if targetScope == SelectedScope then + SetMacroItems() + SelectMacro(newMacro) + end + elseif targetScope == macro.Scope then + -- do nothing + else + local newMacro = MegaMacro.Move(macro, targetScope) + + if targetScope == SelectedScope then + SetMacroItems() + SelectMacro(newMacro) + elseif macro.Scope == SelectedScope then + SetMacroItems() + end + end + else + ClearCursor() + -- 12.0 C_Macro usage for importing + local name, _, body = C_Macro.GetMacroInfo(macroIndex) + local newMacro = MegaMacro.Create(name, targetScope, MegaMacroTexture) + + if newMacro then + MegaMacro.UpdateCode(newMacro, body) + end + + SetMacroItems() + SelectMacro(newMacro) + + if not InCombatLockdown() then + local newMacroIndex = MegaMacroEngine.GetMacroIndexFromId(newMacro.Id) + if newMacroIndex then + for i=1, 120 do + local actionButtonType, actionButtonArg = GetActionInfo(i) + if actionButtonType == "macro" and actionButtonArg == macroIndex then + C_Macro.PickupMacro(newMacroIndex) + PlaceAction(i) + ClearCursor() + end + end + end + + C_Macro.DeleteMacro(macroIndex) + end + end + end + + return type ~= nil +end + +local function UpdateSearchPlaceholder() + if MegaMacro_IconSearchBox:GetText() == "" then + MegaMacro_IconSearchPlaceholder:SetAlpha(0.4) + else + MegaMacro_IconSearchPlaceholder:SetAlpha(0.0) + end +end + +StaticPopupDialogs["CONFIRM_DELETE_SELECTED_MEGA_MACRO"] = { + text = CONFIRM_DELETE_MACRO, + button1 = OKAY, + button2 = CANCEL, + OnAccept = DeleteMegaMacro, + timeout = 0, + whileDead = 1, + showAlert = 1 +} + +MegaMacroWindow = { + Show = function() + if MegaMacroConfig_IsWindowDialog() then + MegaMacro_Frame:SetMovable(false) + MegaMacro_ToggleWindowModeButton:SetText("Unlock") + ShowUIPanel(MegaMacro_Frame); + else + local relativePoint, x, y = MegaMacroConfig_GetWindowPosition() + MegaMacro_Frame:SetMovable(true) + MegaMacro_Frame:SetSize(640, 524) + MegaMacro_Frame:ClearAllPoints() + if relativePoint then + MegaMacro_Frame:SetPoint(relativePoint, x, y) + else + MegaMacro_Frame:SetPoint("CENTER") + end + MegaMacro_ToggleWindowModeButton:SetText("Lock") + MegaMacro_Frame:Show() + end + end, + IsOpen = function() + return IsOpen + end, + SaveMacro = function() + SaveMacro() + end, + OnSpecializationChanged = function(oldValue, newValue) + InitializeTabs() + SetMacroItems() + end +} + +function MegaMacro_OnIconUpdated(macroId, texture) + if IsOpen then + if SelectedMacro and SelectedMacro.Id == macroId then + RefreshSelectedMacroIcon() + end + + local macroItemsLength = #MacroItems + + for i=1, macroItemsLength do + if MacroItems[i].Id == macroId then + local _, _, buttonIcon = GetMacroButtonUI(i) + if buttonIcon then + buttonIcon:SetTexture(texture) + end + end + end + + UpdateTooltipIfButtonIsHovered(macroId) + end +end + +function MegaMacro_Window_OnLoad() + -- Global, Class, ClassSpec, Character, CharacterSpec + PanelTemplates_SetNumTabs(MegaMacro_Frame, 6) + PanelTemplates_SetTab(MegaMacro_Frame, 1) + MegaMacroIconEvaluator.OnIconUpdated(function(macroId, texture) + MegaMacro_OnIconUpdated(macroId, texture) + end) +end + +function MegaMacro_Window_OnShow() + IsOpen = true + InitializeTabs() + InitializeIconListPanel() + UpdateSearchPlaceholder() + MegaMacro_FallbackTextureDescription:SetAlpha(0.6) +end + +function MegaMacro_Window_OnHide() + SaveMacro() + IsOpen = false + MegaMacro_PopupFrame:Hide() +end + +function MegaMacro_Window_OnDragStop() + local _, _, relativePoint, xOfs, yOfs = MegaMacro_Frame:GetPoint() + if MegaMacroGlobalData and MegaMacroGlobalData.WindowInfo then + MegaMacroGlobalData.WindowInfo.RelativePoint = relativePoint + MegaMacroGlobalData.WindowInfo.X = xOfs + MegaMacroGlobalData.WindowInfo.Y = yOfs + end +end + +function MegaMacro_FrameTab_OnClick(self) + local tabIndex = self:GetID() + local scope = GetScopeFromTabIndex(tabIndex) + + if not HandleReceiveDrag(scope) then + PanelTemplates_SetTab(MegaMacro_Frame, tabIndex); + MegaMacro_ButtonScrollFrame:SetVerticalScroll(0) + MegaMacro_ConfigContainer:Hide() + + if tabIndex == 6 then + SelectedScope = 'config' + MegaMacro_FrameTab_ShowConfig() + return + end + + SelectedScope = scope + SelectedTabIndex = tabIndex + + InitializeMacroSlots() + SetMacroItems() + InitializeTabs() + + end +end + +function MegaMacro_FrameTab_ShowConfig() + -- Initialize Config Options + if not MegaMacro_ConfigContainer.initialized then + MecaMacro_GenerateConfig() + MegaMacro_ConfigContainer.initialized = true + end + + --Clear macro area + InitializeMacroSlots() + SelectMacro(nil) + + MegaMacro_ConfigContainer:Show() +end + +function MegaMacro_FrameTab_OnReceiveDrag(self) + local tabIndex = self:GetID() + local scope = GetScopeFromTabIndex(tabIndex) + HandleReceiveDrag(scope) +end + +function MegaMacro_ButtonContainer_OnLoad() + CreateMacroSlotFrames() +end + +function MegaMacro_ButtonContainer_OnShow() + InitializeMacroSlots() + SetMacroItems() +end + +function MegaMacro_ButtonContainer_OnReceiveDrag() + HandleReceiveDrag(SelectedScope) +end + +function MegaMacro_MacroButton_OnClick(self) + if not HandleReceiveDrag(SelectedScope) then + if self.Macro then + SelectMacro(self.Macro) + elseif self.IsNewButton then + NewMacro() + else + self:SetChecked(false) + end + end +end + +function MegaMacro_MacroButton_OnEnter(self) + if self.Macro then + ShowToolTipForMegaMacro(self.Macro.Id) + end +end + +function MegaMacro_MacroButton_OnLeave(self) + GameTooltip:Hide() +end + +function MegaMacro_MacroButton_OnDragStart(self) + if self.Macro then + PickupMegaMacro(self.Macro) + end +end + +function MegaMacro_MacroButton_OnDragStop(self) + for i=1, 5 do + local tab = _G["MegaMacro_FrameTab"..i] + if tab then tab:UnlockHighlight() end + end +end + +function MegaMacro_MacroButton_OnReceiveDrag(self) + HandleReceiveDrag(SelectedScope) +end + +function MegaMacro_FrameSelectedMacroButton_OnEnter() + if SelectedMacro then + ShowToolTipForMegaMacro(SelectedMacro.Id) + end +end + +function MegaMacro_FrameSelectedMacroButton_OnLeave() + GameTooltip:Hide() +end + +function MegaMacro_FrameSelectedMacroButton_OnDragStart() + if SelectedMacro then + PickupMegaMacro(SelectedMacro) + end +end + +function MegaMacro_FrameTextButton_OnClick() + if SelectedMacro then + MegaMacro_FrameText:SetFocus(); + end +end + +function MegaMacro_TextBox_OnKeyDown(_, key) + if SelectedMacro then + if key == "S" and IsControlKeyDown() then + MegaMacro_SaveButton_OnClick() + MegaMacro_FrameText:SetFocus() + end + end +end + +function MegaMacro_TextBox_TextChanged(self) + local text = MegaMacro_FrameText:GetText() + if SelectedMacro ~= nil and SelectedMacro.Code ~= text then + MegaMacro_SaveButton:Enable() + MegaMacro_CancelButton:Enable() + else + MegaMacro_SaveButton:Disable() + MegaMacro_CancelButton:Disable() + end + + MegaMacro_FrameCharLimitText:SetFormattedText( + rendering.CharLimitMessageFormat, + MegaMacro_FrameText:GetNumLetters(), + MegaMacroCodeMaxLength) + -- Set color of text based on length + if MegaMacro_FrameText:GetNumLetters() > MegaMacroCodeMaxLength then + MegaMacro_FrameCharLimitText:SetTextColor(1, 0.267, 0.267) + elseif MegaMacro_FrameText:GetNumLetters() > MegaMacroCodeMaxLengthForNative then + MegaMacro_FrameCharLimitText:SetTextColor(1, 0.85, 0) + else + MegaMacro_FrameCharLimitText:SetTextColor(1,1,1) --0.2, 0.867, 1.0 + end + -- Limit. Custom hover tooltip + MegaMacro_FrameCharLimitText:SetScript("OnEnter", function(self) + -- 12.0: SetOwner required + GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT") + GameTooltip:AddLine("MegaMacro: Character Limit", 1, 1, 1) + GameTooltip:AddLine("\nMacros over 250 characters rely on custom macro logic. They will not have a dynamic icon if you are not using MegaMacros icon rendering. This option can be set in the config tab.", 1, 1, 1, true) + GameTooltip:Show() + end) + MegaMacro_FrameCharLimitText:SetScript("OnLeave", function(self) + GameTooltip:Hide() + end) + + ScrollingEdit_OnTextChanged(self, self:GetParent()) + ScrollingEdit_OnTextChanged(MegaMacro_FormattedFrameText, MegaMacro_FormattedFrameText:GetParent()) + MegaMacro_FormattedFrameText:SetText(MegaMacroParser.Parse(text)) +end + +function MegaMacro_CancelButton_OnClick() + PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON) + + if SelectedMacro ~= nil then + MegaMacro_FrameText:SetText(SelectedMacro.Code) + end + + MegaMacro_PopupFrame:Hide() + MegaMacro_FrameText:ClearFocus() + MegaMacro_CancelButton:Disable() +end + +function MegaMacro_SaveButton_OnClick() + PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON) + SaveMacro() + MegaMacro_PopupFrame:Hide() + MegaMacro_FrameText:ClearFocus() +end + +function MegaMacro_EditButton_OnClick() + if SelectedMacro ~= nil then + MegaMacro_PopupEditBox:SetText(SelectedMacro.DisplayName) + MegaMacro_FallbackTextureCheckBox:SetChecked(SelectedMacro.IsStaticTextureFallback) + MegaMacro_IconSearchBox:SetText("") + MegaMacro_DisplayNameLabel:SetText("(#"..SelectedMacro.Id .. ") Display Name") + MegaMacro_PopupFrame:Show() + PopupMode = PopupModes.Edit + end +end + +function MegaMacro_BlizMacro_Toggle() + -- re-initialize macros + MegaMacroEngine.SafeInitialize() + -- update macro char length limit + MegaMacro_InitialiseConfig() + MegaMacro_FrameCharLimitText:SetFormattedText( + rendering.CharLimitMessageFormat, + MegaMacro_FrameText:GetNumLetters(), + MegaMacroCodeMaxLength) +end + + + +function MegaMacro_EditOkButton_OnClick() + local enteredText = MegaMacro_PopupEditBox:GetText() + local isStaticTextureFallback = MegaMacro_FallbackTextureCheckBox:GetChecked() + + if PopupMode == PopupModes.Edit and SelectedMacro ~= nil then + local selectedMacro = SelectedMacro + MegaMacro.UpdateDetails(SelectedMacro, enteredText, SelectedIcon, isStaticTextureFallback) + SetMacroItems() + SelectMacro(selectedMacro) + elseif PopupMode == PopupModes.New then + local createdMacro = MegaMacro.Create(enteredText, SelectedScope, SelectedIcon, isStaticTextureFallback) + SetMacroItems() + SelectMacro(createdMacro) + MegaMacro_FrameText:SetFocus() + end + + MegaMacro_PopupFrame:Hide() +end + +function MegaMacro_EditOkButton_OnClick_Wrapper() + MegaMacro_EditOkButton_OnClick() +end + +function MegaMacro_EditCancelButton_OnClick() + if SelectedMacro then + SelectIcon(SelectedMacro.StaticTexture) + else + SelectMacro(MacroItems[1]) + end + + MegaMacro_PopupFrame:Hide() +end + +function MegaMacro_EditCancelButton_OnClick_Wrapper() + MegaMacro_EditCancelButton_OnClick() +end + +function MegaMacro_PopupFrame_OnUpdate() + local numMacroIcons = #IconList; + local macroPopupIcon, macroPopupButton; + local macroPopupOffset = FauxScrollFrame_GetOffset(MegaMacro_PopupScrollFrame); + local index; + + -- Icon list + for i=1, NUM_MACRO_ICONS_SHOWN do + macroPopupButton = _G["MegaMacro_PopupButton"..i]; + macroPopupIcon = _G["MegaMacro_PopupButton"..i.."Icon"]; + index = (macroPopupOffset * NUM_ICONS_PER_ROW) + i; + local iconListData = IconList[index] + + if index <= numMacroIcons and iconListData then + macroPopupIcon:SetTexture(iconListData.Icon); + macroPopupButton.SpellId = iconListData.SpellId + macroPopupButton:Show(); + else + macroPopupIcon:SetTexture(""); + macroPopupButton.SpellId = nil + macroPopupButton:Hide(); + end + + macroPopupButton:SetChecked(iconListData and SelectedIcon == iconListData.Icon) + end + + -- Scrollbar stuff + FauxScrollFrame_Update(MegaMacro_PopupScrollFrame, ceil(numMacroIcons / NUM_ICONS_PER_ROW) + 1, NUM_ICON_ROWS, MACRO_ICON_ROW_HEIGHT ); +end + +function MegaMacro_PopupButton_OnClick(self) + local buttonIcon = _G[self:GetName().."Icon"] + SelectIcon(buttonIcon:GetTexture()) +end + +function MegaMacro_PopupButton_OnEnter(self) + if self.SpellId then + GameTooltip:Hide() + -- 12.0: SetOwner Required + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:SetSpellByID(self.SpellId) + GameTooltip:Show() + end +end + +function MegaMacro_PopupButton_OnLeave(self) + GameTooltip:Hide() +end + +function MegaMacro_ToggleWindowModeButton_OnClick() + if MegaMacroConfig_IsWindowDialog() then + local _, _, relativePoint, xOfs, yOfs = MegaMacro_Frame:GetPoint() + MegaMacroGlobalData.WindowInfo = { + IsDialog = false, + RelativePoint = relativePoint, + X = xOfs, + Y = yOfs + } + else + MegaMacroGlobalData.WindowInfo = nil + end + + MegaMacroWindowTogglingMode = true + HideUIPanel(MegaMacro_Frame) + MegaMacroWindow.Show() + MegaMacroWindowTogglingMode = false +end + +function MegaMacro_FallbackTextureCheckBox_OnClick() + RefreshSelectedMacroIcon() +end + +function MegaMacro_IconSearchBox_TextChanged() + UpdateSearchPlaceholder() + UpdateIconList() +end + +function MegaMacro_RegisterShiftClicks() + -- 12.0 Shift-Click handler (Modern) + local function shiftClickHookFunction(self) + -- With 12.0, SpellBook slots are complex. We check if the button has a specific ID or SpellID attached. + -- This logic depends on the new SpellBook UI structure. + -- For safety, we only insert if ChatEdit is open. + + if IsModifiedClick("CHATLINK") and MegaMacro_FrameText:HasFocus() then + -- Note: In 12.0, getting spell info from a button is often simpler: + local spellID = self:GetID() -- or self.spellID depending on frame type + if not spellID or spellID == 0 then + -- fallback for slot-based buttons + -- local slot = SpellBook_GetSpellBookSlot(self); -- REMOVED in 12.0 + end + + -- If we can't reliably get the spellID from the button (due to new UI virtualization), + -- we rely on the standard ChatEdit_InsertLink hook which we can't easily override here without tainting. + + -- Alternative: We don't hook the buttons, we hook ChatEdit_InsertLink + end + end + + -- 12.0 Hook Strategy: Hook the global Chat insertion logic. + hooksecurefunc("ChatEdit_InsertLink", function(link) + if MegaMacro_FrameText and MegaMacro_FrameText:HasFocus() then + -- If our frame is focused, insert the link text there instead of the chat window + -- Strip the link formatting to just get the name? Or keep the link? + -- Macros usually want the name: [Spell Name] -> Spell Name + local name = link:match("%[(.-)%]") + if name then + MegaMacro_FrameText:Insert(name) + -- We might need to prevent it from pasting into the chat frame if possible, + -- but InsertLink is a notification hook. + end + end + end) +end + +-- Create the Config options +function MecaMacro_GenerateConfig() + -- Blizzard Action Bar Icons + MecaMacroConfig_GenerateCheckbox('UseBlizzardActionIcons', 'Blizzard Action Bar Icons', 'Disable Mega Macro Action Bar Engine. \nMacro Icons will be decided by blizzard instead of Mega Macro.', 0, -5, MegaMacroConfig.UseNativeActionBar, function(value) + MegaMacroConfig.UseNativeActionBar = value + end) + -- Make button "Uninstall" + MecaMacroConfig_GenerateButton('Uninstall', 'Uninstall', 'Removes MegaMacro information from Macros. Disable MegaMacro before reload to avoid re-importing them. \n\nNote: Current Class and Spec macros will become global. \nMacros for other Classes and Specs will be lost since Blizzard Macros do not have room for them.', 0, -30, function() + -- Warn and get confirmation + StaticPopupDialogs["MEGAMACRO_UNINSTALL_WARNING"] = { + text = "Are you sure you want to uninstall MegaMacro?", + button1 = "Yes", + button2 = "No", + OnAccept = function() + MegaMacroEngine.Uninstall() + print("MegaMacro uninstalled.") + end, + OnCancel = function() + print("Uninstallation cancelled.") + end, + timeout = 0, + whileDead = true, + hideOnEscape = true, + preferredIndex = 3, -- to avoid UI taint + } + + StaticPopup_Show("MEGAMACRO_UNINSTALL_WARNING") + end) +end + +function MecaMacroConfig_GenerateCheckbox(name, text, tooltip, x, y, checked, onClick) + -- 12.0: Inherit BackdropTemplate for any frame that might need borders, even CheckButtons + local checkbox = CreateFrame("CheckButton", "MegaMacro_ConfigContainer_" .. name, MegaMacro_ConfigContainer, "ChatConfigCheckButtonTemplate, BackdropTemplate") + checkbox:SetPoint("TOPLEFT", x, y) + checkbox:SetChecked(checked) + checkbox:SetScript("OnClick", function(self) + onClick(checkbox:GetChecked()) + end) + checkbox.tooltip = tooltip + + local textFrame = _G[checkbox:GetName() .. "Text"] + if textFrame then + textFrame:SetFontObject("GameFontNormalSmall") + textFrame:SetText(text) + end + + return checkbox +end + +function MecaMacroConfig_GenerateButton(name, text, tooltip, x, y, onClick) + local button = CreateFrame("Button", "MegaMacro_ConfigContainer_" .. name, MegaMacro_ConfigContainer, "UIPanelButtonTemplate, BackdropTemplate") + button:SetPoint("TOPLEFT", x, y) + -- set width + button:SetWidth(100) + button:SetScript("OnClick", function(self) + onClick() + end) + button.tooltipText = tooltip + + local textFrame = _G[button:GetName() .. "Text"] + if textFrame then + textFrame:SetFontObject("GameFontNormalSmall") + textFrame:SetText(text) + end + + return button +end \ No newline at end of file diff --git a/src/windows/mega-macro.window.xml b/src/windows/mega-macro.window.xml index 3badbc7..03360f1 100644 --- a/src/windows/mega-macro.window.xml +++ b/src/windows/mega-macro.window.xml @@ -1,613 +1,604 @@ - -