From 2c58ce2aa3d8713edc0845450b337e307cbb18d7 Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Mon, 2 Nov 2026 17:42:46 -0400 Subject: [PATCH] Merge the ShaguTweaks Extras ClassicAPI modules into ShaguTweaks (3.7) Bring in 27 module files from Dusk/ShaguTweaks-Extras-ClassicAPI (8e14bb4): action bar, chat, loot, macro, merchant (Buy Em All), minimap & world map, tooltip and unit frame modules, with their textures. Titles are unchanged, so saved toggles carry over. - Categories: Chat -> Chat & Social, World Map -> Minimap & World Map, Merchant & Auction -> Merchant; Macro is a new category. - Texture paths point at ShaguTweaks\img. - Not merged: Raid Frames, Bag Item Click and Bag Search Bar (Bagshui covers them), Auction Alt-Buy (aux covers it). - Chat note when the old ShaguTweaks-extras addon is still enabled. - English translation keys, notices, license copy, asset provenance, README, changelog and version 3.7. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7 --- CHANGELOG.md | 25 + Docs/ASSET_PROVENANCE.md | 4 + LICENSES/README.md | 3 + LICENSES/ShaguTweaks-extras-MIT.txt | 21 + README.md | 56 +- ShaguTweaks.toc | 39 +- THIRD_PARTY_NOTICES.md | 51 ++ img/df-gryphon-beta.tga | Bin 0 -> 34945 bytes img/df-gryphon.tga | Bin 0 -> 29430 bytes img/df-wyvern.tga | Bin 0 -> 25998 bytes img/xpbar.tga | Bin 0 -> 3398 bytes locales/translations_enUS.lua | 66 +++ main.lua | 6 + mods/actionbar-df-gryphon.lua | 40 ++ mods/actionbar-float.lua | 107 ++++ mods/actionbar-hide-macro.lua | 34 ++ mods/actionbar-mouseover-bar-right.lua | 15 + mods/actionbar-mouseover-bar-right2.lua | 15 + mods/actionbar-mouseover-common.lua | 138 +++++ mods/actionbar-reagents.lua | 127 +++++ mods/actionbar-right-center.lua | 17 + mods/buy-em-all.lua | 662 ++++++++++++++++++++++++ mods/chat-filter-common.lua | 68 +++ mods/chat-input-center.lua | 45 ++ mods/chat-npc-spam.lua | 92 ++++ mods/chat-text-shadow.lua | 19 + mods/chat-timestamps.lua | 57 ++ mods/classic-snowfall.lua | 299 +++++++++++ mods/cursor-tooltip.lua | 99 ++++ mods/hide-combat-tooltip.lua | 146 ++++++ mods/loot-quality.lua | 128 +++++ mods/macro-icons.lua | 144 ++++++ mods/macro-tweaks.lua | 160 ++++++ mods/metric-range.lua | 78 +++ mods/minimap-framerate-latency.lua | 162 ++++++ mods/reduced-actionbar-bags.lua | 126 +++++ mods/reduced-actionbar-micromenu.lua | 126 +++++ mods/unitframes-abbrev-names.lua | 147 ++++++ mods/world-chat-hider.lua | 59 +++ mods/worldmap-reveal.lua | 160 ++++++ 40 files changed, 3537 insertions(+), 4 deletions(-) create mode 100755 LICENSES/ShaguTweaks-extras-MIT.txt create mode 100755 img/df-gryphon-beta.tga create mode 100755 img/df-gryphon.tga create mode 100755 img/df-wyvern.tga create mode 100755 img/xpbar.tga create mode 100755 mods/actionbar-df-gryphon.lua create mode 100755 mods/actionbar-float.lua create mode 100755 mods/actionbar-hide-macro.lua create mode 100755 mods/actionbar-mouseover-bar-right.lua create mode 100755 mods/actionbar-mouseover-bar-right2.lua create mode 100755 mods/actionbar-mouseover-common.lua create mode 100755 mods/actionbar-reagents.lua create mode 100755 mods/actionbar-right-center.lua create mode 100755 mods/buy-em-all.lua create mode 100755 mods/chat-filter-common.lua create mode 100755 mods/chat-input-center.lua create mode 100755 mods/chat-npc-spam.lua create mode 100755 mods/chat-text-shadow.lua create mode 100755 mods/chat-timestamps.lua create mode 100755 mods/classic-snowfall.lua create mode 100755 mods/cursor-tooltip.lua create mode 100755 mods/hide-combat-tooltip.lua create mode 100755 mods/loot-quality.lua create mode 100755 mods/macro-icons.lua create mode 100755 mods/macro-tweaks.lua create mode 100755 mods/metric-range.lua create mode 100755 mods/minimap-framerate-latency.lua create mode 100755 mods/reduced-actionbar-bags.lua create mode 100755 mods/reduced-actionbar-micromenu.lua create mode 100755 mods/unitframes-abbrev-names.lua create mode 100755 mods/world-chat-hider.lua create mode 100755 mods/worldmap-reveal.lua 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 0000000000000000000000000000000000000000..bb4262fe7bf811fa7fff958f7f29269b69038280 GIT binary patch literal 34945 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

YDoz?$T`BYIa{L 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 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