diff --git a/CHANGELOG.md b/CHANGELOG.md index 999c2c3..ccf972d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ Changes in this fork (OEVG/ShaguTweaks-ClassicAPI), newest first. Each entry is This fork is based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at commit `4391564` (2026-09-08). Earlier changes are in that repository's history. +## 3.7 — 2026-11-02 — Extras modules built in + +### Added + +- The modules of Dusk's ShaguTweaks Extras ClassicAPI are now part of this addon, so the separate addon is no longer needed: + - **Action Bar:** Dragonflight Gryphons, Floating Actionbar, Hide Macro Text, Mouseover Right, Mouseover Right 2, Reagent Counter, Center Vertical Actionbar, Key-Down Casting, Alt Self-Cast, Show Bags and Show Micro Menu. + - **Chat & Social:** Center Text Input Box, Block NPC Spam, Enable Text Shadow, Chat Timestamps and World Chat Hider. + - **Loot:** Loot Quality. + - **Macro** (new category): Macro Icons and Macro Tweaks. + - **Merchant** (new category): Buy Em All. + - **Minimap & World Map:** MiniMap Framerate & Latency and Reveal World Map. + - **Tooltip & Items:** Cursor Tooltip, Hide Combat Tooltip and Metric Range. + - **Unit Frames:** Unit Frame Abbreviated Names. +- They keep their names, so saved on/off choices carry over. If the old Extras addon is still enabled, a chat message asks you to disable it. + +### Not merged + +- **Raid Frames** (all its modules). +- **Bag Item Click** and **Bag Search Bar:** Bagshui already has a search box and sends items to trade, the auction house and mail on right-click. +- **Auction Alt-Buy:** aux already buys out or cancels with Alt+click. + +### Fixed + +- **XP Bar Text:** the numbers are drawn with a shadow instead of an outline, so neighbouring digits no longer run into each other. + ## 3.6 — 2026-11-02 — Map window, Transports button and Loot History ### Added diff --git a/Docs/ASSET_PROVENANCE.md b/Docs/ASSET_PROVENANCE.md index 11b142b..607f5a6 100644 --- a/Docs/ASSET_PROVENANCE.md +++ b/Docs/ASSET_PROVENANCE.md @@ -33,6 +33,10 @@ hashes. | `img/UI-TargetingFrame.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/UI-TargetingFrame.tga` | `c3c56c28ff81f99325a8aa5a8d8c5cd4a9c56a95` | | `img/circleparty.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleparty.tga` | `9634f9ffb07ae4fb047223f83c299a8ba1906dff` | | `img/circleraid.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleraid.tga` | `c8a5c3e4c572ebd4af426d878bdc8db21d758691` | +| `img/df-gryphon-beta.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-gryphon-beta.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `bb4262fe7bf811fa7fff958f7f29269b69038280` | +| `img/df-gryphon.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-gryphon.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `6c7e965613222b0313dc1d56d9aa81414a828eef` | +| `img/df-wyvern.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-wyvern.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `4414fedcec732ea3096c30598ba7ada25a6af585` | +| `img/xpbar.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/xpbar.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `de80c16875cd7d9fa96baf99bd6ae2e1514595f7` | ## Screenshot manifest diff --git a/LICENSES/README.md b/LICENSES/README.md index 673695d..c62b932 100644 --- a/LICENSES/README.md +++ b/LICENSES/README.md @@ -11,6 +11,9 @@ apply every listed license to the ShaguTweaks-ClassicAPI repository as a whole. - `ShaguTweaks-mods-MIT.txt` — MIT license for the adapted TokensWorth/ShaguTweaks-mods material documented in `THIRD_PARTY_NOTICES.md`. +- `ShaguTweaks-extras-MIT.txt` — MIT license of ShaguTweaks-extras, for the + modules merged from ShaguTweaks-Extras-ClassicAPI documented in + `THIRD_PARTY_NOTICES.md`. - `pfUI-MIT.txt` — MIT license for the Swing Timer and Wand Timer ported from brues-code/pfUI, documented in `THIRD_PARTY_NOTICES.md`. - `ClassicAPI-GPL-3.0.txt` — verbatim license document from the external diff --git a/LICENSES/ShaguTweaks-extras-MIT.txt b/LICENSES/ShaguTweaks-extras-MIT.txt new file mode 100755 index 0000000..475b557 --- /dev/null +++ b/LICENSES/ShaguTweaks-extras-MIT.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Eric Mauser (Shagu) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 31d8988..dc8a567 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,15 @@ This is an English-only customization of [Dusk/ShaguTweaks-ClassicAPI](https://o > This fork requires ClassicAPI and targets **Turtle WoW-like server/client environments**. -**Version 3.6.** What changed in each update: see the [changelog](CHANGELOG.md). +**Version 3.7.** What changed in each update: see the [changelog](CHANGELOG.md). + +> **Upgrading to 3.7:** the modules of the separate *ShaguTweaks Extras ClassicAPI* addon are now built into ShaguTweaks. **Disable or delete `ShaguTweaks-extras`** after updating, otherwise its older copies replace the built-in ones. Your on/off settings carry over. Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy were not merged. See [Built-in Extras modules](#built-in-extras-modules). ## 🔌 Requirements - **[ClassicAPI](https://github.com/brues-code/ClassicAPI) — required.** Install both the DLL and its bundled `!!!ClassicAPI` addon, and restart the client after a DLL update. v1.18.10 or newer is recommended (it includes a crash fix for addons that restyle nameplates). The integration was last reviewed against v1.18.13; see [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md). - **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional / recommended.** Only used as an additional fallback for some cast/GUID information. -- **[ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) — optional / recommended.** Dusk's companion addon with some extra modules and features. It loads alongside this one and adds its own option categories. +- **[ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) — no longer needed.** Its modules are built in since 3.7 (see **Built-in Extras modules** below). Disable or remove it; while it is enabled, its copies replace the built-in ones. ClassicAPI is the main API layer. @@ -59,6 +61,12 @@ Settings: **Esc → Advanced Options**. - **WorldMap Window:** the map keeps the size you pick with Ctrl+Mousewheel (saved per character), and the title bar drags the window as well. With [OctoBus](https://github.com/Phridgey/OctoBus) installed, a **Transports** button left of the Continent dropdown shows or hides its transport timers. - **Loot History:** `/loothistory` lists recent group-loot rolls with the winner of each item; click an item for everyone's need, greed or pass and their rolls. Ported from [brues-code/pfUI](https://github.com/brues-code/pfUI). Requires ClassicAPI. +**Built-in Extras modules** + +- The modules of Dusk's [ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) are part of this addon, each under the matching category: Action Bar, Chat & Social, Loot, Minimap & World Map, Tooltip & Items and Unit Frames, plus two new categories, **Macro** and **Merchant**. +- They keep their names, so your saved on/off choices carry over. +- Left out: **Raid Frames**; **Bag Item Click** and **Bag Search Bar**, which [Bagshui](https://octowow.st/git/OEVG/Bagshui) already covers; and **Auction Alt-Buy**, which [aux](https://octowow.st/git/Dusk/aux-addon-ClassicAPI) already does (Alt+click a row to buy out or cancel). + **Performance and stability** - Reduced per-frame work, tooltip scans and repeated UI updates. @@ -97,6 +105,13 @@ Modules that use ClassicAPI directly: - Enemy Castbars - Tooltip Details - WorldMap Window +- Buy Em All +- Key-Down Casting and Alt Self-Cast +- Macro Icons and Macro Tweaks +- MiniMap Framerate & Latency +- Reagent Counter +- Reveal World Map +- Show Bags and Show Micro Menu Other modules can also benefit indirectly from ClassicAPI through shared ShaguTweaks libraries and caches. Per-version notes are in [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md). @@ -116,6 +131,17 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - Wand Timer *(off)* - No Toggle Auto-Attack - Reduced Actionbar Size *(off)* +- Show Bags *(off)* +- Show Micro Menu *(off)* +- Key-Down Casting +- Alt Self-Cast *(off)* +- Reagent Counter *(off)* +- Hide Macro Text *(off)* +- Mouseover Right *(off)* +- Mouseover Right 2 *(off)* +- Center Vertical Actionbar *(off)* +- Floating Actionbar *(off)* +- Dragonflight Gryphons *(off)* ### Unit Frames @@ -125,6 +151,7 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - Unit Frame Class Portraits *(off)* - Unit Frame Energy & Mana Tick *(off)* - Hide Pet Unit Frame *(off)* +- Unit Frame Abbreviated Names *(off)* - Movable Unit Frames - Real Health Numbers *(off)* - White Unit Frame Numbers *(off)* @@ -146,6 +173,9 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - Sell Junk - Tooltip Details - Vendor Values +- Metric Range +- Cursor Tooltip *(off)* +- Hide Combat Tooltip *(off)* ### Chat & Social @@ -154,6 +184,11 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - Chat Levels *(off)* - Chat Spam Filter *(off)* - Social Colors +- Chat Timestamps *(off)* +- Enable Text Shadow *(off)* +- Center Text Input Box *(off)* +- Block NPC Spam *(off)* +- World Chat Hider *(off)* ### Minimap & World Map @@ -166,6 +201,8 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - WorldMap Class Colors - WorldMap Coordinates - WorldMap Window +- Reveal World Map +- MiniMap Framerate & Latency *(off)* ### Loot @@ -173,6 +210,16 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - Improved Roll Frames *(off)* - Loot Cursor - Loot History (`/loothistory`) +- Loot Quality *(off)* + +### Macro + +- Macro Icons +- Macro Tweaks + +### Merchant + +- Buy Em All ### Interface @@ -193,6 +240,7 @@ The following original ShaguTweaks modules are not included as standalone module - **Chat Links** → integrated into **Chat Tweaks** - **Movable Unit Frames Extended** → integrated into **Movable Unit Frames** +- **ShaguTweaks Extras ClassicAPI** → its modules are built in, except Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy ## 📜 Project identity & licensing @@ -225,8 +273,10 @@ Special thanks to **Jesper_92** for reporting issues that helped improve the add Additional code and ideas from **pfUI** and **zUI**. +Extras modules from **Dusk-92**'s ShaguTweaks-Extras-ClassicAPI, a fork of **paokkerkir**'s ShaguTweaks-extras (MIT, Shagu). + Swing Timer, Wand Timer and Loot History ported from **brues-code/pfUI** (MIT, Shagu and contributors). -Extended Movable Unit Frames support adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`. +Extended Movable Unit Frames support and several of the Extras modules adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`. Released under the original **MIT License**. diff --git a/ShaguTweaks.toc b/ShaguTweaks.toc index 890d374..2072be7 100644 --- a/ShaguTweaks.toc +++ b/ShaguTweaks.toc @@ -2,7 +2,7 @@ ## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI] ## Author: Shagu ## Notes: A collection of tweaks and enhancements to the Default UI -## Version: 3.6 +## Version: 3.7 ## Dependencies: !!!ClassicAPI ## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache ## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache @@ -86,6 +86,43 @@ mods\swing-timer.lua mods\smaller-errors-frame.lua mods\skip-gossip.lua +# modules merged from ShaguTweaks Extras ClassicAPI (Dusk) +# action bar +mods\actionbar-df-gryphon.lua +mods\actionbar-right-center.lua +mods\actionbar-reagents.lua +mods\actionbar-float.lua +mods\classic-snowfall.lua +mods\reduced-actionbar-bags.lua +mods\reduced-actionbar-micromenu.lua +mods\actionbar-mouseover-common.lua +mods\actionbar-mouseover-bar-right.lua +mods\actionbar-mouseover-bar-right2.lua +mods\actionbar-hide-macro.lua +# merchant +mods\buy-em-all.lua +# loot +mods\loot-quality.lua +# tooltip & items +mods\metric-range.lua +mods\cursor-tooltip.lua +mods\hide-combat-tooltip.lua +# unit frames +mods\unitframes-abbrev-names.lua +# minimap & world map +mods\minimap-framerate-latency.lua +mods\worldmap-reveal.lua +# macro +mods\macro-icons.lua +mods\macro-tweaks.lua +# chat & social +mods\chat-filter-common.lua +mods\chat-npc-spam.lua +mods\world-chat-hider.lua +mods\chat-input-center.lua +mods\chat-text-shadow.lua +mods\chat-timestamps.lua + # compatibility compat-dragonflightui.lua compat-guda.lua diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 0e399b3..4f913de 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -33,9 +33,60 @@ from `TokensWorth/ShaguTweaks-mods`: - Copyright (c) 2022 GryllsAddons - License: MIT +The following modules, merged from ShaguTweaks-Extras-ClassicAPI, also +contain code adapted from `TokensWorth/ShaguTweaks-mods` (as recorded by that +project): + +- `mods/actionbar-mouseover-common.lua` +- `mods/actionbar-mouseover-bar-right.lua` +- `mods/actionbar-mouseover-bar-right2.lua` +- `mods/actionbar-hide-macro.lua` +- `mods/unitframes-abbrev-names.lua` +- `mods/cursor-tooltip.lua` +- `mods/hide-combat-tooltip.lua` +- `mods/minimap-framerate-latency.lua` +- `mods/chat-npc-spam.lua` +- `mods/world-chat-hider.lua` +- `mods/loot-quality.lua` + The preserved license text is stored in `LICENSES/ShaguTweaks-mods-MIT.txt`. +## ShaguTweaks-Extras-ClassicAPI + +These modules were merged from Dusk-92's ShaguTweaks-Extras-ClassicAPI +(commit `8e14bb4`, 2026-09-18), itself a fork of paokkerkir's +ShaguTweaks-extras: + +- Action bar: `actionbar-df-gryphon.lua`, `actionbar-right-center.lua`, + `actionbar-reagents.lua`, `actionbar-float.lua`, `classic-snowfall.lua`, + `reduced-actionbar-bags.lua`, `reduced-actionbar-micromenu.lua`, + `actionbar-mouseover-common.lua`, `actionbar-mouseover-bar-right.lua`, + `actionbar-mouseover-bar-right2.lua`, `actionbar-hide-macro.lua` +- Merchant: `buy-em-all.lua` +- Loot: `loot-quality.lua` +- Tooltip & items: `metric-range.lua`, `cursor-tooltip.lua`, + `hide-combat-tooltip.lua` +- Unit frames: `unitframes-abbrev-names.lua` +- Minimap & world map: `minimap-framerate-latency.lua`, `worldmap-reveal.lua` +- Macro: `macro-icons.lua`, `macro-tweaks.lua` +- Chat & social: `chat-filter-common.lua`, `chat-npc-spam.lua`, + `world-chat-hider.lua`, `chat-input-center.lua`, `chat-text-shadow.lua`, + `chat-timestamps.lua` + +Their option categories, texture paths and one chat message were changed for +the merge; the rest of the code is unchanged. The textures +`img/df-gryphon.tga`, `img/df-gryphon-beta.tga`, `img/df-wyvern.tga` and +`img/xpbar.tga` come from the same tree (see `Docs/ASSET_PROVENANCE.md`). + +- Sources: https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI and + https://github.com/paokkerkir/ShaguTweaks-extras +- Copyright (c) 2025 Eric Mauser (Shagu) +- License: MIT + +The preserved license text is stored in +`LICENSES/ShaguTweaks-extras-MIT.txt`. + ## brues-code/pfUI The Swing Timer and Wand Timer in `mods/swing-timer.lua` are ported from diff --git a/img/df-gryphon-beta.tga b/img/df-gryphon-beta.tga new file mode 100755 index 0000000..bb4262f Binary files /dev/null and b/img/df-gryphon-beta.tga differ diff --git a/img/df-gryphon.tga b/img/df-gryphon.tga new file mode 100755 index 0000000..6c7e965 Binary files /dev/null and b/img/df-gryphon.tga differ diff --git a/img/df-wyvern.tga b/img/df-wyvern.tga new file mode 100755 index 0000000..4414fed Binary files /dev/null and b/img/df-wyvern.tga differ diff --git a/img/xpbar.tga b/img/xpbar.tga new file mode 100755 index 0000000..de80c16 Binary files /dev/null and b/img/xpbar.tga differ diff --git a/locales/translations_enUS.lua b/locales/translations_enUS.lua index 8a84a6a..00c5154 100644 --- a/locales/translations_enUS.lua +++ b/locales/translations_enUS.lua @@ -101,4 +101,70 @@ ShaguTweaks_translation["enUS"] = { ["Clear"] = nil, ["No loot rolls yet."] = nil, ["Loot History requires ClassicAPI."] = nil, + ["Dragonflight Gryphons"] = nil, + ["Replaces actionbar gryphons with the dragonflight version."] = nil, + ["Action Bar"] = nil, + ["Floating Actionbar"] = nil, + ["Removes all background textures and lets the actionbar float."] = nil, + ["Hide Macro Text"] = nil, + ["Hides macro names on action buttons."] = nil, + ["Mouseover Right"] = nil, + ["Hide the right actionbar and show it on mouseover."] = nil, + ["Mouseover Right 2"] = nil, + ["Hide the second right actionbar and show it on mouseover."] = nil, + ["Reagent Counter"] = nil, + ["Shows a reagent counter on action buttons."] = nil, + ["Center Vertical Actionbar"] = nil, + ["Center the vertical actionbar on the right side."] = nil, + ["Buy Em All"] = nil, + ["Shift-click vendor items to choose stack, maximum, or a custom purchase amount."] = nil, + ["Merchant"] = nil, + ["Center Text Input Box"] = nil, + ["Move the chat input box to the center of the screen."] = nil, + ["Chat & Social"] = nil, + ["Block NPC Spam"] = nil, + ["Blocks known repetitive NPC say/yell spam messages."] = nil, + ["Enable Text Shadow"] = nil, + ["Enable text shadow in all chat frames."] = nil, + ["Chat Timestamps"] = nil, + ["Add timestamps to chat messages."] = nil, + ["Alt Self-Cast"] = nil, + ["When Key-Down Casting is enabled, hold Alt to cast eligible actions on yourself."] = nil, + ["Key-Down Casting"] = nil, + ["Activates action-bar and pet actions when the key is pressed instead of when it is released."] = nil, + ["Cursor Tooltip"] = nil, + ["Attaches default tooltips to the cursor."] = nil, + ["Hide Combat Tooltip"] = nil, + ["Hides the tooltip in combat. Hold Shift to show it temporarily."] = nil, + ["Loot Quality"] = nil, + ["Colors the loot frame using the highest item quality in the current loot window."] = nil, + ["Loot"] = nil, + ["Macro Icons"] = nil, + ["Detect showtooltip and spells in macros to use them on action buttons."] = nil, + ["Macro"] = nil, + ["Macro Tweaks"] = nil, + ["Add /equip, /use and modern ClassicAPI macro commands, remove #showtooltip from chat and hide macro commands from history."] = nil, + ["Metric Range"] = nil, + ["Displays tooltip range units in metres instead of yards."] = nil, + ["MiniMap Framerate & Latency"] = nil, + ["Adds a small framerate & latency display to the mini map."] = nil, + ["Minimap & World Map"] = nil, + ["Framerate"] = nil, + ["High"] = nil, + ["fps"] = nil, + ["Low"] = nil, + ["Latency"] = nil, + ["ms"] = nil, + ["Show Bags"] = nil, + ["Shows bag and keyring buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the bag bar."] = nil, + ["Show Micro Menu"] = nil, + ["Shows micro menu buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the micro menu."] = nil, + ["Unit Frame Abbreviated Names"] = nil, + ["Abbreviates long target and target-of-target names."] = nil, + ["World Chat Hider"] = nil, + ["Hides World channel messages while inside an instance without leaving the channel."] = nil, + ["Reveal World Map"] = nil, + ["Reveals unexplored world map areas and shows exploration hints."] = nil, + ["Reveal Unexplored"] = nil, + ["Exploration Point"] = nil, } diff --git a/main.lua b/main.lua index 0cefe90..57ed611 100644 --- a/main.lua +++ b/main.lua @@ -43,6 +43,12 @@ ShaguTweaks:RegisterEvent("VARIABLES_LOADED") ShaguTweaks:SetScript("OnEvent", function() -- flag all external modules as unofficial if event == "ADDON_LOADED" then + -- Most Extras ClassicAPI modules are built in now. With the old addon + -- still enabled, its copies replace the built-in ones (same titles). + if arg1 == "ShaguTweaks-extras" then + DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00ShaguTweaks:|r the Extras modules are now built in. Disable the |cffffcc00ShaguTweaks Extras ClassicAPI|r addon so they only run once.") + end + ShaguTweaks.provider = nil official = false return diff --git a/mods/actionbar-df-gryphon.lua b/mods/actionbar-df-gryphon.lua new file mode 100755 index 0000000..f34d3f3 --- /dev/null +++ b/mods/actionbar-df-gryphon.lua @@ -0,0 +1,40 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Dragonflight Gryphons"], + description = T["Replaces actionbar gryphons with the dragonflight version."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + maintainer = "@shagu (GitHub)", + category = T["Action Bar"], + config = { + ["dragonfly.gryphon"] = "retail", + }, + enabled = nil, +}) + +module.enable = function(self) + local gryphon = CreateFrame("Frame", nil, MainMenuBarArtFrame) + gryphon:SetFrameStrata("HIGH") + gryphon:RegisterEvent("PLAYER_ENTERING_WORLD") + gryphon:SetScript("OnEvent", function() + -- replace original gryphons by dragonflight versions + if module.config["dragonfly.gryphon"] == "retail" and UnitFactionGroup("player") == "Horde" then + -- retail horde + MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-wyvern") + MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-wyvern") + elseif module.config["dragonfly.gryphon"] == "retail" and UnitFactionGroup("player") == "Alliance" then + -- retail alliance + MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon") + MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon") + else + -- beta artwork + MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon-beta") + MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon-beta") + end + + -- move gryphons above action buttons + MainMenuBarLeftEndCap:SetParent(this) + MainMenuBarRightEndCap:SetParent(this) + end) +end diff --git a/mods/actionbar-float.lua b/mods/actionbar-float.lua new file mode 100755 index 0000000..89bf0df --- /dev/null +++ b/mods/actionbar-float.lua @@ -0,0 +1,107 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local hooksecurefunc = ShaguTweaks.hooksecurefunc + +local module = ShaguTweaks:register({ + title = T["Floating Actionbar"], + description = T["Removes all background textures and lets the actionbar float."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + maintainer = "@shagu (GitHub)", + category = T["Action Bar"], + enabled = nil, +}) + +local texture_removals = { + MainMenuXPBarTexture0, MainMenuXPBarTexture1, MainMenuXPBarTexture2, MainMenuXPBarTexture3, + ReputationXPBarTexture0, ReputationXPBarTexture1, ReputationXPBarTexture2, ReputationXPBarTexture3, + ReputationWatchBarTexture0, ReputationWatchBarTexture1, ReputationWatchBarTexture2, ReputationWatchBarTexture3, + MainMenuBarTexture0, MainMenuBarTexture1, MainMenuBarTexture2, MainMenuBarTexture3, + BonusActionBarTexture1, BonusActionBarTexture0, BonusActionBarTexture2 +} + +local actionbars = { + "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", + "MultiBarLeft", "MultiBarRight", "Shapeshift" +} + +module.enable = function(self) + MainMenuBar:ClearAllPoints() + MainMenuBar:SetPoint("BOTTOM", 0, 8) + + -- align actionbutton textures and add border + for _, prefix in ipairs(actionbars) do + for i = 1, NUM_ACTIONBAR_BUTTONS do + local button = _G[prefix .. "Button" .. i] + local texture = _G[prefix.."Button"..i.."NormalTexture"] + + if button and texture then + texture:SetWidth(60) + texture:SetHeight(60) + texture:SetPoint("CENTER", 0, 0) + ShaguTweaks.AddBorder(button, 3, { r=.7, g=.7, b=.7, a=1 }) + end + end + end + + ShaguTweaks.AddBorder(MainMenuBarPerformanceBarFrameButton, { -12, -0.5, -8, 4.5 }, { r=.7, g=.7, b=.7, a=1 }) + + -- replace reputation bar texture + ReputationWatchStatusBar:SetStatusBarTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar") + ReputationWatchStatusBarBackground:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar") + ReputationWatchStatusBarBackground:SetVertexColor(0, 0, 0, .5) + + -- replace experience bar texture + MainMenuExpBar:SetStatusBarTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar") + local _, _, _, _, _, background = MainMenuExpBar:GetRegions() + if background then + background:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar") + background:SetVertexColor(0, 0, 0, .5) + end + + -- center the experience bar when Floating Actionbar is used on its own. + -- Reduced Actionbar applies its own matching offset, so avoid shifting it twice. + local function UpdateExperiencePosition() + local reducedEnabled = ShaguTweaks_config + and ShaguTweaks_config[T["Reduced Actionbar Size"]] == 1 + + if reducedEnabled then return end + + MainMenuExpBar:ClearAllPoints() + if ReputationWatchBar:IsShown() then + MainMenuExpBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 0) + else + MainMenuExpBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 3) + end + end + + -- keep the custom reputation position without replacing Blizzard's updater + local function UpdateReputationPosition() + UpdateExperiencePosition() + + ReputationWatchBar:ClearAllPoints() + if MainMenuExpBar:IsShown() then + ReputationWatchBar:SetPoint("BOTTOM", MainMenuBar, "TOP", 0, -7) + else + ReputationWatchBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 2) + end + end + + hooksecurefunc("ReputationWatchBar_Update", UpdateReputationPosition) + + -- Blizzard can restore the default XP bar anchor after login/reloads and + -- actionbar visibility changes, so re-apply the fixed floating offset. + hooksecurefunc("UIParent_ManageFramePositions", UpdateExperiencePosition) + + UpdateReputationPosition() + + -- hide max level top frame + MainMenuBarMaxLevelBar:SetAlpha(0) + + -- remove textures + for _, texture in ipairs(texture_removals) do + if texture then + texture:SetTexture() + texture:Hide() + end + end +end diff --git a/mods/actionbar-hide-macro.lua b/mods/actionbar-hide-macro.lua new file mode 100755 index 0000000..b2894f0 --- /dev/null +++ b/mods/actionbar-hide-macro.lua @@ -0,0 +1,34 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Hide Macro Text"], + description = T["Hides macro names on action buttons."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + local function HideMacroText(button) + if not button or not button.GetName then return end + + local name = button:GetName() + local text = name and _G[name .. "Name"] + if text then text:SetAlpha(0) end + end + + for i = 1, 24 do + HideMacroText(_G["BonusActionButton" .. i]) + end + + for i = 1, 12 do + HideMacroText(_G["ActionButton" .. i]) + HideMacroText(_G["MultiBarRightButton" .. i]) + HideMacroText(_G["MultiBarLeftButton" .. i]) + HideMacroText(_G["MultiBarBottomLeftButton" .. i]) + HideMacroText(_G["MultiBarBottomRightButton" .. i]) + end +end diff --git a/mods/actionbar-mouseover-bar-right.lua b/mods/actionbar-mouseover-bar-right.lua new file mode 100755 index 0000000..1d40a32 --- /dev/null +++ b/mods/actionbar-mouseover-bar-right.lua @@ -0,0 +1,15 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Mouseover Right"], + description = T["Hide the right actionbar and show it on mouseover."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + ShaguTweaks.CreateMouseoverActionBar(MultiBarRight, "SHOW_MULTI_ACTIONBAR_3") +end diff --git a/mods/actionbar-mouseover-bar-right2.lua b/mods/actionbar-mouseover-bar-right2.lua new file mode 100755 index 0000000..e345835 --- /dev/null +++ b/mods/actionbar-mouseover-bar-right2.lua @@ -0,0 +1,15 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Mouseover Right 2"], + description = T["Hide the second right actionbar and show it on mouseover."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + ShaguTweaks.CreateMouseoverActionBar(MultiBarLeft, "SHOW_MULTI_ACTIONBAR_4") +end diff --git a/mods/actionbar-mouseover-common.lua b/mods/actionbar-mouseover-common.lua new file mode 100755 index 0000000..72d8c71 --- /dev/null +++ b/mods/actionbar-mouseover-common.lua @@ -0,0 +1,138 @@ +-- Shared mouseover actionbar helper. +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() + +if not ShaguTweaks.CreateMouseoverActionBar then + ShaguTweaks.CreateMouseoverActionBar = function(bar, visibilityFlag) + if not bar or not visibilityFlag then return end + if bar.ShaguTweaksMouseoverController then return end + + local controller = CreateFrame("Frame", nil, UIParent) + local hotspot = CreateFrame("Frame", nil, UIParent) + + controller.bar = bar + controller.hotspot = hotspot + controller.visibilityFlag = visibilityFlag + controller.enabled = false + controller.elapsed = 0 + controller.idle = 0 + + bar.ShaguTweaksMouseoverController = controller + + -- One invisible reveal area replaces the original per-button overlay + -- frames. It only accepts mouse input while the actionbar is hidden. + hotspot:SetAllPoints(bar) + hotspot:SetFrameStrata("DIALOG") + hotspot:EnableMouse(false) + hotspot:Hide() + + local function ReadEnabled() + return _G[visibilityFlag] and true or false + end + + local function StopWatching() + controller:SetScript("OnUpdate", nil) + controller.elapsed = 0 + controller.idle = 0 + end + + local function StartWatching() + if not controller.enabled or not bar:IsShown() then return end + + hotspot:EnableMouse(false) + controller.elapsed = 0 + controller.idle = 0 + + controller:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + (arg1 or 0) + if this.elapsed < .10 then return end + + local step = this.elapsed + this.elapsed = 0 + + if not this.enabled then + StopWatching() + hotspot:EnableMouse(false) + hotspot:Hide() + return + end + + if not bar:IsShown() then + StopWatching() + hotspot:Show() + hotspot:EnableMouse(true) + return + end + + if MouseIsOver(bar) then + this.idle = 0 + return + end + + this.idle = this.idle + step + if this.idle >= 2 then + StopWatching() + bar:Hide() + end + end) + end + + local function Sync() + controller.enabled = ReadEnabled() + + if not controller.enabled then + StopWatching() + hotspot:EnableMouse(false) + hotspot:Hide() + return + end + + hotspot:Show() + + if bar:IsShown() then + StartWatching() + else + hotspot:EnableMouse(true) + end + end + + hotspot:SetScript("OnEnter", function() + if not controller.enabled then return end + + hotspot:EnableMouse(false) + bar:Show() + -- The bar's OnShow script starts the watcher. Avoid a second redundant + -- StartWatching() call here. + end) + + -- Keep existing scripts intact while observing native/UI-addon visibility + -- changes. These wrappers are installed once per bar. + local oldOnShow = bar:GetScript("OnShow") + bar:SetScript("OnShow", function() + if oldOnShow then oldOnShow() end + if controller.enabled then StartWatching() end + end) + + local oldOnHide = bar:GetScript("OnHide") + bar:SetScript("OnHide", function() + if oldOnHide then oldOnHide() end + + StopWatching() + + if controller.enabled then + hotspot:Show() + hotspot:EnableMouse(true) + else + hotspot:EnableMouse(false) + hotspot:Hide() + end + end) + + controller:RegisterEvent("PLAYER_ENTERING_WORLD") + controller:RegisterEvent("CVAR_UPDATE") + controller:SetScript("OnEvent", Sync) + + Sync() + end +end diff --git a/mods/actionbar-reagents.lua b/mods/actionbar-reagents.lua new file mode 100755 index 0000000..743dfdf --- /dev/null +++ b/mods/actionbar-reagents.lua @@ -0,0 +1,127 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Reagent Counter"], + description = T["Shows a reagent counter on action buttons."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + maintainer = "@shagu (GitHub)", + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + local reagent_slots = { } + local reagent_counts = { } + local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight" } + + local reagentcounter = CreateFrame("Frame", "ShaguTweaksReagentCount", UIParent) + reagentcounter:RegisterEvent("PLAYER_ENTERING_WORLD") + reagentcounter:RegisterEvent("ACTIONBAR_SLOT_CHANGED") + reagentcounter:RegisterEvent("ACTIONBAR_PAGE_CHANGED") + reagentcounter:RegisterEvent("UPDATE_BONUS_ACTIONBAR") + reagentcounter:RegisterEvent("UPDATE_MACROS") + reagentcounter:RegisterEvent("BAG_UPDATE") + + -- Hidden frames still receive events, but do not execute OnUpdate. + -- Wake for one frame only when work is pending. + reagentcounter:Hide() + reagentcounter:SetScript("OnEvent", function() + this.pending = true + if event ~= "BAG_UPDATE" then + this.rescan = true + end + this:Show() + end) + + reagentcounter:SetScript("OnUpdate", function() + if not this.pending then + this:Hide() + return + end + + -- Rebuild the action-slot -> reagent mapping only when the action layout + -- changes. ClassicAPI resolves the spell/macro directly, so no tooltip + -- scan or localized reagent-name parsing is required. + if this.rescan then + reagent_counts = {} + for slot = 1, 120 do + reagentcounter.ScanSlot(slot) + end + end + + -- Count only reagents referenced by active actions. ClassicAPI reads the + -- inventory directly, avoiding a Lua bag scan for every reagent. + for itemID in pairs(reagent_counts) do + reagent_counts[itemID] = API.GetItemCount(itemID, false, false) or 0 + end + + -- update all actionbar buttons + for _, prefix in ipairs(bars) do + for i = 1, NUM_ACTIONBAR_BUTTONS do + local button = _G[prefix .. "Button" .. i] + if button then + local text = _G[button:GetName().."Count"] + local slot = ActionButton_GetPagedID(button) + + if text then + local itemID = reagent_slots[slot] + if itemID then + text:SetText(reagent_counts[itemID] or 0) + elseif not IsConsumableAction(slot) then + text:SetText() + end + end + end + end + end + + this.pending = nil + this.rescan = nil + this:Hide() + end) + + reagentcounter.ScanSlot = function(slot) + reagent_slots[slot] = nil + if not HasAction(slot) then return end + + local actionType, actionID = API.GetActionInfo(slot) + local spellID + + if actionType == "spell" then + spellID = actionID + elseif actionType == "macro" and actionID then + local _, _, macroSpellID = API.GetMacroSpell(actionID) + spellID = macroSpellID + end + + if not spellID then return end + + local reagents = API.GetSpellReagents(spellID) + local reagent = reagents and reagents[1] + if not reagent or not reagent.itemID then return end + + -- The action button only has one count field. Preserve the historical + -- behavior by showing the first reagent's owned stack count. + reagent_slots[slot] = reagent.itemID + reagent_counts[reagent.itemID] = reagent_counts[reagent.itemID] or 0 + end + + -- The stock UI clears the Count fontstring on non-consumable actions whenever + -- ActionButton_UpdateCount() runs. Re-apply our reagent value afterwards so + -- login/reload refreshes cannot erase it a moment later. + ShaguTweaks.hooksecurefunc("ActionButton_UpdateCount", function() + local button = this + if not button or not button.GetName then return end + + local slot = ActionButton_GetPagedID(button) + local itemID = slot and reagent_slots[slot] + if not itemID then return end + + local text = _G[button:GetName().."Count"] + if text then + text:SetText(reagent_counts[itemID] or 0) + end + end) +end diff --git a/mods/actionbar-right-center.lua b/mods/actionbar-right-center.lua new file mode 100755 index 0000000..d0fb300 --- /dev/null +++ b/mods/actionbar-right-center.lua @@ -0,0 +1,17 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local rgbhex = ShaguTweaks.rgbhex + +local module = ShaguTweaks:register({ + title = T["Center Vertical Actionbar"], + description = T["Center the vertical actionbar on the right side."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + maintainer = "@shagu (GitHub)", + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + MultiBarRight:ClearAllPoints() + MultiBarRight:SetPoint("RIGHT", 0, -16) +end diff --git a/mods/buy-em-all.lua b/mods/buy-em-all.lua new file mode 100755 index 0000000..4b2568c --- /dev/null +++ b/mods/buy-em-all.lua @@ -0,0 +1,662 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API +local HookScript = ShaguTweaks.HookScript +local floor = math.floor +local ceil = math.ceil +local min = math.min +local mod = math.mod or mod + +STBuyEmAll = STBuyEmAll or {} +local Buy = STBuyEmAll + +local strings = { + enUS = { + ["Max"] = "Max", + ["Stack"] = "Stack", + ["Are you sure you want to buy\n %d × %s?"] = "Are you sure you want to buy\n %d × %s?", + ["Stack purchase"] = "Stack purchase", + ["Stack size"] = "Stack size", + ["Partial stack"] = "Partial stack", + ["Maximum purchase"] = "Maximum purchase", + ["You can fit"] = "You can fit", + ["You can afford"] = "You can afford", + ["Vendor has"] = "Vendor has", + }, + deDE = { + ["Are you sure you want to buy\n %d × %s?"] = "Bist du sicher das du\n %d × %s kaufen willst?", + ["Stack size"] = "Stack größe", + ["Maximum purchase"] = "Größt möglicher Einkauf", + ["You can fit"] = "Du hast Platz für", + ["You can afford"] = "Du kannst dir leisten", + ["Vendor has"] = "Der Verkäufer hat", + }, + frFR = { + ["Stack"] = "Pile", + ["Are you sure you want to buy\n %d × %s?"] = "Voulez-vous vraiment acheter\n %d × %s?", + ["Stack size"] = "Taille de pile", + ["Maximum purchase"] = "Achat maximum", + ["You can fit"] = "Vous pouvez transporter", + ["You can afford"] = "Vous pouvez payer", + ["Vendor has"] = "Le marchand a", + }, +} + +local locale = strings[GetLocale()] or strings.enUS +local function L(key) + return locale[key] or strings.enUS[key] or key +end + +BUYEMALL_MAX = L("Max") +BUYEMALL_STACK = L("Stack") + +local module = ShaguTweaks:register({ + title = T["Buy Em All"], + description = T["Shift-click vendor items to choose stack, maximum, or a custom purchase amount."], + expansions = { ["vanilla"] = true }, + category = T["Merchant"], + enabled = true, +}) + +local function FamilyMatches(itemFamily, bagFamily) + bagFamily = tonumber(bagFamily) or 0 + itemFamily = tonumber(itemFamily) or 0 + + if bagFamily == 0 then return true end + if itemFamily == 0 then return false end + + -- Bag families are power-of-two bit flags. Avoid requiring an external bit + -- library on 1.12 while still supporting Turtle custom specialty families. + return mod(floor(itemFamily / bagFamily), 2) == 1 +end + +function Buy:CountPurchaseItem() + if not self.purchaseItemID then return nil end + if API and API.GetItemCount then + return API.GetItemCount(self.purchaseItemID, false, false) + end + return nil +end + +function Buy:StopPurchaseQueue() + if self.purchaseFrame then self.purchaseFrame:Hide() end + self.purchaseItemIndex = nil + self.purchaseItemID = nil + self.purchasePreset = 1 + self.purchaseLoops = 0 + self.purchaseAmount = 0 + self.purchaseLeftover = 0 + self.purchaseElapsed = 0 + self.purchaseWaiting = nil + self.purchaseExpectedCount = nil + self.purchasePendingKind = nil +end + +function Buy:AbortPurchaseQueue() + self:StopPurchaseQueue() + DEFAULT_CHAT_FRAME:AddMessage("Buy Em All: purchase queue stopped because the previous stack was not confirmed. No further stacks were sent.") +end + +function Buy:ConfirmPendingPurchase() + if not self.purchaseWaiting or not self.purchaseExpectedCount then return false end + + local count = self:CountPurchaseItem() + if count == nil or count < self.purchaseExpectedCount then return false end + + if self.purchasePendingKind == "loop" then + self.purchaseLoops = self.purchaseLoops - 1 + elseif self.purchasePendingKind == "leftover" then + self.purchaseLeftover = 0 + end + + self.purchaseWaiting = nil + self.purchaseExpectedCount = nil + self.purchasePendingKind = nil + self.purchaseElapsed = 0 + return true +end + +function Buy:Purchase_OnEvent() + self:ConfirmPendingPurchase() +end + +function Buy:SendPurchase(quantity, kind) + local before = self:CountPurchaseItem() + if before == nil then + self:AbortPurchaseQueue() + return + end + + self.purchaseWaiting = true + self.purchasePendingKind = kind + self.purchaseExpectedCount = before + quantity * (self.purchasePreset or 1) + self.purchaseElapsed = 0 + BuyMerchantItem(self.purchaseItemIndex, quantity) +end + +function Buy:Purchase_OnUpdate(elapsed) + self.purchaseElapsed = (self.purchaseElapsed or 0) + (elapsed or 0) + + if self.purchaseWaiting then + if self.purchaseElapsed >= self.purchaseTimeout then + if not self:ConfirmPendingPurchase() then self:AbortPurchaseQueue() end + end + return + end + + if self.purchaseElapsed < self.purchaseDelay then return end + self.purchaseElapsed = 0 + + if self.purchaseLoops and self.purchaseLoops > 0 then + self:SendPurchase(self.purchaseAmount, "loop") + return + end + + if self.purchaseLeftover and self.purchaseLeftover > 0 then + self:SendPurchase(self.purchaseLeftover, "leftover") + return + end + + self:StopPurchaseQueue() +end + +function Buy:DoPurchase(amount) + if not amount or amount <= 0 or not self.itemIndex or not self.preset or not self.stack then return end + if STBuyEmAllFrame then STBuyEmAllFrame:Hide() end + + local numLoops, purchaseAmount, leftover + if self.preset > 1 then + numLoops = floor(amount / self.preset) + purchaseAmount = 1 + leftover = 0 + else + numLoops = floor(amount / self.stack) + purchaseAmount = self.stack + leftover = mod(amount, self.stack) + end + + self:StopPurchaseQueue() + self.purchaseItemIndex = self.itemIndex + self.purchaseItemID = self.itemID + self.purchasePreset = self.preset + self.purchaseLoops = numLoops + self.purchaseAmount = purchaseAmount + self.purchaseLeftover = leftover + self.purchaseElapsed = self.purchaseDelay + + if self.purchaseLoops > 0 or self.purchaseLeftover > 0 then + self.purchaseFrame:Show() + end +end + +function Buy:DoConfirmation(amount) + self.confirmAmount = amount + local dialog = StaticPopup_Show("STBUYEMALL_CONFIRM", amount, self.itemName) + if dialog then dialog.data = amount end +end + +function Buy:VerifyPurchase(amount) + amount = amount or self.split + if not amount or amount <= 0 or not self.preset or self.preset <= 0 then return end + + amount = ceil(amount / self.preset) * self.preset + if amount > self.max then amount = self.max end + + if amount > self.stack and amount > self.defaultStack then + self:DoConfirmation(amount) + else + self:DoPurchase(amount) + end +end + +function Buy:SetStackClick() + if not self.stack or self.stack <= 0 then + self.stackClick = self.max or self.split or 1 + return + end + + local targetRemainder = self.partialFit or 0 + if targetRemainder == 0 then targetRemainder = self.stack end + + local currentRemainder = mod(self.split, self.stack) + local increase = targetRemainder - currentRemainder + if increase <= 0 then increase = increase + self.stack end + + self.stackClick = self.split + increase +end + +function Buy:UpdateDisplay(amount) + if not self.preset or not self.price or not self.split then return end + + local purchase = ceil((amount or self.split) / self.preset) + local cost = purchase * self.price + if STBuyEmAllMoneyFrame then + MoneyFrame_Update("STBuyEmAllMoneyFrame", cost) + end + + if not amount and not self.isUpdating then + self.isUpdating = true + + STBuyEmAllText:SetText(self.split) + STBuyEmAllLeftButton:Enable() + STBuyEmAllRightButton:Enable() + + if self.split >= self.max then + STBuyEmAllRightButton:Disable() + elseif self.split <= self.preset then + STBuyEmAllLeftButton:Disable() + end + + self:SetStackClick() + STBuyEmAllStackButton:Enable() + if not self.stackClick or self.max < self.stackClick then + STBuyEmAllStackButton:Disable() + end + + self.isUpdating = nil + end +end + +function Buy:Show(anchor) + self.typing = 0 + STBuyEmAllLeftButton:Disable() + STBuyEmAllRightButton:Enable() + + self:SetStackClick() + STBuyEmAllStackButton:Enable() + if not self.stackClick or self.max < self.stackClick then STBuyEmAllStackButton:Disable() end + + STBuyEmAllFrame:ClearAllPoints() + STBuyEmAllFrame:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, 4) + STBuyEmAllFrame:Show() + self:UpdateDisplay() +end + +function Buy:OnClick() + if this == STBuyEmAllOkayButton then + self:VerifyPurchase() + elseif this == STBuyEmAllCancelButton then + STBuyEmAllFrame:Hide() + elseif this == STBuyEmAllStackButton then + if self.stackClick and self.stackClick <= self.max then + self.split = self.stackClick + self:UpdateDisplay() + if this:IsEnabled() == 1 then self:OnEnter() else GameTooltip:Hide() end + end + elseif this == STBuyEmAllMaxButton then + self.split = self.max + self:UpdateDisplay() + end +end + +function Buy:OnChar() + local digit = tonumber(arg1) + if digit == nil then return end + + if self.typing == 0 then + self.typing = 1 + self.split = 0 + end + + local split = (self.split * 10) + digit + if split <= self.max then + self.split = split + end + + if self.split <= 0 then self.split = 1 end + self:UpdateDisplay() +end + +function Buy:OnKeyDown() + if arg1 == "BACKSPACE" or arg1 == "DELETE" then + if self.typing == 0 or self.split == 1 then return end + + self.split = floor(self.split / 10) + if self.split <= 1 then + self.split = 1 + self.typing = 0 + end + self:UpdateDisplay() + elseif arg1 == "ENTER" then + self:VerifyPurchase() + elseif arg1 == "ESCAPE" then + STBuyEmAllFrame:Hide() + elseif arg1 == "LEFT" or arg1 == "DOWN" then + self:Left_Click() + elseif arg1 == "RIGHT" or arg1 == "UP" then + self:Right_Click() + end +end + +function Buy:Left_Click() + if self.split <= self.preset then return end + + local decrease = mod(self.split, self.preset) + decrease = decrease == 0 and self.preset or decrease + self.split = self.split - decrease + self:UpdateDisplay() +end + +function Buy:Right_Click() + local increase = self.preset - mod(self.split, self.preset) + if self.split + increase > self.max then return end + + self.split = self.split + increase + self:UpdateDisplay() +end + +Buy.lines = { + stack = { + label = L("Stack purchase"), + field = "stackClick", + { label = L("Stack size"), field = "stack" }, + { label = L("Partial stack"), field = "partialFit" }, + }, + max = { + label = L("Maximum purchase"), + field = "max", + { label = L("You can fit"), field = "fit" }, + { label = L("You can afford"), field = "afford" }, + { + label = L("Vendor has"), + field = "available", + Hide = function() + return not Buy.available or Buy.available <= 1 + end, + }, + }, +} + +function Buy:CreateTooltip(lines) + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + GameTooltip:SetText(lines.label .. "|cFFFFFFFF - |r" .. GREEN_FONT_COLOR_CODE .. (lines.amount or 0) .. "|r") + + for _, line in ipairs(lines) do + if not (line.Hide and line.Hide()) then + local color = line.amount == lines.amount and GREEN_FONT_COLOR or HIGHLIGHT_FONT_COLOR + GameTooltip:AddDoubleLine(line.label, line.amount or 0, 1, 1, 1, color.r, color.g, color.b) + end + end + + GameTooltip:Show() +end + +function Buy:OnEnter() + local lines = self.lines[this == STBuyEmAllMaxButton and "max" or "stack"] + lines.amount = self[lines.field] + for _, line in ipairs(lines) do line.amount = self[line.field] end + + self:CreateTooltip(lines) + self:UpdateDisplay(lines.amount) +end + +function Buy:OnLeave() + self:UpdateDisplay() + GameTooltip:Hide() +end + +function Buy:OnHide() + StaticPopup_Hide("STBUYEMALL_CONFIRM") +end + +function Buy:FreeBagSpace(itemID) + if not itemID then return 0, 1 end + + local stackSize = API.GetItemMaxStackSizeByID(itemID) or 1 + if stackSize < 1 then stackSize = 1 end + + local itemFamily = API.GetItemFamily(itemID) or 0 + local capacity = 0 + + for bag = 0, 4 do + local freeSlots, bagFamily = API.GetContainerNumFreeSlots(bag) + if FamilyMatches(itemFamily, bagFamily) then + capacity = capacity + (freeSlots or 0) * stackSize + end + + local numSlots = GetContainerNumSlots(bag) + for slot = 1, numSlots do + local bagItemID = API.GetContainerItemID(bag, slot) + if bagItemID == itemID then + local itemCount = API.GetContainerItemStackCount(bag, slot) or 0 + if itemCount < stackSize then + capacity = capacity + stackSize - itemCount + end + end + end + end + + return capacity, stackSize +end + +function Buy:GetMerchantData(index) + local info = API.GetMerchantItemInfo and API.GetMerchantItemInfo(index) + if info and info.itemID then + local name = API.GetItemNameByID(info.itemID) + if not name then + name = GetMerchantItemInfo(index) + end + + return name, info.itemID, info.price or 0, info.stackCount or 1, + info.numAvailable or -1 + end + + local name, _, price, quantity, numAvailable = GetMerchantItemInfo(index) + local itemID = API.GetMerchantItemID and API.GetMerchantItemID(index) + return name, itemID, price, quantity, numAvailable +end + +function Buy:HandleMerchantClick(anchor, button, ignoreModifiers) + if not anchor or not anchor.GetID then return false end + + if MerchantFrame.selectedTab ~= 1 + or not API.IsShiftKeyDown() + or API.IsControlKeyDown() + or ignoreModifiers + or (ChatFrameEditBox:IsVisible() and button == "LeftButton") then + return false + end + + self.itemIndex = anchor:GetID() + local name, itemID, price, quantity, numAvailable = self:GetMerchantData(self.itemIndex) + if not name or not itemID or not price or not quantity or quantity < 1 then return false end + + self.preset = quantity + self.price = price + self.itemName = name + self.itemID = itemID + self.available = numAvailable + + local bagCapacity, stack = self:FreeBagSpace(itemID) + self.stack = stack + self.fit = floor(bagCapacity / quantity) * quantity + + if price > 0 then + self.afford = floor(GetMoney() / price) * quantity + else + self.afford = self.fit + end + + self.max = min(self.fit, self.afford) + if numAvailable and numAvailable > -1 then + self.max = min(self.max, numAvailable * quantity) + end + + if self.max <= 0 then return true end + if self.max == 1 then + if self.originalMerchantItemButton_OnClick then + self.originalMerchantItemButton_OnClick("LeftButton", 1) + end + return true + end + + self.defaultStack = quantity + if self.defaultStack > self.max then self.defaultStack = self.max end + + self.split = self.defaultStack + self.partialFit = mod(self.fit, stack) + self:SetStackClick() + self:Show(anchor) + return true +end + +function Buy:CreateUI() + if STBuyEmAllFrame then return end + + local frame = CreateFrame("Frame", "STBuyEmAllFrame", MerchantFrame) + frame:SetWidth(210) + frame:SetHeight(110) + frame:SetFrameStrata("DIALOG") + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + frame:SetBackdropColor(0, 0, 0, .9) + frame:EnableKeyboard(true) + frame:SetScript("OnChar", function() Buy:OnChar() end) + frame:SetScript("OnKeyDown", function() Buy:OnKeyDown() end) + frame:SetScript("OnHide", function() Buy:OnHide() end) + frame:Hide() + + local title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + title:SetPoint("TOP", frame, "TOP", 0, -10) + title:SetText(T["Buy Em All"]) + + local amount = frame:CreateFontString("STBuyEmAllText", "ARTWORK", "GameFontHighlight") + amount:SetWidth(50) + amount:SetPoint("TOP", frame, "TOP", 0, -29) + amount:SetText("1") + + local left = CreateFrame("Button", "STBuyEmAllLeftButton", frame, "UIPanelButtonTemplate") + left:SetWidth(28) + left:SetHeight(22) + left:SetPoint("RIGHT", amount, "LEFT", -4, 0) + left:SetText("-") + left:SetScript("OnClick", function() Buy:Left_Click() end) + + local right = CreateFrame("Button", "STBuyEmAllRightButton", frame, "UIPanelButtonTemplate") + right:SetWidth(28) + right:SetHeight(22) + right:SetPoint("LEFT", amount, "RIGHT", 4, 0) + right:SetText("+") + right:SetScript("OnClick", function() Buy:Right_Click() end) + + local stack = CreateFrame("Button", "STBuyEmAllStackButton", frame, "UIPanelButtonTemplate") + stack:SetWidth(58) + stack:SetHeight(22) + stack:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -50) + stack:SetText(BUYEMALL_STACK) + stack:SetScript("OnClick", function() Buy:OnClick() end) + stack:SetScript("OnEnter", function() Buy:OnEnter() end) + stack:SetScript("OnLeave", function() Buy:OnLeave() end) + + local maxButton = CreateFrame("Button", "STBuyEmAllMaxButton", frame, "UIPanelButtonTemplate") + maxButton:SetWidth(58) + maxButton:SetHeight(22) + maxButton:SetPoint("LEFT", stack, "RIGHT", 4, 0) + maxButton:SetText(BUYEMALL_MAX) + maxButton:SetScript("OnClick", function() Buy:OnClick() end) + maxButton:SetScript("OnEnter", function() Buy:OnEnter() end) + maxButton:SetScript("OnLeave", function() Buy:OnLeave() end) + + local okay = CreateFrame("Button", "STBuyEmAllOkayButton", frame, "UIPanelButtonTemplate") + okay:SetWidth(58) + okay:SetHeight(22) + okay:SetPoint("LEFT", maxButton, "RIGHT", 4, 0) + okay:SetText(OKAY) + okay:SetScript("OnClick", function() Buy:OnClick() end) + + local cancel = CreateFrame("Button", "STBuyEmAllCancelButton", frame, "UIPanelButtonTemplate") + cancel:SetWidth(58) + cancel:SetHeight(22) + cancel:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -78) + cancel:SetText(CANCEL) + cancel:SetScript("OnClick", function() Buy:OnClick() end) + + local money = CreateFrame("Frame", "STBuyEmAllMoneyFrame", frame, "SmallMoneyFrameTemplate") + money:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 11) + + MoneyTypeInfo["STBUYEMALL"] = { + UpdateFunc = function() return 0 end, + showSmallerCoins = "Backpack", + fixedWidth = 1, + collapse = 1, + } + money.info = MoneyTypeInfo["STBUYEMALL"] + money.moneyType = "STBUYEMALL" + money.small = 1 +end + +module.enable = function(self) + if IsAddOnLoaded("BuyEmAll") then return end + + local required = { + "GetItemCount", "GetItemMaxStackSizeByID", "GetItemFamily", + "GetContainerNumFreeSlots", "GetContainerItemID", + "GetContainerItemStackCount", "GetMerchantItemInfo", + "GetMerchantItemID", "IsShiftKeyDown", "IsControlKeyDown", + } + for _, name in ipairs(required) do + if not API or type(API[name]) ~= "function" then + DEFAULT_CHAT_FRAME:AddMessage("|cffff5555ShaguTweaks:|r Buy Em All requires ClassicAPI.") + return + end + end + + Buy:CreateUI() + + Buy.purchaseDelay = 0.05 + Buy.purchaseTimeout = 3.0 + Buy.purchaseFrame = Buy.purchaseFrame or CreateFrame("Frame", "STBuyEmAllPurchaseQueue", UIParent) + Buy.purchaseFrame:Hide() + Buy.purchaseFrame:RegisterEvent("BAG_UPDATE") + Buy.purchaseFrame:RegisterEvent("MERCHANT_UPDATE") + Buy.purchaseFrame:SetScript("OnEvent", function() + Buy:Purchase_OnEvent() + end) + Buy.purchaseFrame:SetScript("OnUpdate", function() + Buy:Purchase_OnUpdate(arg1) + end) + + StaticPopupDialogs["STBUYEMALL_CONFIRM"] = { + text = L("Are you sure you want to buy\n %d × %s?"), + button1 = YES, + button2 = NO, + OnAccept = function() + Buy:DoPurchase(Buy.confirmAmount) + end, + timeout = 0, + hideOnEscape = true, + } + + HookScript(MerchantFrame, "OnHide", function() + STBuyEmAllFrame:Hide() + Buy:StopPurchaseQueue() + end) + + local function MerchantItemButton_OnClick_Wrapper(button, ignoreModifiers) + local anchor = this + if Buy:HandleMerchantClick(anchor, button, ignoreModifiers) then return end + if Buy.originalMerchantItemButton_OnClick then + return Buy.originalMerchantItemButton_OnClick(button, ignoreModifiers) + end + end + + local function InstallHook() + local current = _G.MerchantItemButton_OnClick + if type(current) ~= "function" then return end + if current == MerchantItemButton_OnClick_Wrapper then return end + + Buy.originalMerchantItemButton_OnClick = current + _G.MerchantItemButton_OnClick = MerchantItemButton_OnClick_Wrapper + end + + InstallHook() + + local frame = CreateFrame("Frame", nil, UIParent) + frame:RegisterEvent("PLAYER_ENTERING_WORLD") + frame:RegisterEvent("MERCHANT_SHOW") + frame:SetScript("OnEvent", InstallHook) +end diff --git a/mods/chat-filter-common.lua b/mods/chat-filter-common.lua new file mode 100755 index 0000000..344b8b7 --- /dev/null +++ b/mods/chat-filter-common.lua @@ -0,0 +1,68 @@ +-- Shared chat-filter dispatcher for the chat filter modules (formerly in Extras). +-- Installs one ChatFrame_OnEvent wrapper only when a filter is enabled. + +local Filter = ShaguTweaks.ExtrasChatFilter + +if not Filter then + Filter = { + handlers = {}, + order = {}, + known = {}, + installed = false, + installer = nil, + } + ShaguTweaks.ExtrasChatFilter = Filter +end + +function Filter:Install() + if self.installed then return end + + self.installed = true + self.original = ChatFrame_OnEvent + + ChatFrame_OnEvent = function(event) + for i = 1, table.getn(Filter.order) do + local current = Filter.handlers[Filter.order[i]] + if current and current(event) then + return + end + end + + if Filter.original then + return Filter.original(event) + end + end +end + +function Filter:ScheduleInstall() + if self.installed or self.installer then return end + + -- ShaguTweaks enables modules during VARIABLES_LOADED in unspecified table + -- order. Install on PLAYER_ENTERING_WORLD so this dispatcher becomes the + -- outer wrapper after the core Chat Spam Filter regardless of module order. + -- That prevents locally hidden World/NPC messages from entering its cache. + self.installer = CreateFrame("Frame") + self.installer:RegisterEvent("PLAYER_ENTERING_WORLD") + self.installer:SetScript("OnEvent", function() + Filter:Install() + this:UnregisterAllEvents() + this:Hide() + Filter.installer = nil + end) +end + +function Filter:Register(key, handler) + if type(key) ~= "string" or type(handler) ~= "function" then return end + + if not self.known[key] then + self.known[key] = true + self.order[table.getn(self.order) + 1] = key + end + + self.handlers[key] = handler + self:ScheduleInstall() +end + +function Filter:Unregister(key) + self.handlers[key] = nil +end diff --git a/mods/chat-input-center.lua b/mods/chat-input-center.lua new file mode 100755 index 0000000..3e3eba6 --- /dev/null +++ b/mods/chat-input-center.lua @@ -0,0 +1,45 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local hooksecurefunc = ShaguTweaks.hooksecurefunc + +local module = ShaguTweaks:register({ + title = T["Center Text Input Box"], + description = T["Move the chat input box to the center of the screen."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + category = T["Chat & Social"], + maintainer = "@shagu (GitHub)", + enabled = nil, +}) + +local dodge_frames = { + MainMenuBarArtFrame, MultiBarBottomLeft, MultiBarBottomRight, PetActionBarFrame, ShapeshiftBarFrame +} + +module.enable = function(self) + ChatFrameEditBox:SetWidth(300) + + local function UpdateInputPosition() + local top = 0 + for _, frame in ipairs(dodge_frames) do + if frame and frame:IsVisible() and frame:GetTop() then + top = math.max(top, frame:GetTop()) + end + end + + ChatFrameEditBox:ClearAllPoints() + ChatFrameEditBox:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, top) + end + + -- Other ShaguTweaks modules also post-hook UIParent_ManageFramePositions. + -- Module enable order is intentionally unordered, so install this layout + -- hook at PLAYER_ENTERING_WORLD after the normal module pass. This makes + -- the edit box position use the final actionbar/pet/stance positions. + local installer = CreateFrame("Frame", nil, UIParent) + installer:RegisterEvent("PLAYER_ENTERING_WORLD") + installer:SetScript("OnEvent", function() + hooksecurefunc("UIParent_ManageFramePositions", UpdateInputPosition) + UpdateInputPosition() + this:UnregisterAllEvents() + this:Hide() + end) +end diff --git a/mods/chat-npc-spam.lua b/mods/chat-npc-spam.lua new file mode 100755 index 0000000..5b696f5 --- /dev/null +++ b/mods/chat-npc-spam.lua @@ -0,0 +1,92 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T +local Filter = ShaguTweaks.ExtrasChatFilter + +local module = ShaguTweaks:register({ + title = T["Block NPC Spam"], + description = T["Blocks known repetitive NPC say/yell spam messages."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Chat & Social"], + enabled = nil, +}) + +local blockedEvents = { + ["CHAT_MSG_MONSTER_SAY"] = true, + ["CHAT_MSG_MONSTER_YELL"] = true, +} + +local blockedNPCs = {} +local npcNames = { + -- enUS + "Tansy Sparkpen", + "Fara Boltbreaker", + "Shellcoin Promoter", + + -- ruRU + "Зазывала ярмарки Новолуния", + "Томас Миллер", + "Уильям", + "Донна", + "Жюстина Демалье", + "Мерлис Малаган", + "Лиана Пирс", + "Сюзанна", + "Джейни Аншип", +} + +for _, name in pairs(npcNames) do + blockedNPCs[name] = true + blockedNPCs[string.lower(name)] = true +end + +local blockedPhrases = { + { "shellcoin", "invest" }, + { "shells", "trade" }, + { "shells", "money" }, +} + +local function IsBlockedNPC(sender) + if not sender then return false end + return blockedNPCs[sender] or blockedNPCs[string.lower(sender)] or false +end + +local function IsBlockedPhrase(message) + if not message then return false end + + local normalized = string.lower(string.gsub(message, "[^A-Za-z0-9]", "")) + + for _, phrase in pairs(blockedPhrases) do + local matched = true + + for _, word in pairs(phrase) do + if not string.find(normalized, word, 1, true) then + matched = false + break + end + end + + if matched then return true end + end + + return false +end + +local function FilterNPCSpam(event) + if not blockedEvents[event] then return false end + if not arg1 or not arg2 then return false end + + return IsBlockedNPC(arg2) or IsBlockedPhrase(arg1) +end + +module.enable = function(self) + if Filter then + Filter:Register("BlockNPCSpam", FilterNPCSpam) + end +end + +module.disable = function(self) + if Filter then + Filter:Unregister("BlockNPCSpam") + end +end diff --git a/mods/chat-text-shadow.lua b/mods/chat-text-shadow.lua new file mode 100755 index 0000000..0dfcae2 --- /dev/null +++ b/mods/chat-text-shadow.lua @@ -0,0 +1,19 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local rgbhex = ShaguTweaks.rgbhex + +local module = ShaguTweaks:register({ + title = T["Enable Text Shadow"], + description = T["Enable text shadow in all chat frames."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + category = T["Chat & Social"], + maintainer = "@shagu (GitHub)", + enabled = nil, +}) + +module.enable = function(self) + for i=1,NUM_CHAT_WINDOWS do + local font, size = _G["ChatFrame"..i]:GetFont() + _G["ChatFrame"..i]:SetFont(font, size, "OUTLINE") + end +end diff --git a/mods/chat-timestamps.lua b/mods/chat-timestamps.lua new file mode 100755 index 0000000..bb52274 --- /dev/null +++ b/mods/chat-timestamps.lua @@ -0,0 +1,57 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local rgbhex = ShaguTweaks.rgbhex + +local module = ShaguTweaks:register({ + title = T["Chat Timestamps"], + description = T["Add timestamps to chat messages."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + category = T["Chat & Social"], + maintainer = "@shagu (GitHub)", + enabled = false, + config = { + ["chat.timestamp.bracket"] = "[]", + ["chat.timestamp.format"] = 24, + ["chat.timestamp.color"] = { r = .8, g = .8, b = .8, a = 1}, + } +}) + +module.enable = function(self) + -- config shortcuts + local bracket = self.config["chat.timestamp.bracket"] + local clock = self.config["chat.timestamp.format"] + local rgb = self.config["chat.timestamp.color"] + + -- parse config + local left = string.sub(bracket, 1, 1) or "" + local right = string.sub(bracket, 2, 2) or "" + local format = clock == 24 and "%H:%M:%S" or "%I:%M:%S %p" + local color = rgbhex({ rgb.r, rgb.g, rgb.b, rgb.a }) + + local function InstallTimestampHooks() + for i = 1, NUM_CHAT_WINDOWS do + local frame = _G["ChatFrame"..i] + if frame and not frame.ShaguTweaksExtrasTimestampAddMessage then + frame.ShaguTweaksExtrasTimestampAddMessage = frame.AddMessage + frame.AddMessage = function(self, msg, a1, a2, a3, a4, a5) + if not msg then return end + + msg = color .. left .. date(format) .. right .. "|r " .. msg + self:ShaguTweaksExtrasTimestampAddMessage(msg, a1, a2, a3, a4, a5) + end + end + end + end + + -- Install after the normal ShaguTweaks module pass. Chat Tweaks in the + -- main fork also wraps AddMessage, and its module order is intentionally + -- unordered. Waiting for PLAYER_ENTERING_WORLD makes timestamps the outer + -- wrapper every time, so history consistently stores the displayed time. + local installer = CreateFrame("Frame", nil, UIParent) + installer:RegisterEvent("PLAYER_ENTERING_WORLD") + installer:SetScript("OnEvent", function() + InstallTimestampHooks() + this:UnregisterAllEvents() + this:Hide() + end) +end diff --git a/mods/classic-snowfall.lua b/mods/classic-snowfall.lua new file mode 100755 index 0000000..2b96377 --- /dev/null +++ b/mods/classic-snowfall.lua @@ -0,0 +1,299 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local selfcastTitle = T["Alt Self-Cast"] + +local selfcast = ShaguTweaks:register({ + title = selfcastTitle, + description = T["When Key-Down Casting is enabled, hold Alt to cast eligible actions on yourself."], + expansions = { ["vanilla"] = true }, + category = T["Action Bar"], + enabled = nil, +}) + +selfcast.enable = function(self) + -- Runtime behavior is handled by Key-Down Casting below. +end + +local module = ShaguTweaks:register({ + title = T["Key-Down Casting"], + description = T["Activates action-bar and pet actions when the key is pressed instead of when it is released."], + expansions = { ["vanilla"] = true }, + category = T["Action Bar"], + enabled = true, +}) + +module.enable = function(self) + -- Keep this feature independent from No Toggle Auto-Attack. + if IsAddOnLoaded("ClassicSnowFall") or IsAddOnLoaded("ClassicSnowfall") then return end + + local function SelfCast() + if ShaguTweaks_config and ShaguTweaks_config[selfcastTitle] == 1 then + return API.IsAltKeyDown() and 1 or 0 + end + return 0 + end + + -- Preserve the original slash command while using a unique registry key. + SLASH_STCLASSICSNOWFALLSELFCAST1 = "/csselfcast" + SlashCmdList["STCLASSICSNOWFALLSELFCAST"] = function() + ShaguTweaks_config = ShaguTweaks_config or {} + if ShaguTweaks_config[selfcastTitle] == 1 then + ShaguTweaks_config[selfcastTitle] = 0 + DEFAULT_CHAT_FRAME:AddMessage("Classic Snowfall ALT SelfCast now disabled.") + else + ShaguTweaks_config[selfcastTitle] = 1 + DEFAULT_CHAT_FRAME:AddMessage("Classic Snowfall ALT SelfCast now enabled.") + end + end + + local function IsBongosReady() + return _G.BActionButton and type(_G.BActionButton.GetPagedID) == "function" + end + + local function ST_ActionButtonDown(id) + local button, pagedID + + if IsBongosReady() then + button = _G["BActionButton" .. id] + pagedID = _G.BActionButton.GetPagedID(id) + if button and button:GetButtonState() == "NORMAL" then + button:SetButtonState("PUSHED") + end + if pagedID then UseAction(pagedID, 0, SelfCast()) end + return + end + + if BonusActionBarFrame and BonusActionBarFrame:IsShown() then + button = _G["BonusActionButton" .. id] + else + button = _G["ActionButton" .. id] + end + + if button and button:GetButtonState() == "NORMAL" then + button:SetButtonState("PUSHED") + UseAction(ActionButton_GetPagedID(button), 0, SelfCast()) + end + end + + local function ST_ActionButtonUp(id) + local button, pagedID + + if IsBongosReady() then + button = _G["BActionButton" .. id] + pagedID = _G.BActionButton.GetPagedID(id) + if button and button:GetButtonState() == "PUSHED" then + button:SetButtonState("NORMAL") + if MacroFrame_SaveMacro then MacroFrame_SaveMacro() end + if pagedID and IsCurrentAction(pagedID) then + button:SetChecked(1) + else + button:SetChecked(0) + end + end + return + end + + if BonusActionBarFrame and BonusActionBarFrame:IsShown() then + button = _G["BonusActionButton" .. id] + else + button = _G["ActionButton" .. id] + end + + if button and button:GetButtonState() == "PUSHED" then + button:SetButtonState("NORMAL") + if MacroFrame_SaveMacro then MacroFrame_SaveMacro() end + if IsCurrentAction(ActionButton_GetPagedID(button)) then + button:SetChecked(1) + else + button:SetChecked(0) + end + end + end + + local function ST_MultiActionButtonDown(bar, id) + local button = _G[bar .. "Button" .. id] + if button and button:GetButtonState() == "NORMAL" then + button:SetButtonState("PUSHED") + UseAction(ActionButton_GetPagedID(button), 0, SelfCast()) + end + end + + local function ST_MultiActionButtonUp(bar, id) + local button = _G[bar .. "Button" .. id] + if button and button:GetButtonState() == "PUSHED" then + button:SetButtonState("NORMAL") + if MacroFrame_SaveMacro then MacroFrame_SaveMacro() end + if IsCurrentAction(ActionButton_GetPagedID(button)) then + button:SetChecked(1) + else + button:SetChecked(0) + end + end + end + + local function ST_PetActionButtonDown(id) + local button = _G["PetActionButton" .. id] + if button and button:GetButtonState() == "NORMAL" then + button:SetButtonState("PUSHED") + CastPetAction(id) + end + end + + local function ST_PetActionButtonUp(id) + local button = _G["PetActionButton" .. id] + if button and button:GetButtonState() == "PUSHED" then + button:SetButtonState("NORMAL") + end + end + + -- Vanilla's BONUSACTIONBUTTON bindings are aliases for the pet-action + -- handlers. Keep the same relationship while moving the cast to key-down. + local function ST_BonusActionButtonDown(id) + ST_PetActionButtonDown(id) + end + + local function ST_BonusActionButtonUp(id) + ST_PetActionButtonUp(id) + end + + local function Install() + _G.ActionButtonDown = ST_ActionButtonDown + _G.ActionButtonUp = ST_ActionButtonUp + _G.MultiActionButtonDown = ST_MultiActionButtonDown + _G.MultiActionButtonUp = ST_MultiActionButtonUp + _G.PetActionButtonDown = ST_PetActionButtonDown + _G.PetActionButtonUp = ST_PetActionButtonUp + + if type(_G.BonusActionButtonDown) == "function" then + _G.BonusActionButtonDown = ST_BonusActionButtonDown + end + if type(_G.BonusActionButtonUp) == "function" then + _G.BonusActionButtonUp = ST_BonusActionButtonUp + end + end + + local function HandlersAreInstalled() + return _G.ActionButtonDown == ST_ActionButtonDown + and _G.ActionButtonUp == ST_ActionButtonUp + and _G.MultiActionButtonDown == ST_MultiActionButtonDown + and _G.MultiActionButtonUp == ST_MultiActionButtonUp + and _G.PetActionButtonDown == ST_PetActionButtonDown + and _G.PetActionButtonUp == ST_PetActionButtonUp + end + + -- Modified bindings (SHIFT-*, CTRL-*, ALT-*) are routed through ClassicAPI's + -- normalized override-binding layer. Vanilla's runOnUp binding path is not + -- reliable for every modified-key combination on custom 1.12 clients. + -- Normal unmodified keys continue through the original ClassicSnowfall-style + -- global handlers above. + local bindingOwner = CreateFrame("Frame", "ShaguTweaksKeyDownBindingOwner", UIParent) + local proxies = {} + local specs = {} + + local function AddSpec(command, kind, a, b) + table.insert(specs, { command = command, kind = kind, a = a, b = b }) + end + + for i = 1, 12 do + AddSpec("ACTIONBUTTON" .. i, "action", i) + AddSpec("MULTIACTIONBAR1BUTTON" .. i, "multi", "MultiBarBottomLeft", i) + AddSpec("MULTIACTIONBAR2BUTTON" .. i, "multi", "MultiBarBottomRight", i) + AddSpec("MULTIACTIONBAR3BUTTON" .. i, "multi", "MultiBarRight", i) + AddSpec("MULTIACTIONBAR4BUTTON" .. i, "multi", "MultiBarLeft", i) + AddSpec("BONUSACTIONBUTTON" .. i, "bonus", i) + end + + local function IsModifiedKey(key) + if type(key) ~= "string" then return false end + local upper = string.upper(key) + return string.find(upper, "SHIFT-", 1, true) + or string.find(upper, "CTRL-", 1, true) + or string.find(upper, "ALT-", 1, true) + end + + local function GetProxy(spec, index) + local name = "ShaguTweaksKeyDownProxy" .. index + if proxies[index] then return proxies[index] end + + local button = CreateFrame("Button", name, bindingOwner) + button.spec = spec + button:SetScript("OnClick", function() + local s = this.spec + if s.kind == "action" then + ST_ActionButtonDown(s.a) + ST_ActionButtonUp(s.a) + elseif s.kind == "multi" then + ST_MultiActionButtonDown(s.a, s.b) + ST_MultiActionButtonUp(s.a, s.b) + elseif s.kind == "bonus" then + ST_BonusActionButtonDown(s.a) + ST_BonusActionButtonUp(s.a) + end + end) + proxies[index] = button + return button + end + + local refreshingBindings + local function RefreshModifiedBindings() + if refreshingBindings then return end + if not API or not API.overridebindings then return end + + refreshingBindings = true + API.ClearOverrideBindings(bindingOwner) + + for index, spec in ipairs(specs) do + local key1, key2 = GetBindingKey(spec.command) + local proxy = GetProxy(spec, index) + + if IsModifiedKey(key1) then + API.SetOverrideBindingClick(bindingOwner, true, key1, proxy:GetName()) + end + if IsModifiedKey(key2) then + API.SetOverrideBindingClick(bindingOwner, true, key2, proxy:GetName()) + end + end + + refreshingBindings = nil + end + + Install() + RefreshModifiedBindings() + + local frame = CreateFrame("Frame", nil, UIParent) + frame:RegisterEvent("PLAYER_LOGIN") + frame:RegisterEvent("PLAYER_ENTERING_WORLD") + frame:RegisterEvent("UPDATE_BINDINGS") + frame.guardUntil = 0 + frame.guardTicker = nil + + local function GuardTick() + if not frame.guardUntil or frame.guardUntil <= GetTime() then + if frame.guardTicker then + frame.guardTicker:Cancel() + frame.guardTicker = nil + end + return + end + + if not HandlersAreInstalled() then Install() end + end + + local function StartGuard() + frame.guardUntil = GetTime() + 8 + if not frame.guardTicker then + frame.guardTicker = C_Timer.NewTicker(0.25, GuardTick) + end + end + + frame:SetScript("OnEvent", function() + Install() + RefreshModifiedBindings() + + if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then + StartGuard() + end + end) +end diff --git a/mods/cursor-tooltip.lua b/mods/cursor-tooltip.lua new file mode 100755 index 0000000..d797055 --- /dev/null +++ b/mods/cursor-tooltip.lua @@ -0,0 +1,99 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local hooksecurefunc = ShaguTweaks.hooksecurefunc + +local module = ShaguTweaks:register({ + title = T["Cursor Tooltip"], + description = T["Attaches default tooltips to the cursor."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Tooltip & Items"], + enabled = nil, +}) + +module.enable = function(self) + if ShaguTweaks.CursorTooltipInstalled then return end + ShaguTweaks.CursorTooltipInstalled = true + + -- Keep the native helper for non-GameTooltip callers. The feature only + -- needs to move the normal world/UI GameTooltip and should not unexpectedly + -- change other tooltip frames that may reuse the same helper. + local originalDefaultAnchor = ShaguTweaks.CursorTooltipOriginalDefaultAnchor + or _G.GameTooltip_SetDefaultAnchor + + ShaguTweaks.CursorTooltipOriginalDefaultAnchor = originalDefaultAnchor + + local cursor = CreateFrame("Frame", nil, UIParent) + cursor:SetWidth(1) + cursor:SetHeight(1) + cursor:Hide() + + local following = false + local lastX, lastY, lastScale + + local function UpdateCursor(force) + local scale = UIParent:GetEffectiveScale() + if not scale or scale == 0 then scale = UIParent:GetScale() end + if not scale or scale == 0 then scale = 1 end + + local x, y = GetCursorPosition() + + -- GetCursorPosition is cheap and required for following the pointer, but + -- avoid a frame-layout operation while the mouse is completely still. + if not force and x == lastX and y == lastY and scale == lastScale then + return + end + + lastX, lastY, lastScale = x, y, scale + + -- Reusing the same CENTER anchor updates it in place; ClearAllPoints every + -- rendered frame is unnecessary. + cursor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x / scale, y / scale) + end + + cursor:SetScript("OnUpdate", function() + if not following or not GameTooltip:IsShown() then + this:Hide() + return + end + + UpdateCursor(false) + end) + + function _G.GameTooltip_SetDefaultAnchor(tooltip, parent) + if tooltip ~= GameTooltip then + return originalDefaultAnchor(tooltip, parent) + end + + -- Cursor Tooltip intentionally uses ANCHOR_CURSOR without marking the + -- tooltip as a default anchored tooltip. This matches the upstream mod's + -- lifecycle and makes world-unit tooltips disappear immediately when the + -- mouseover unit is lost, while keeping the custom cursor positioning. + tooltip:SetOwner(parent or UIParent, "ANCHOR_CURSOR") + tooltip:ClearAllPoints() + + -- Position once before Show() so there is no first-frame jump from the + -- cursor frame's default origin. + UpdateCursor(true) + + tooltip:SetPoint("BOTTOMLEFT", cursor, "TOPRIGHT", 12, 12) + tooltip:SetClampedToScreen(true) + + following = true + end + + -- Default anchoring normally happens before GameTooltip:Show(). Start the + -- per-frame cursor tracker only once the tooltip is actually visible. + hooksecurefunc(GameTooltip, "Show", function() + if not following then return end + + UpdateCursor(true) + cursor:Show() + end) + + hooksecurefunc(GameTooltip, "Hide", function() + following = false + cursor:Hide() + end) +end diff --git a/mods/hide-combat-tooltip.lua b/mods/hide-combat-tooltip.lua new file mode 100755 index 0000000..7c4242a --- /dev/null +++ b/mods/hide-combat-tooltip.lua @@ -0,0 +1,146 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Hide Combat Tooltip"], + description = T["Hides the tooltip in combat. Hold Shift to show it temporarily."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Tooltip & Items"], + enabled = nil, +}) + +module.enable = function(self) + if ShaguTweaks.HideCombatTooltipInstalled then return end + ShaguTweaks.HideCombatTooltipInstalled = true + + local controller = CreateFrame("Frame", nil, UIParent) + local inCombat = UnitAffectingCombat("player") and true or false + local shiftDown = API.IsShiftKeyDown() and true or false + local guardActive = false + local elapsed = 0 + + local function Apply() + if not inCombat then + if GameTooltip:GetAlpha() ~= 1 then + GameTooltip:SetAlpha(1) + end + return + end + + local alpha = shiftDown and 1 or 0 + if GameTooltip:GetAlpha() ~= alpha then + GameTooltip:SetAlpha(alpha) + end + end + + local function GuardTick() + elapsed = elapsed + (arg1 or 0) + if elapsed < .05 then return end + elapsed = 0 + + -- ClassicAPI provides modifier events, so normal clients don't need to + -- query Shift here. This fallback is only for old environments. + if not API.modifierstate then + shiftDown = API.IsShiftKeyDown() and true or false + end + + Apply() + end + + local function StartGuard() + if guardActive or not inCombat or not GameTooltip:IsShown() then return end + + guardActive = true + elapsed = 0 + controller:SetScript("OnUpdate", GuardTick) + end + + local function StopGuard() + if not guardActive then return end + + guardActive = false + elapsed = 0 + controller:SetScript("OnUpdate", nil) + end + + local function RefreshShift() + shiftDown = API.IsShiftKeyDown() and true or false + + if inCombat and GameTooltip:IsShown() then + Apply() + StartGuard() + end + end + + local function EnterCombat() + inCombat = true + shiftDown = API.IsShiftKeyDown() and true or false + + if GameTooltip:IsShown() then + Apply() + StartGuard() + end + end + + local function LeaveCombat() + inCombat = false + StopGuard() + Apply() + end + + -- Vanilla/Turtle can restore GameTooltip alpha internally while updating a + -- visible unit tooltip. A purely event-driven implementation therefore does + -- not stay hidden reliably. The guard below exists only while BOTH combat + -- and an actual GameTooltip are active, and is throttled to 20 Hz instead of + -- running the full upstream logic every rendered frame. + local oldOnShow = GameTooltip:GetScript("OnShow") + GameTooltip:SetScript("OnShow", function() + if oldOnShow then oldOnShow() end + + if inCombat then + shiftDown = API.IsShiftKeyDown() and true or false + Apply() + StartGuard() + else + Apply() + end + end) + + local oldOnHide = GameTooltip:GetScript("OnHide") + GameTooltip:SetScript("OnHide", function() + if oldOnHide then oldOnHide() end + StopGuard() + end) + + controller:RegisterEvent("PLAYER_ENTERING_WORLD") + controller:RegisterEvent("PLAYER_REGEN_DISABLED") + controller:RegisterEvent("PLAYER_REGEN_ENABLED") + + if API.modifierstate then + controller:RegisterEvent("MODIFIER_STATE_CHANGED") + end + + controller:SetScript("OnEvent", function() + if event == "PLAYER_REGEN_DISABLED" then + EnterCombat() + elseif event == "PLAYER_REGEN_ENABLED" then + LeaveCombat() + elseif event == "MODIFIER_STATE_CHANGED" then + RefreshShift() + elseif event == "PLAYER_ENTERING_WORLD" then + if UnitAffectingCombat("player") then + EnterCombat() + else + LeaveCombat() + end + end + end) + + if inCombat then + EnterCombat() + else + LeaveCombat() + end +end diff --git a/mods/loot-quality.lua b/mods/loot-quality.lua new file mode 100755 index 0000000..0ff713f --- /dev/null +++ b/mods/loot-quality.lua @@ -0,0 +1,128 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Loot Quality"], + description = T["Colors the loot frame using the highest item quality in the current loot window."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Loot"], + enabled = nil, +}) + +local MIN_QUALITY = 2 -- uncommon / green + +local function FindLootTitle() + local regions = { LootFrame:GetRegions() } + + for i = 1, table.getn(regions) do + local region = regions[i] + -- Some Vanilla/Turtle regions expose GetText without being FontStrings. + -- Only keep a region that can actually be recolored. + if region + and type(region.GetText) == "function" + and type(region.SetTextColor) == "function" + and region:GetText() == ITEMS then + return region + end + end +end + +module.enable = function(self) + if not self.border then + self.border = LootFrame:CreateTexture(nil, "OVERLAY") + self.border:SetTexture("Interface\\LootFrame\\UI-LootPanel") + self.border:SetPoint("TOPLEFT", LootFrame, "TOPLEFT", -4, 3) + self.border:SetPoint("BOTTOMRIGHT", LootFrame, "BOTTOMRIGHT", 5, -3) + self.border:SetDrawLayer("ARTWORK") + + self.highlight = LootFrame:CreateTexture(nil, "OVERLAY") + self.highlight:SetTexture("Interface\\LootFrame\\UI-LootPanel") + self.highlight:SetAllPoints(self.border) + self.highlight:SetDrawLayer("ARTWORK") + self.highlight:SetBlendMode("ADD") + end + + self.title = self.title or FindLootTitle() + + if self.title + and not self.titleColor + and type(self.title.GetTextColor) == "function" then + local r, g, b, a = self.title:GetTextColor() + self.titleColor = { r or 1, g or .82, b or 0, a or 1 } + end + + local function RestoreTitle() + if not self.title or type(self.title.SetTextColor) ~= "function" then return end + + local color = self.titleColor + if color then + self.title:SetTextColor(color[1], color[2], color[3], color[4]) + else + self.title:SetTextColor(1, .82, 0) + end + end + + local function ResetVisuals() + self.border:Hide() + self.highlight:Hide() + RestoreTitle() + end + + local function UpdateQuality() + local highestQuality = 0 + local count = GetNumLootItems() or 0 + + for i = 1, count do + if LootSlotIsItem(i) then + local _, _, _, quality = GetLootSlotInfo(i) + quality = tonumber(quality) or 0 + if quality > highestQuality then + highestQuality = quality + end + end + end + + local color = ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[highestQuality] + if highestQuality < MIN_QUALITY or not color then + ResetVisuals() + return + end + + self.border:SetVertexColor(color.r, color.g, color.b) + self.highlight:SetVertexColor(color.r, color.g, color.b) + self.border:Show() + self.highlight:Show() + + if self.title and type(self.title.SetTextColor) == "function" then + self.title:SetTextColor(color.r, color.g, color.b) + end + end + + self.ResetVisuals = ResetVisuals + self.events = self.events or CreateFrame("Frame") + self.events:UnregisterAllEvents() + self.events:RegisterEvent("LOOT_OPENED") + self.events:RegisterEvent("LOOT_SLOT_CLEARED") + self.events:RegisterEvent("LOOT_CLOSED") + + self.events:SetScript("OnEvent", function() + if event == "LOOT_CLOSED" then + ResetVisuals() + else + UpdateQuality() + end + end) + + ResetVisuals() +end + +module.disable = function(self) + if self.events then + self.events:UnregisterAllEvents() + end + + if self.ResetVisuals then + self.ResetVisuals() + end +end diff --git a/mods/macro-icons.lua b/mods/macro-icons.lua new file mode 100755 index 0000000..4eb3ca6 --- /dev/null +++ b/mods/macro-icons.lua @@ -0,0 +1,144 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API +local libspell = ShaguTweaks.libspell + +local module = ShaguTweaks:register({ + title = T["Macro Icons"], + description = T["Detect showtooltip and spells in macros to use them on action buttons."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + maintainer = "@shagu (GitHub)", + category = T["Macro"], + enabled = true, +}) + +module.enable = function(self) + local gfind = string.gmatch or string.gfind + + local function BuildMacroCache() + local cache = {} + for macroSlot = 1, 36 do + local name, _, body = GetMacroInfo(macroSlot) + if name and body then + cache[name] = { id = macroSlot, body = body } + end + end + return cache + end + + local function GetMacroData(actionSlot, cache) + local actionType, id = API.GetActionInfo(actionSlot) + if actionType == "macro" and id then + local _, _, body = GetMacroInfo(id) + if body then return id, body end + end + + local macroName = GetActionText(actionSlot) + local entry = macroName and cache[macroName] + if entry then return entry.id, entry.body end + end + + local function ParseSpellText(match) + if not match then return end + local _, _, spell, rank = string.find(match, "(.+)%((.+)%)") + return spell or match, rank + end + + local function ResolveSpell(body, macroSlot) + if not body then return end + + local customTooltip + local fallback + + for line in gfind(body, "[^%\n]+") do + -- Explicit #showtooltip always wins. + local _, _, match = string.find(line, "^#showtooltip%s+(.+)") + if match then + return ParseSpellText(match) + end + + -- Preserve the historical ShaguTweaks custom override. + if not customTooltip then + _, _, customTooltip = string.find(line, "%-%-showtooltip%s+(.+)") + end + + -- Keep text parsing as a fallback for old/stale macro caches and /pfcast. + if not fallback then + _, _, fallback = string.find(line, "^/cast%s+(.+)") + end + if not fallback then + _, _, fallback = string.find(line, "^/pfcast%s+(.+)") + end + if not fallback then + _, _, fallback = string.find(line, "CastSpellByName%(%\"(.+)%\"%)") + end + end + + if customTooltip then + return ParseSpellText(customTooltip) + end + + -- ClassicAPI already resolves and caches the macro's primary spell. + -- This is the correct modern behavior for a bare "#showtooltip" line and + -- also covers /castsequence, CastSpellByName and CastSpellNoToggle. + if macroSlot then + local spell, rank = API.GetMacroSpell(macroSlot) + if spell then + return spell, rank ~= "" and rank or nil + end + end + + return ParseSpellText(fallback) + end + + local function ButtonMacroScan(bar, macroCache) + if not bar:IsVisible() then return end + + local prefix = bar:GetName() + prefix = bar == MainMenuBar and "Action" or prefix + prefix = bar == BonusActionBarFrame and "BonusAction" or prefix + + for index = 1, 12 do + local button = _G[prefix.."Button"..index] + local icon = _G[prefix.."Button"..index.."Icon"] + if not button then break end + + local actionSlot = ActionButton_GetPagedID(button) + local texture = GetActionTexture(actionSlot) + button.spellslot, button.booktype = nil, nil + + local macroSlot, body = GetMacroData(actionSlot, macroCache) + local spell, rank = ResolveSpell(body, macroSlot) + if spell then + button.spellslot, button.booktype = libspell.GetSpellIndex(spell, rank) + + if button.spellslot and button.booktype then + texture = GetSpellTexture(button.spellslot, button.booktype) or texture + end + end + + if icon and texture and texture ~= icon:GetTexture() then + icon:SetTexture(texture) + end + end + end + + local bars = { + MainMenuBar, BonusActionBarFrame, MultiBarRight, MultiBarLeft, + MultiBarBottomRight, MultiBarBottomLeft + } + + local macroicons = CreateFrame("Frame", "ShaguTweaksMacroIcons", UIParent) + macroicons:RegisterEvent("PLAYER_ENTERING_WORLD") + macroicons:RegisterEvent("ACTIONBAR_SLOT_CHANGED") + macroicons:RegisterEvent("ACTIONBAR_PAGE_CHANGED") + macroicons:RegisterEvent("UPDATE_BONUS_ACTIONBAR") + macroicons:RegisterEvent("ACTIONBAR_SHOWGRID") + macroicons:RegisterEvent("UPDATE_MACROS") + macroicons:SetScript("OnEvent", function() + local macroCache = BuildMacroCache() + for _, bar in ipairs(bars) do + ButtonMacroScan(bar, macroCache) + end + end) +end diff --git a/mods/macro-tweaks.lua b/mods/macro-tweaks.lua new file mode 100755 index 0000000..fee3257 --- /dev/null +++ b/mods/macro-tweaks.lua @@ -0,0 +1,160 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Macro Tweaks"], + description = T["Add /equip, /use and modern ClassicAPI macro commands, remove #showtooltip from chat and hide macro commands from history."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + maintainer = "@shagu (GitHub)", + category = T["Macro"], + enabled = true, +}) + +module.enable = function(self) + -- This interception is intentional: #showtooltip is macro metadata and must + -- not be sent as chat text by the Vanilla macro parser. + local hookSendChatMessage = _G.SendChatMessage + local function IsShowTooltipMetadata(msg) + if not msg then return false end + + local line = string.gsub(msg, "^%s+", "") + line = string.gsub(line, "%s+$", "") + + return line == "#showtooltip" + or string.find(line, "^#showtooltip%s+") ~= nil + end + + function _G.SendChatMessage(msg, a1, a2, a3, a4, a5, a6, a7, a8) + if IsShowTooltipMetadata(msg) then return end + return hookSendChatMessage(msg, a1, a2, a3, a4, a5, a6, a7, a8) + end + + -- do not write executed macro commands into chat input history + if not ChatFrameEditBox._AddHistoryLine then + local userinput + + ChatFrameEditBox._AddHistoryLine = ChatFrameEditBox.AddHistoryLine + ChatFrameEditBox.AddHistoryLine = function(self, text) + if not userinput and text then + if string.find(text, "^/run%s*") then return end + if string.find(text, "^/script%s*") then return end + if string.find(text, "^/cast%s*") then return end + if string.find(text, "^/startattack%s*") then return end + if string.find(text, "^/stopattack%s*") then return end + if string.find(text, "^/focus%s*") then return end + if string.find(text, "^/clearfocus%s*") then return end + end + ChatFrameEditBox._AddHistoryLine(self, text) + end + + local OnEnter = ChatFrameEditBox:GetScript("OnEnterPressed") + ChatFrameEditBox:SetScript("OnEnterPressed", function(a1,a2,a3,a4) + userinput = true + if OnEnter then OnEnter(a1,a2,a3,a4) end + userinput = nil + end) + end + + local function GetBagItemName(bag, slot) + local itemID = API.GetContainerItemID(bag, slot) + local name = itemID and API.GetItemNameByID(itemID) + if name then return name end + + local itemLink = API.GetContainerItemLink(bag, slot) + local linkedID = itemLink and API.GetItemIDFromLink(itemLink) + local itemName = linkedID and API.GetItemInfo(linkedID) + return itemName + end + + local function FindItem(item) + local wanted = string.lower(item or "") + for bag = 4, 0, -1 do + for slot = 1, GetContainerNumSlots(bag) do + local query = GetBagItemName(bag, slot) + if query and string.lower(query) == wanted then + return bag, slot + end + end + end + end + + -- ClassicAPI exposes several modern macro primitives as Lua functions but + -- Vanilla does not register their familiar slash-command equivalents. + -- + -- Use ShaguTweaks-specific command IDs instead of generic STARTATTACK/FOCUS + -- keys. A SlashCmdList key can exist without the corresponding /command + -- alias, so testing only SlashCmdList.STARTATTACK can silently skip a valid + -- registration and leave Vanilla reporting "Type '/help'...". + local function TrimUnit(msg) + if not msg then return nil end + local _, _, unit = string.find(msg, "^%s*(.-)%s*$") + return unit ~= "" and unit or nil + end + + local function IsSlashRegistered(command) + command = string.lower(command) + for key, value in pairs(_G) do + if type(key) == "string" + and string.find(key, "^SLASH_") + and type(value) == "string" + and string.lower(value) == command then + return true + end + end + return false + end + + local function RegisterSlashAlias(id, command, handler) + if IsSlashRegistered(command) then return end + _G["SLASH_" .. id .. "1"] = command + _G.SlashCmdList[id] = handler + end + + RegisterSlashAlias("SHAGUTWEAKS_STARTATTACK", "/startattack", function(msg) + API.StartAttack(TrimUnit(msg)) + end) + + RegisterSlashAlias("SHAGUTWEAKS_STOPATTACK", "/stopattack", function() + API.StopAttack() + end) + + RegisterSlashAlias("SHAGUTWEAKS_FOCUS", "/focus", function(msg) + API.FocusUnit(TrimUnit(msg)) + end) + + RegisterSlashAlias("SHAGUTWEAKS_CLEARFOCUS", "/clearfocus", function() + API.ClearFocus() + end) + + local function EquipOrUse(msg) + if not msg or msg == "" then return end + + local bag, slot + local _, _, parsedBag, parsedSlot = string.find(msg, "^(%d+)%s+(%d+)$") + if parsedBag and parsedSlot then + bag, slot = parsedBag, parsedSlot + else + local _, _, inventorySlot = string.find(msg, "^(%d+)$") + if inventorySlot then + slot = inventorySlot + else + bag, slot = FindItem(msg) + end + end + + bag = bag and tonumber(bag) or nil + slot = slot and tonumber(slot) or nil + + if bag and slot then + UseContainerItem(bag, slot) + elseif slot then + UseInventoryItem(slot) + end + end + + -- Keep the original Macro Tweaks conveniences, but do not steal /equip or + -- /use from another addon that already registered either command. + RegisterSlashAlias("SHAGUTWEAKS_EQUIP", "/equip", EquipOrUse) + RegisterSlashAlias("SHAGUTWEAKS_USE", "/use", EquipOrUse) +end diff --git a/mods/metric-range.lua b/mods/metric-range.lua new file mode 100755 index 0000000..1860664 --- /dev/null +++ b/mods/metric-range.lua @@ -0,0 +1,78 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local HookScript = ShaguTweaks.HookScript + +local module = ShaguTweaks:register({ + title = T["Metric Range"], + description = T["Displays tooltip range units in metres instead of yards."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Tooltip & Items"], + enabled = true, +}) + +module.enable = function(self) + -- Avoid double processing while the old standalone addon is still enabled. + if IsAddOnLoaded("MetricRange") then return end + + local function ConvertText(text) + if type(text) ~= "string" then return text end + + -- Intentionally keep the original behavior: only replace the displayed + -- unit label. Do not numerically convert the range value. + text = string.gsub(text, " yards", " metres") + text = string.gsub(text, " yard", " metre") + text = string.gsub(text, " yd", " m") + return text + end + + local function ConvertTooltip(tooltip) + if not tooltip or not tooltip.GetName then return end + + local name = tooltip:GetName() + if not name then return end + + -- GameTooltip-style frames expose NumLines() in Vanilla. Only inspect the + -- lines that actually exist instead of probing 30 left/right font strings + -- on every tooltip show. Keep the historical 30-line ceiling as fallback. + local lines = 30 + if tooltip.NumLines then + lines = math.min(tooltip:NumLines() or 0, 30) + end + + for i = 1, lines do + local left = _G[name .. "TextLeft" .. i] + if left then + local old = left:GetText() + if old then + local new = ConvertText(old) + if new ~= old then left:SetText(new) end + end + end + + local right = _G[name .. "TextRight" .. i] + if right then + local old = right:GetText() + if old then + local new = ConvertText(old) + if new ~= old then right:SetText(new) end + end + end + end + end + + local function HookTooltip(tooltip) + if not tooltip then return end + HookScript(tooltip, "OnShow", function() + ConvertTooltip(tooltip) + end) + end + + HookTooltip(GameTooltip) + HookTooltip(ItemRefTooltip) + HookTooltip(ShoppingTooltip1) + HookTooltip(ShoppingTooltip2) + HookTooltip(ShoppingTooltip3) + + if GameTooltip and GameTooltip:IsVisible() then ConvertTooltip(GameTooltip) end + if ItemRefTooltip and ItemRefTooltip:IsVisible() then ConvertTooltip(ItemRefTooltip) end +end diff --git a/mods/minimap-framerate-latency.lua b/mods/minimap-framerate-latency.lua new file mode 100755 index 0000000..3299cf9 --- /dev/null +++ b/mods/minimap-framerate-latency.lua @@ -0,0 +1,162 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local floor = math.floor + +local module = ShaguTweaks:register({ + title = T["MiniMap Framerate & Latency"], + description = T["Adds a small framerate & latency display to the mini map."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Minimap & World Map"], + enabled = nil, +}) + +local TARGET_FPS = 60 +local LOW_LATENCY = tonumber(_G.PERFORMANCEBAR_LOW_LATENCY) or 300 +local MEDIUM_LATENCY = tonumber(_G.PERFORMANCEBAR_MEDIUM_LATENCY) or 600 + +local function CreateStatusFrame(width) + local frame = CreateFrame("Frame", nil, Minimap) + frame:SetFrameLevel(64) + frame:SetWidth(width) + frame:SetHeight(23) + frame:SetClampedToScreen(true) + frame:EnableMouse(true) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 8, + edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropBorderColor(.9, .8, .5, 1) + frame:SetBackdropColor(.4, .4, .4, 1) + + frame.text = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + frame.text:SetFont(STANDARD_TEXT_FONT, 12, "OUTLINE") + frame.text:SetAllPoints(frame) + frame.text:SetFontObject(GameFontWhite) + + return frame +end + +local function SetFPSColor(text, fps) + local perc = fps / TARGET_FPS + if perc < 0 then perc = 0 elseif perc > 1 then perc = 1 end + + local r, g + if perc <= .5 then + r = 1 + g = perc * 2 + else + r = (1 - perc) * 2 + g = 1 + end + + text:SetTextColor(r, g, 0, 1) +end + +local function SetLatencyColor(text, latency) + if latency > MEDIUM_LATENCY then + text:SetTextColor(1, 0, 0, 1) + elseif latency > LOW_LATENCY then + text:SetTextColor(1, 1, 0, 1) + else + text:SetTextColor(0, 1, 0, 1) + end +end + +module.enable = function(self) + if self.ticker then + self.ticker:Cancel() + self.ticker = nil + end + + -- Extras is ClassicAPI-first. Do not restore the original permanent + -- OnUpdate implementation when the native scheduler is unavailable. + if type(_G.C_Timer) ~= "table" or type(_G.C_Timer.NewTicker) ~= "function" then + return + end + + if not self.fpsFrame then + self.fpsFrame = CreateStatusFrame(49) + self.latencyFrame = CreateStatusFrame(51) + end + + local fpsFrame = self.fpsFrame + local latencyFrame = self.latencyFrame + local lowFPS, highFPS + local lowMS, highMS + + -- Follow the existing ShaguTweaks MiniMap Clock instead of independently + -- anchoring to MinimapCluster. MiniMap Square and mover integrations already + -- reposition that clock, so these counters follow automatically regardless + -- of module initialization order. + fpsFrame:ClearAllPoints() + latencyFrame:ClearAllPoints() + + if _G.MinimapClock then + fpsFrame:SetPoint("RIGHT", _G.MinimapClock, "LEFT", 2, 0) + latencyFrame:SetPoint("LEFT", _G.MinimapClock, "RIGHT", -2, 0) + else + fpsFrame:SetPoint("BOTTOM", Minimap, "BOTTOM", -28, -25) + latencyFrame:SetPoint("BOTTOM", Minimap, "BOTTOM", 28, -25) + end + + local function Update() + local fps = floor(GetFramerate() or 0) + local _, _, latency = GetNetStats() + latency = floor(latency or 0) + + if fps > 0 and (not lowFPS or fps < lowFPS) then lowFPS = fps end + if not highFPS or fps > highFPS then highFPS = fps end + if latency > 0 and (not lowMS or latency < lowMS) then lowMS = latency end + if not highMS or latency > highMS then highMS = latency end + + fpsFrame.text:SetText(tostring(fps)) + latencyFrame.text:SetText(tostring(latency)) + SetFPSColor(fpsFrame.text, fps) + SetLatencyColor(latencyFrame.text, latency) + end + + fpsFrame:SetScript("OnEnter", function() + GameTooltip:ClearLines() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") + GameTooltip:AddLine(T["Framerate"]) + GameTooltip:AddDoubleLine(T["High"], tostring(highFPS or 0) .. " " .. T["fps"], 1,1,1,1,1,1) + GameTooltip:AddDoubleLine(T["Low"], tostring(lowFPS or 0) .. " " .. T["fps"], 1,1,1,1,1,1) + GameTooltip:Show() + end) + + latencyFrame:SetScript("OnEnter", function() + GameTooltip:ClearLines() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") + GameTooltip:AddLine(T["Latency"]) + GameTooltip:AddDoubleLine(T["High"], tostring(highMS or 0) .. " " .. T["ms"], 1,1,1,1,1,1) + GameTooltip:AddDoubleLine(T["Low"], tostring(lowMS or 0) .. " " .. T["ms"], 1,1,1,1,1,1) + GameTooltip:Show() + end) + + fpsFrame:SetScript("OnLeave", function() GameTooltip:Hide() end) + latencyFrame:SetScript("OnLeave", function() GameTooltip:Hide() end) + + fpsFrame:Show() + latencyFrame:Show() + Update() + + -- One native ClassicAPI ticker services both displays. Lua only wakes once + -- per second instead of running two permanent OnUpdate handlers every frame. + self.ticker = _G.C_Timer.NewTicker(1, Update) +end + +module.disable = function(self) + if self.ticker then + self.ticker:Cancel() + self.ticker = nil + end + + if self.fpsFrame then self.fpsFrame:Hide() end + if self.latencyFrame then self.latencyFrame:Hide() end +end diff --git a/mods/reduced-actionbar-bags.lua b/mods/reduced-actionbar-bags.lua new file mode 100755 index 0000000..e11d49b --- /dev/null +++ b/mods/reduced-actionbar-bags.lua @@ -0,0 +1,126 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Show Bags"], + description = T["Shows bag and keyring buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the bag bar."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + maintainer = "@shagu (GitHub)", + enabled = nil, + config = { + ["panelbag.scale"] = 1, + } +}) + +module.enable = function(self) + -- The ShaguTweaks loader initializes module config while iterating an + -- unordered table. On a fresh install the Reduced Actionbar key may not have + -- been visited yet, so fall back to that module's declared default. + local reducedTitle = T["Reduced Actionbar Size"] + local reducedEnabled = ShaguTweaks_config and ShaguTweaks_config[reducedTitle] + if reducedEnabled == nil then + local reducedModule = ShaguTweaks.mods and ShaguTweaks.mods[reducedTitle] + reducedEnabled = reducedModule and reducedModule.enabled and 1 or 0 + end + if reducedEnabled ~= 1 then return end + + -- Moving the bag bar is driven by ClassicAPI modifier events and native + -- Region:IsMouseOver state. Do not keep a permanent polling fallback here. + if not API.modifierstate or not API.regionmouseover then return end + + local frames = { + KeyRingButton, CharacterBag3Slot, CharacterBag2Slot, CharacterBag1Slot, + CharacterBag0Slot, MainMenuBarBackpackButton, + } + + local bagframe = CreateFrame("Button", "ShaguTweaksReducedActionBarBags", UIParent) + bagframe:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -8, 48) + bagframe:SetWidth(180) + bagframe:SetHeight(42) + bagframe:SetScale(module.config["panelbag.scale"]) + + bagframe:SetFrameStrata("MEDIUM") + bagframe:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + + bagframe:SetBackdropBorderColor(.9,.8,.5,1) + bagframe:SetBackdropColor(.4,.4,.4,1) + + bagframe:SetClampedToScreen(true) + bagframe:SetMovable(true) + bagframe:EnableMouse(true) + bagframe:RegisterForDrag("LeftButton") + + bagframe:SetUserPlaced(true) + + bagframe:RegisterEvent("PLAYER_ENTERING_WORLD") + + local function UpdateMoveMode() + local active = API.IsMouseOver(bagframe) + and API.IsShiftKeyDown() + and API.IsControlKeyDown() + + if active then + if bagframe.mousedisabled then return end + bagframe.mousedisabled = true + for _, frame in ipairs(frames) do + frame:EnableMouse(0) + end + else + if not bagframe.mousedisabled then return end + bagframe.mousedisabled = false + for _, frame in ipairs(frames) do + frame:EnableMouse(1) + end + end + end + + bagframe:SetScript("OnEnter", UpdateMoveMode) + bagframe:SetScript("OnLeave", UpdateMoveMode) + + local modifier = CreateFrame("Frame", nil, bagframe) + modifier:RegisterEvent("MODIFIER_STATE_CHANGED") + modifier:SetScript("OnEvent", UpdateMoveMode) + + bagframe:SetScript("OnDragStart", function() + local shift = API.IsShiftKeyDown() + local control = API.IsControlKeyDown() + if not shift or not control then return end + this:StartMoving() + end) + + bagframe:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + end) + + bagframe:SetScript("OnEvent", function() + if this.initialized then return end + this.initialized = true + + ShaguTweaks.DarkenFrame(bagframe) + + for id, frame in ipairs(frames) do + local anchor = frames[id-1] or bagframe + frame:ClearAllPoints() + frame:SetPoint("LEFT", anchor, id == 1 and "LEFT" or "RIGHT", id == 1 and 5 or 2, 0) + frame:SetParent(bagframe) + frame:SetScale(.8) + frame.Show = nil + frame:Show() + + -- Child bag buttons normally own the mouse. Re-evaluate move mode when + -- entering or leaving one so Ctrl+Shift already held before mouseover is + -- handled without polling and without replacing their tooltip scripts. + ShaguTweaks.HookScript(frame, "OnEnter", UpdateMoveMode) + ShaguTweaks.HookScript(frame, "OnLeave", UpdateMoveMode) + end + + UpdateMoveMode() + end) +end diff --git a/mods/reduced-actionbar-micromenu.lua b/mods/reduced-actionbar-micromenu.lua new file mode 100755 index 0000000..79991b4 --- /dev/null +++ b/mods/reduced-actionbar-micromenu.lua @@ -0,0 +1,126 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Show Micro Menu"], + description = T["Shows micro menu buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the micro menu."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + maintainer = "@shagu (GitHub)", + enabled = nil, + config = { + ["panelmicro.scale"] = 1, + } +}) + +module.enable = function(self) + -- The ShaguTweaks loader initializes module config while iterating an + -- unordered table. On a fresh install the Reduced Actionbar key may not have + -- been visited yet, so fall back to that module's declared default. + local reducedTitle = T["Reduced Actionbar Size"] + local reducedEnabled = ShaguTweaks_config and ShaguTweaks_config[reducedTitle] + if reducedEnabled == nil then + local reducedModule = ShaguTweaks.mods and ShaguTweaks.mods[reducedTitle] + reducedEnabled = reducedModule and reducedModule.enabled and 1 or 0 + end + if reducedEnabled ~= 1 then return end + + -- Moving the micro menu is driven by ClassicAPI modifier events and native + -- Region:IsMouseOver state. Do not keep a permanent polling fallback here. + if not API.modifierstate or not API.regionmouseover then return end + + local frames = { + CharacterMicroButton, SpellbookMicroButton, TalentMicroButton, + QuestLogMicroButton, MainMenuMicroButton, SocialsMicroButton, + WorldMapMicroButton, HelpMicroButton + } + + local microframe = CreateFrame("Button", "ShaguTweaksReducedActionBarMicroMenu", UIParent) + microframe:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -8, 8) + microframe:SetWidth(225) + microframe:SetHeight(44) + microframe:SetScale(module.config["panelmicro.scale"]) + + microframe:SetFrameStrata("MEDIUM") + microframe:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + + microframe:SetBackdropBorderColor(.9,.8,.5,1) + microframe:SetBackdropColor(.4,.4,.4,1) + + microframe:SetClampedToScreen(true) + microframe:SetMovable(true) + microframe:EnableMouse(true) + microframe:RegisterForDrag("LeftButton") + + microframe:SetUserPlaced(true) + + microframe:RegisterEvent("PLAYER_ENTERING_WORLD") + + local function UpdateMoveMode() + local active = API.IsMouseOver(microframe) + and API.IsShiftKeyDown() + and API.IsControlKeyDown() + + if active then + if microframe.mousedisabled then return end + microframe.mousedisabled = true + for _, frame in ipairs(frames) do + frame:EnableMouse(0) + end + else + if not microframe.mousedisabled then return end + microframe.mousedisabled = false + for _, frame in ipairs(frames) do + frame:EnableMouse(1) + end + end + end + + microframe:SetScript("OnEnter", UpdateMoveMode) + microframe:SetScript("OnLeave", UpdateMoveMode) + + local modifier = CreateFrame("Frame", nil, microframe) + modifier:RegisterEvent("MODIFIER_STATE_CHANGED") + modifier:SetScript("OnEvent", UpdateMoveMode) + + microframe:SetScript("OnDragStart", function() + local shift = API.IsShiftKeyDown() + local control = API.IsControlKeyDown() + if not shift or not control then return end + this:StartMoving() + end) + + microframe:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + end) + + microframe:SetScript("OnEvent", function() + if this.initialized then return end + this.initialized = true + + ShaguTweaks.DarkenFrame(microframe) + + for id, frame in ipairs(frames) do + local anchor = frames[id-1] or microframe + frame:ClearAllPoints() + frame:SetPoint("LEFT", anchor, id == 1 and "LEFT" or "RIGHT", id == 1 and 3.5 or -2, id==1 and 10 or 0) + frame:SetParent(microframe) + frame.Show = nil + frame:Show() + + -- Child micro buttons normally own the mouse. Re-evaluate move mode when + -- entering or leaving one so Ctrl+Shift already held before mouseover is + -- handled without polling and without replacing their tooltip scripts. + ShaguTweaks.HookScript(frame, "OnEnter", UpdateMoveMode) + ShaguTweaks.HookScript(frame, "OnLeave", UpdateMoveMode) + end + + UpdateMoveMode() + end) +end diff --git a/mods/unitframes-abbrev-names.lua b/mods/unitframes-abbrev-names.lua new file mode 100755 index 0000000..a06a696 --- /dev/null +++ b/mods/unitframes-abbrev-names.lua @@ -0,0 +1,147 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Unit Frame Abbreviated Names"], + description = T["Abbreviates long target and target-of-target names."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Unit Frames"], + enabled = nil, +}) + +module.enable = function(self) + local maxLength = 15 + local cache = {} + + local function AbbrevWord(word) + return string.sub(word, 1, 1) .. ". " + end + + local function GetShortName(unit) + local name = UnitName(unit) + if not name then + cache[unit] = nil + return + end + + local cached = cache[unit] + if cached and cached.raw == name then + return cached.short + end + + local short = name + + if strlen(short) > maxLength then + short = string.gsub(short, "^(%S+) ", AbbrevWord) + end + + if strlen(short) > maxLength then + short = string.gsub(short, "(%S+) ", AbbrevWord) + end + + cache[unit] = { + raw = name, + short = short, + } + + return short + end + + local function GetNameText(frame) + if not frame then return end + if frame.name then return frame.name end + + if frame.GetName then + local frameName = frame:GetName() + if frameName then + return _G[frameName .. "Name"] + end + end + end + + local function UpdateFrame(frame, unit) + local text = GetNameText(frame) + local name = GetShortName(unit) + if not text or not name then return end + + -- Avoid forcing a FontString update when the displayed value is already + -- correct. This matters most for the 250 ms target-of-target fallback. + if not text.GetText or text:GetText() ~= name then + text:SetText(name) + end + end + + local function UpdateTarget() + UpdateFrame(TargetFrame, "target") + end + + local function UpdateTargetTarget() + if TargetofTargetFrame then + UpdateFrame(TargetofTargetFrame, "targettarget") + end + end + + local function EventValid(name) + return API.eventutils + and _G.C_EventUtils + and _G.C_EventUtils.IsEventValid + and _G.C_EventUtils.IsEventValid(name) + end + + local events = CreateFrame("Frame") + events:RegisterEvent("PLAYER_ENTERING_WORLD") + events:RegisterEvent("PLAYER_TARGET_CHANGED") + + local hasUnitTarget = EventValid("UNIT_TARGET") + local hasUnitName = EventValid("UNIT_NAME_UPDATE") + + if hasUnitTarget then events:RegisterEvent("UNIT_TARGET") end + if hasUnitName then events:RegisterEvent("UNIT_NAME_UPDATE") end + + events:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_TARGET_CHANGED" then + UpdateTarget() + UpdateTargetTarget() + elseif event == "UNIT_TARGET" then + if arg1 == "target" then UpdateTargetTarget() end + elseif event == "UNIT_NAME_UPDATE" then + if arg1 == "target" then + UpdateTarget() + elseif arg1 == "targettarget" then + UpdateTargetTarget() + end + end + end) + + -- Vanilla has no UNIT_TARGET event for target-of-target changes. Prefer the + -- ClassicAPI native ticker so Lua only runs at the required 250 ms cadence, + -- instead of entering a Lua OnUpdate handler every rendered frame. Keep the + -- old throttled path only for older environments without C_Timer.NewTicker. + if not hasUnitTarget then + local function UpdateTargetTargetFallback() + if UnitName("targettarget") then + UpdateTargetTarget() + end + end + + if _G.C_Timer and type(_G.C_Timer.NewTicker) == "function" then + _G.C_Timer.NewTicker(.25, UpdateTargetTargetFallback) + else + local fallback = CreateFrame("Frame") + fallback.elapsed = 0 + + fallback:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + (arg1 or 0) + if this.elapsed < .25 then return end + this.elapsed = 0 + UpdateTargetTargetFallback() + end) + end + end + + UpdateTarget() + UpdateTargetTarget() +end diff --git a/mods/world-chat-hider.lua b/mods/world-chat-hider.lua new file mode 100755 index 0000000..10d41e7 --- /dev/null +++ b/mods/world-chat-hider.lua @@ -0,0 +1,59 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T +local Filter = ShaguTweaks.ExtrasChatFilter + +local module = ShaguTweaks:register({ + title = T["World Chat Hider"], + description = T["Hides World channel messages while inside an instance without leaving the channel."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Chat & Social"], + enabled = nil, +}) + +local function IsInsideInstance() + if type(IsInInstance) ~= "function" then return false end + + local inside = IsInInstance() + return inside == 1 or inside == true +end + +local function IsWorldChannel() + -- Vanilla 1.12 CHAT_MSG_CHANNEL exposes the base channel name in arg9. + -- Custom channel names are not localized, so comparing the actual event is + -- both cheaper and safer than querying GetChannelName("world") every time. + if arg9 and string.lower(arg9) == "world" then + return true + end + + -- Conservative fallback for clients that omit arg9: arg4 is the numbered + -- channel label (for example "5. World"). + if arg4 then + local full = string.lower(arg4) + if full == "world" then return true end + + local _, _, base = string.find(full, "^%d+%.%s*(.+)$") + if base == "world" then return true end + end + + return false +end + +local function FilterWorldChat(event) + if event ~= "CHAT_MSG_CHANNEL" then return false end + if not IsInsideInstance() then return false end + + return IsWorldChannel() +end + +module.enable = function(self) + if Filter then + Filter:Register("WorldChatHider", FilterWorldChat) + end +end + +module.disable = function(self) + if Filter then + Filter:Unregister("WorldChatHider") + end +end diff --git a/mods/worldmap-reveal.lua b/mods/worldmap-reveal.lua new file mode 100755 index 0000000..a19957a --- /dev/null +++ b/mods/worldmap-reveal.lua @@ -0,0 +1,160 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API +local hooksecurefunc = ShaguTweaks.hooksecurefunc + +local module = ShaguTweaks:register({ + title = T["Reveal World Map"], + description = T["Reveals unexplored world map areas and shows exploration hints."], + expansions = { ["vanilla"] = true, ["tbc"] = false }, + category = T["Minimap & World Map"], + maintainer = "@shagu (GitHub)", + enabled = true, + config = { + ["map.reveal.marker"] = "on", + } +}) + +module.enable = function(self) + -- do not load if another map addon is controlling the world map + if Cartographer then return end + if METAMAP_TITLE then return end + + local enabled = true + local overlayTextures = {} + local exploreFrames = {} + + local mapreveal = {} + mapreveal.onmap = CreateFrame("CheckButton", "shagutweaks_mapreveal_onmap", WorldMapFrame, "UICheckButtonTemplate") + mapreveal.onmap.text = _G["shagutweaks_mapreveal_onmapText"] + mapreveal.onmap:SetWidth(14) + mapreveal.onmap:SetHeight(14) + mapreveal.onmap:SetPoint("TOPLEFT", WorldMapButton, "TOPLEFT", 1, 19) + mapreveal.onmap.text:SetPoint("LEFT", mapreveal.onmap, "RIGHT", 2, 0) + mapreveal.onmap.text:SetText(T["Reveal Unexplored"]) + + mapreveal.onmap:SetScript("OnShow", function() + this:SetChecked(enabled) + end) + + mapreveal.onmap:SetScript("OnClick", function() + enabled = this:GetChecked() and true or false + WorldMapFrame_Update() + end) + + local function HideRevealLayer() + for _, texture in ipairs(overlayTextures) do + texture:Hide() + end + + for _, frame in ipairs(exploreFrames) do + frame:Hide() + end + end + + local function ExploreEnter() + WorldMapTooltip:ClearLines() + WorldMapTooltip:SetOwner(this, "ANCHOR_TOP") + WorldMapTooltip:AddLine(T["Exploration Point"]..":", .3, 1, .8) + WorldMapTooltip:AddLine(this.name or "?", 1, 1, 1) + WorldMapTooltip:Show() + end + + local function ExploreLeave() + WorldMapTooltip:Hide() + end + + local function GetExploreCenter(overlay) + if overlay.hitRectLeft and overlay.hitRectRight + and overlay.hitRectTop and overlay.hitRectBottom then + return (overlay.hitRectLeft + overlay.hitRectRight) / 2, + (overlay.hitRectTop + overlay.hitRectBottom) / 2 + end + + if overlay.offsetX and overlay.offsetY + and overlay.textureWidth and overlay.textureHeight then + return overlay.offsetX + overlay.textureWidth / 2, + overlay.offsetY + overlay.textureHeight / 2 + end + end + + local function UpdateRevealLayer() + HideRevealLayer() + if not enabled then return end + + -- ClassicAPI reads WorldMapOverlay.dbc directly from the active client, + -- including custom Turtle-like zones and their real tile layout. + local overlays = API.GetUnexploredMapTextures() + if not overlays then return end + + local textureIndex = 0 + local exploreIndex = 0 + + for _, overlay in ipairs(overlays) do + if overlay.tiles then + for _, tile in ipairs(overlay.tiles) do + if tile.file and tile.width and tile.height + and tile.offsetX and tile.offsetY then + textureIndex = textureIndex + 1 + + local texture = overlayTextures[textureIndex] + if not texture then + texture = WorldMapDetailFrame:CreateTexture(nil, "ARTWORK") + overlayTextures[textureIndex] = texture + end + + texture:ClearAllPoints() + texture:SetWidth(tile.width) + texture:SetHeight(tile.height) + texture:SetTexCoord(0, tile.texCoordX or 1, 0, tile.texCoordY or 1) + texture:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", tile.offsetX, -tile.offsetY) + texture:SetTexture(tile.file) + texture:SetVertexColor(.4, .4, .4, 1) + texture:Show() + end + end + end + + if self.config["map.reveal.marker"] == "on" then + local x, y = GetExploreCenter(overlay) + if x and y then + exploreIndex = exploreIndex + 1 + + local explore = exploreFrames[exploreIndex] + if not explore then + explore = CreateFrame("Frame", nil, WorldMapDetailFrame) + explore:SetWidth(16) + explore:SetHeight(16) + explore:EnableMouse(true) + explore:SetFrameLevel(255) + explore:SetScript("OnEnter", ExploreEnter) + explore:SetScript("OnLeave", ExploreLeave) + + explore.tex = explore:CreateTexture(nil, "OVERLAY") + explore.tex:SetTexture("Interface\\WorldMap\\WorldMap-MagnifyingGlass") + explore.tex:SetBlendMode("ADD") + explore.tex:SetTexCoord(.08, .92, .08, .92) + explore.tex:SetAllPoints() + + exploreFrames[exploreIndex] = explore + end + + explore:ClearAllPoints() + explore:SetPoint("CENTER", WorldMapDetailFrame, "TOPLEFT", x, -y) + explore.name = API.GetAreaInfo(overlay.areaID) + or overlay.textureName + or "?" + explore:Show() + end + end + end + end + + -- Keep Blizzard's normal explored-map rendering untouched and append only + -- ClassicAPI's unexplored overlays after the native update. + hooksecurefunc("WorldMapFrame_Update", UpdateRevealLayer) + + if WorldMapFrame:IsShown() then + UpdateRevealLayer() + end +end