46 Commits
Author SHA1 Message Date
OEVG 6fc566feaf Release 3.8.7: unit frame colors and theme fixes 2026-11-08 19:32:00 -04:00
OEVG e43d93a524 Release 3.8.6: module fixes and native timer optimizations 2026-11-06 17:20:18 -04:00
OEVG b9912256bf Refresh README screenshot URL to bypass stale image caches 2026-11-06 11:44:50 -04:00
OEVG 40762969a1 Update Advanced Options screenshot for 3.8.5 2026-11-06 11:42:16 -04:00
OEVG 9101a0b32f Release 3.8.5: UI polish and initialization cleanup 2026-11-06 11:40:25 -04:00
OEVG a81ca54e0c Merge remote README updates with 3.8.4 release 2026-11-06 00:35:14 -04:00
OEVG 224f6db154 Release 3.8.4: pale-blue proc and reactive spell highlights 2026-11-06 00:34:09 -04:00
OEVG 4f0c80c58e Update README.md 2026-11-05 16:35:08 +00:00
OEVG eb9faafafd Update README.md 2026-11-05 16:32:49 +00:00
OEVG 662de4fced Update README.md 2026-11-05 16:12:13 +00:00
OEVG f393a827c7 Update README.md 2026-11-05 16:07:44 +00:00
OEVG ffa1ceb83b Update README.md 2026-11-05 16:06:32 +00:00
OEVG 8b62382660 Refine resource predictions and attached druid mana display 2026-11-05 11:50:27 -04:00
OEVG 97ff7a7d84 Document Advanced Options with a screenshot 2026-11-05 11:28:28 -04:00
OEVG 5db1aa9c41 Release 3.8.3: personal resource display and player-frame refinements 2026-11-05 11:27:15 -04:00
OEVG d51d2556ef Release 3.8.2: healing predictions and native API improvements 2026-11-04 21:39:57 -04:00
OEVG 48ee0747c3 Update Docs/EXTRAS_MERGE.md 2026-11-04 18:28:05 +00:00
OEVG dfe8cea692 Release 3.8.1: streamline ClassicAPI support and fix action bars 2026-11-04 14:03:16 -04:00
OEVG 7da9684025 Update Docs/EXTRAS_MERGE.md 2026-11-04 16:53:29 +00:00
OEVG e557dbe058 Release 3.8: player frames, movable bars and UI polish 2026-11-04 12:47:05 -04:00
OEVG 19bc58620b Release 3.7.4: add options search and polish UI compatibility 2026-11-03 17:27:54 -04:00
OEVGandClaude Opus 5.5 0784e2e5bb Golden UI: Forever-brown window title bars; mail inbox gets the send page's stationery (3.7.3 hotfix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-03 08:14:46 +00:00
OEVGandClaude Opus 5.5 767b458aa6 Add Golden UI, a gold recolor sharing Darkened UI's code (3.7.3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-03 06:04:52 +00:00
OEVGandClaude Sonnet 5.5 9ef84b50fe Swing Timer: move each bar on its own; dragging the cast bar pins the swing bars (3.7.2 hotfix)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:43:45 +00:00
OEVGandClaude Sonnet 5.5 28da226764 Cast bar move markers: use the Swing Timer's classic cast bar look
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:32:32 +00:00
OEVG b47b2ec06e Update Docs/EXTRAS_MERGE.md 2026-11-02 22:27:22 +00:00
OEVGandClaude Sonnet 5.5 8e47666644 Add Loss of Control Alert, Totem Timers, Movable Cast Bar and movable Enemy Castbars (3.7.2), Docs/EXTRAS_MERGE.md for the Extras merge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:23:04 +00:00
OEVGandClaude Sonnet 5.5 338135be34 Remove Key-Down Casting and Alt Self-Cast in favour of ClassicAPI's ActionButtonUseKeyDown (3.7.1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:02:50 +00:00
OEVGandClaude Opus 5.5 2c58ce2aa3 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 21:52:53 +00:00
OEVGandClaude Sonnet 5.5 b2c30df0e1 README: list ShaguTweaks Extras ClassicAPI as an optional requirement
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 21:04:26 +00:00
OEVGandClaude Sonnet 5.5 3c5f36b67a XP Bar Text: draw with a shadow instead of an outline so digits no longer touch
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 21:03:27 +00:00
OEVGandClaude Opus 5.5 6de8e93496 README: client-mod badges
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 14:54:06 +00:00
OEVGandClaude Opus 5.5 00c6903a81 Changelog: number each entry as a version, 3.0 to 3.6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 04:59:03 +00:00
OEVGandClaude Opus 5.5 c5b3df38a1 Version 3.6
Set ## Version to 3.6 in the toc and mark the release in the changelog
and README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 04:55:19 +00:00
OEVGandClaude Opus 5.5 15712e82c7 WorldMap Window: keep the size, drag by the title, OctoBus Transports button; add Loot History
- The map keeps the scale picked with Ctrl+Mousewheel (saved per
  character, .4 to 2). It was reset to .85 on every open and by the
  Turtle WoW maximize wrapper.
- The title bar drags the window: on Turtle the title sits 17 units above
  the frame, so the frame's click area now extends over it.
- With OctoBus loaded, a Transports button left of the Continent dropdown
  runs OctoBus' own toggle. Its window is lifted above the map while both
  are open and restored when the map closes.
- New Loot History module (/loothistory), ported from brues-code/pfUI and
  rebuilt with stock 1.12 widgets, on ClassicAPI's C_LootHistory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 04:08:31 +00:00
OEVGandClaude Opus 5.5 f3fd1d5c56 README: add Swing Timer, Wand Timer and today's other changes
The feature list now covers Swing Timer and Wand Timer (with the queue
colors and the ClassicAPI requirement), Hide Pet Unit Frame, the XP
numbers, the World Map raid colors, the bounded player cache and Range
Color's 0.2-second check. Swing Timer and Wand Timer join the list of
modules that use ClassicAPI directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 03:11:44 +00:00
OEVGandClaude Opus 5.5 0a0cd055c4 Swing Timer: color the main-hand bar for queued Heroic Strike, Cleave and Maul
Ported from pfUI's swing timer. UNIT_SPELLCAST_SENT with
C_Spell.IsNextMeleeSpell and the spell name sets the queue; it clears on
UNIT_SPELLCAST_SUCCEEDED or _FAILED, on leaving combat, or when the
ability's action button goes dark after being lit (Esc or a second press
un-queues it without an event). Heroic Strike is light blue instead of
pfUI's yellow, which would not stand out on the gold bar; Cleave green
and Maul orange as in pfUI. Out-of-range gray still wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 01:57:32 +00:00
OEVGandClaude Opus 5.5 151abf3280 Add Swing Timer and Wand Timer, ported from pfUI
Two Action Bar options, off by default, ported from brues-code/pfUI's
swing timer and restyled as classic cast bars: dark background,
UI-StatusBar fill, the cast bar border and spark, and the weapon's icon
on the left like Improved Castbar's spell icon.

- Swing Timer: main hand and off hand (shields are skipped).
- Wand Timer: wand Shoot, and Auto Shot for bows, guns and crossbows,
  red during Auto Shot's last half second.

ClassicAPI's PLAYER_SWING drives the timers and PLAYER_SWING_RANGE_UPDATE
grays a bar while the target is out of reach. Auto-attack state uses the
1.12 PLAYER_ENTER_COMBAT/PLAYER_LEAVE_COMBAT and START/STOP_AUTOREPEAT_SPELL
events, plus START/STOP_AUTOATTACK on ClassicAPI v1.18.14+. Without
PLAYER_SWING (before v1.15.10) both options do nothing.

The bars stack just above the cast bar. Holding Ctrl+Shift+Alt shows a
preview to drag; right-click resets. The position is saved per character.

pfUI's MIT license is preserved in LICENSES/pfUI-MIT.txt and credited in
THIRD_PARTY_NOTICES.md and the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 01:48:51 +00:00
OEVGandClaude Opus 5.5 a482808743 Add Hide Pet Unit Frame and show XP numbers on the XP bar
Hide Pet Unit Frame (Unit Frames, off by default) parents PetFrame to a
frame that is never shown. PetFrame_Update shows the frame again on
every pet change, so a plain Hide() would not last; with the hidden
parent the stock pet code runs unchanged and nothing draws.

XP Bar Text shows current/needed XP like the reputation bar
("1555/3000  51%"), followed by the rested bonus as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 01:25:13 +00:00
OEVGandClaude Opus 5.5 f5f651936c Enlarged Minimap: run the buff anchor check only while BuffFrame is shown
The per-frame check that keeps the buff rows clear of the enlarged
minimap now lives on a child frame of BuffFrame, so it only runs while
the stock buffs are visible. Buff addons such as VCB hide BuffFrame and
TemporaryEnchantFrame; the stock BuffFrame_Enchant_OnUpdate re-anchoring
stops with them, and the check was moving hidden frames every frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 00:45:25 +00:00
OEVGandClaude Opus 5.5 4ef534fabb Fix raid colors on the world map and drop two per-frame checks
WorldMap Class Colors assumed WorldMapRaidN shows raidN. In a raid the
map packs its icons and stores the real unit on each icon, so icons
showed other players' class colors. Use the icon's unit, or its name
for battleground team members outside the raid. The updaters are now
children of the maps, so they only run while a map is open.

Class Portraits checked the target-of-target GUID on every frame. Check
when the stock handler reports a new name, and otherwise five times a
second.

Enlarged Minimap is unchanged: the stock UI re-anchors the buff frame
every frame, so its check has to as well. Documented in
Docs/PERFORMANCE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 00:32:33 +00:00
OEVGandClaude Opus 5.5 c7d0f9c5f4 Add a changelog and link it from the README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 00:18:50 +00:00
OEVGandClaude Opus 5.5 1ae8398a2d Bound the player cache and stop per-frame range checks
- libunitscan: expire the shared player cache once per session (180 days
  for known classes, 90 for class-less, at most 10,000 players). Every
  sighting now stamps an age, so roster and mouseover entries can expire.
- libunitscan: keep a player's GUID only while the class is unknown and
  resolve such entries later with GetPlayerInfoByGUID.
- libunitscan: skip the GUID lookup for known chat speakers seen within
  the hour, throttle full guild rescans to once a minute, skip UnitGUID
  when UnitClass answered, and query ClassicAPI's player cache once per
  miss instead of twice.
- Social Colors: drop its separate 180-day cleanup.
- Range Color: use its own 0.2 s timer. Sharing Blizzard's rangeTimer ran
  the check every frame for every ranged button.
- Docs/PERFORMANCE.md: document the cache rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 00:09:59 +00:00
OEVGandClaude Sonnet 5.5 e4c7741226 Tidy README and review ClassicAPI v1.18.13 changes
- Debuff Timer: hide the native Cooldown's own countdown numbers so the
  countdownForCooldowns CVar cannot double the overlay text.
- README: merge the duplicated feature lists, match module names and
  default states to the in-game options, link the extra Docs, and state
  the recommended ClassicAPI version.
- Docs/CLASSICAPI_UPDATES.md: record the v1.17.7 to v1.18.13 review.
- .gitignore: ignore editor and merge leftovers.
- ShaguTweaks.toc: drop the Russian note from the English-only fork.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-01 00:15:11 +00:00
OEVG d705a9b221 Update README.md 2026-10-30 20:10:17 +00:00
OEVG 48d45f8fe6 Apply Patch 2026-10-30 20:06:32 +00:00
196 changed files with 16136 additions and 20670 deletions
+8
View File
@@ -0,0 +1,8 @@
* text=auto eol=lf
*.tga -text
*.blp -text
*.jpg -text
*.jpeg -text
*.png -text
*.gif -text
*.zip -text
+10
View File
@@ -0,0 +1,10 @@
*.bak
*.tmp
*.orig
*.rej
*.log
.DS_Store
Thumbs.db
desktop.ini
.vscode/
.idea/
+137
View File
@@ -0,0 +1,137 @@
# Changelog
Changes in OEVG/ShaguTweaks-ClassicAPI, newest first. Entries summarize released behavior; development iterations and test notes belong in `Docs/` and `tests/`.
Based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at `4391564` (2026-09-08). Earlier history is available upstream.
## 3.8.7 — 2026-11-08 — Unit frames and minimap buttons
- Add optional owner class colors for pet health bars, including targeted pets with identifiable owners.
- Add an option to remove the class-colored background behind your player name, including on the detached name frame.
- Apply Darkened and Golden UI tints to the Outfitter, TrinketMenu and Atlas-CFM minimap buttons.
- Preserve the Spellbook's original parchment and torn edges in Darkened and Golden UI.
## 3.8.6 — 2026-11-06 — Module fixes and native timers
- Prevent recursive merchant-click hooks when Buy Em All runs alongside another merchant addon.
- Keep the viewed world map unchanged when hovering the Turtle clock.
- Refresh reagent counts when conditional macros change spells, and apply Minimap Clean's hidden state to late addon buttons.
- Keep the dungeon-finder minimap button's size and position stable when dragging it with Enlarged Minimap enabled.
- Stop Sell Junk's completion check when sales stall, and refresh rarity borders/glows when previously uncached item data arrives.
- Use native ClassicAPI cooldown numbers when native countdowns are enabled, retaining the existing appearance and legacy fallback; remove unused static spell/debuff locale data.
- Use native target cast-bar animation and player countdown text where supported, and limit power/portrait styling updates to affected unit frames.
- Use native totem/control-alert timers where supported, and reduce repeated resource/map styling writes and chat-cache allocations.
- Keep swing-timer and cast-bar preview borders above their fills and match them to the selected UI theme.
- Remove AoE Loot and retire its saved setting. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for AutoAreaLoot.
## 3.8.5 — 2026-11-06 — UI polish and initialization cleanup
- Replace the theme checkboxes with one **UI:** dropdown below Search: Default, Darkened or Golden, preserving existing choices.
- Add a version button beside Defaults in Advanced Options, opening a copyable Gitea project link.
- Match equipped-item comparison backgrounds to the source tooltip in Darkened UI.
- Preserve native mail, quest-log and NPC quest/gossip parchment colors and torn edges in both themes, including empty quest-log artwork. Restore Golden UI's darker NPC title strip.
- Keep item-rarity borders and formatted Golden titles consistent when windows update or are recolored again.
- Consolidate deferred initialization, batch duplicate delayed recolors and bound the texture-classification cache while preserving frame exclusions.
## 3.8.4 — 2026-11-06 — Action-bar proc highlights
- Add **Proc & Reactive Spell Highlights** (Action Bar, off): event-driven pale light-blue glows for supported spells and macros, with native pulsing animation where available. Central proc alerts remain with dedicated addons such as Proc Doc.
## 3.8.3 — 2026-11-05 — Personal Resource Display
- Replace **Standalone Player Frame** with **Personal Resource Display**: independent, movable and resizable copies of player health and power, leaving the original player frame intact. Existing health/power positions migrate.
- Add **Hide Player Frame Resources** to keep only the clean portrait and independently movable class-colored name. **Hide Player Name** now requires this option and also hides the raid group number. Resource hiding and the personal display can be used independently.
- Share healing predictions and Energy & Mana Tick timing with the copied bars, including resizing, and retain the active UI border palette.
- Show a small attached mana bar for druids using Personal Resource Display while shapeshifted.
- Remove **Hide Player Portrait** and retire its saved setting.
- Condense the changelog into release summaries, removing intermediate testing history.
## 3.8.2 — 2026-11-04 — Healing predictions and native API improvements
- Add **Healing Predictions** for player and target: incoming direct heals and upcoming HoT ticks, with Big Health/standalone support. Reuse compatible Puppeteer data when available; otherwise use an observed-heal estimator and vanilla HealComm. See [prediction details and limitations](Docs/HEALING_PREDICTION_REVIEW.md).
- Use native ClassicAPI range subscriptions for **Range Color**, with balanced button subscriptions and an older-version fallback.
- Improve **Energy & Mana Tick** handling of power changes, mana-costing casts and regeneration timing; use native spark animation when available.
- Use ClassicAPI's macro icons and command parsing, retaining compatibility for legacy custom hints and other macro addons.
- Use native equipment comparisons where available, including full item links, paired equipment slots and stat differences; keep comparisons stable during Bagshui tooltip refreshes.
- Route health/power number updates separately and use native duration timers for swing/wand displays where supported.
- Remove redundant cast tracking and consolidate movement/startup helpers. Add a shared regression-test runner. See the [cast tracking review](Docs/CAST_TRACKING_REVIEW.md).
- Prevent caster-bar icons showing through empty druid form slots. Clarify that Auto Stance enters forms; ClassicAPI Auto Cancel Form leaves them.
## 3.8.1 — 2026-11-04 — ClassicAPI baseline and legacy cleanup
- Require **ClassicAPI 1.18.10+**, with a startup check and update message for unsupported runtimes.
- Remove **Auto Dismount** and its unused support code in favor of ClassicAPI's settings; retain Auto Stance for entering required forms.
- Respect **Always Show Action Bars** for empty main/bonus-bar slots.
- Fix disappearing Big Health fills after health updates in standalone mode.
- Remove the unused debuff-timing library and legacy per-frame nameplate discovery; retain optional-mod and missing-data compatibility checks.
## 3.8 — 2026-11-04 — Player frames, movable bars and UI polish
- Add **Standalone Player Frame**, detaching the native portrait, name, health and power into movable pieces, with clean rounded artwork, a class-colored name and saved positions. Add **Hide Player Portrait** and standalone-only **Hide Player Name**.
- Split **Big Health** into independent player and target options, migrating existing settings. Standalone power sizing follows the player setting.
- Include action, pet, stance, XP and reputation bar movement in **Reduced Actionbar Size**. Ctrl+Shift+Alt dragging supports saved positions, reset and snapping to visible bars.
- Make cast-bar movement always available and remove its checkbox.
- Give **Loss of Control Alert** classic artwork and borders that follow the active UI mode.
- Preserve native quest/gossip parchment colors and torn edges in Golden/Darkened UI. Golden UI NPC name headers use opaque title artwork; player names retain class styling.
- Extend Modern Class Colors to name backgrounds. Leave TrinketMenu styling to TrinketMenu.
- Remove Debuff Timer and the three nameplate options to avoid overlapping dedicated addons. See [alternatives and migration notes](Docs/EXTRAS_MERGE.md#removed-later).
## 3.7.4 — 2026-11-03 — Advanced Options search
- Add case-insensitive search by option name, description, category and slider label, retaining unsaved changes while filtering.
## 3.7.3 — 2026-11-03 — Golden UI
- Add **Golden UI**, with configurable gold frame/border colors and brown title bars while retaining default text colors. Darkened UI takes precedence when both are selected.
- Share theme handling across native windows and supported addons, including aux/AuxCraft, Bagshui, Outfitter, VCB and RollFor. Preserve parchment, item icons, slot interiors and existing quality colors.
- Extend **Item Rarity Borders** to outgoing mail/TurtleMail attachments and both sides of trade windows, refreshing on item changes and clearing empty slots. **Loot Quality** remains specific to the loot window.
- Center Tooltip Details health text within its bar.
## 3.7.2 — 2026-11-02 — Loss of Control Alert, Totem Timers and movable cast bar
- Add **Loss of Control Alert** and **Totem Timers**, using ClassicAPI events with animation only while active. Both support modifier dragging and reset.
- Add **Movable Cast Bar** and independent movement for swing, wand and enemy cast bars, with saved positions.
- Link the [Extras migration guide](Docs/EXTRAS_MERGE.md) from the README.
## 3.7.1 — 2026-11-02 — Key-Down Casting removed
- Remove **Key-Down Casting** and its dependent **Alt Self-Cast** option in favor of ClassicAPI's native settings. Use `/console ActionButtonUseKeyDown 1` for key-down activation and the native SELFCAST modified click for self-casting.
## 3.7 — 2026-11-02 — Extras modules built in
- Merge Dusk's Extras action-bar, chat, loot, macro, merchant, map, tooltip and abbreviated-name modules, preserving their saved choices. Warn when the separate Extras addon remains enabled.
- Leave raid frames, bag helpers and auction Alt-Buy to dedicated addons. See [the module list](Docs/EXTRAS_MERGE.md).
- Improve XP text legibility with a shadow instead of an outline.
## 3.6 — 2026-11-02 — Map window, Transports button and Loot History
- Add **Loot History**, using ClassicAPI's stored group-roll results, and an OctoBus **Transports** button on the world map.
- Preserve map scale across reopening/reload and allow dragging from its title bar.
## 3.5 — 2026-11-01 — Swing Timer and Wand Timer
- Add classic-style main/off-hand swing timers, queued-attack colors and ranged/wand timing with Hunter wind-up indication, based on brues-code/pfUI.
## 3.4 — 2026-11-01 — Hide Pet Unit Frame and XP numbers
- Add **Hide Pet Unit Frame** and show current/required XP, percentage and rested bonus on hover.
## 3.3 — 2026-11-01 — World map colors and per-frame checks
- Correct class colors for packed world-map raid icons and battleground teammates.
- Limit map, target-of-target portrait and enlarged-minimap checks to relevant visibility/update intervals.
## 3.2 — 2026-11-01 — Performance
- Bound and expire the shared player cache, refresh sighting ages and avoid redundant GUID/guild lookups. See [cache details](Docs/PERFORMANCE.md).
- Throttle Range Color checks and retain Blizzard's native out-of-range hotkey coloring.
## 3.1 — 2026-10-31 — ClassicAPI review and tidy-up
- Prevent duplicate Debuff Timer countdown text with ClassicAPI's native countdown enabled.
- Refresh feature documentation, compatibility notes and repository ignore rules.
## 3.0 — 2026-10-30 — OEVG customization
- Add **Modern Class Colors** and modern health/power textures, including druid mana, plus independent **White Unit Frame Numbers**.
- Place health percentage left and current health right with matching name-font styling. Limit Big Health's resting glow to the portrait.
+4
View File
@@ -33,6 +33,10 @@ hashes.
| `img/UI-TargetingFrame.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/UI-TargetingFrame.tga` | `c3c56c28ff81f99325a8aa5a8d8c5cd4a9c56a95` | | `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/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/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 ## Screenshot manifest
+46
View File
@@ -0,0 +1,46 @@
# Cast tracking review (3.8.2)
## Data ownership
The Improved Castbar and healing estimator query the normalized ClassicAPI cast
surface directly. Enemy Castbars also queries ClassicAPI first. The old libcast
copy of ClassicAPI cast events, keyed by unit name, had no built-in display
consumer. Name keys also cannot distinguish two enemies with the same name.
Removed the native-event mirror and legacy localized combat-text parser from
libcast. Ordinary non-Hunter initialization now registers no cast events or
casting hooks. Native unit queries do not create cached Lua records.
SuperWoW still supplies timed GUID records when ClassicAPI cannot supply complete
timing. Remote channels require ClassicAPI's live channel state and matching
spell ID before display, so an old timed record cannot resurrect a stopped or
different channel. A cache update refreshes the current target immediately.
The SuperWoW listener is created only when Super WoW Compatibility and Enemy
Castbars are enabled. It ignores the local player and complete native records.
It retains at most 128 remote records, prunes expired records on incoming events
at most every half second, and expires records when read. There is no cache
cleanup OnUpdate loop. Expired records may remain until another event/query,
but the cache remains bounded and expired entries cannot be displayed.
Hunter-only Aimed Shot/Multi-Shot custom timing is retained for compatibility
through ShaguTweaks' legacy query wrappers. Those hooks are no longer installed
for other classes. The built-in Improved Castbar continues to use ClassicAPI
directly; this cleanup does not add custom Hunter casts to that display.
## Validation and limits
`tests/test_cast_tracking.lua` covers native query ownership, duplicate enable
guards, disabled consumers, complete native records, missing channel timing,
live channel identity, delayed failures, expiry, cache limits, and Hunter
custom-cast entry points. These are mocked Lua 5.0 checks, not client/DLL tests.
Before release, test player casts/channels and interruptions; enemy casts and
interruptions; switching targets during a channel; and Enemy Castbars with
Super WoW Compatibility both enabled and disabled. If a Hunter is available,
also test Aimed Shot and Multi-Shot through spellbook, macro and action buttons.
This reduces redundant Lua handlers and records. No FPS, memory or CPU gain has
been measured. Compare the same encounter/options using ClassicAPI's
GetAddOnCPUUsage/GetAddOnMemoryUsage with script profiling enabled; do not rely
on GetFrameCPUUsage or infer a frame-rate gain from code size.
+208
View File
@@ -0,0 +1,208 @@
# ClassicAPI / pfUI overlap review — 3.8.2
Reviewed 2026-11-04 against the current, uncommitted ShaguTweaks build.
This is a source review and implementation shortlist, not a performance result
or a claim that every finding has been reproduced in game. No addon behavior
or recoloring was changed during this review.
Implementation follow-up: the energy/mana, macro ownership and native equipment
comparison work below has now been applied and has three new mocked regression
suites (25 total). The user confirmed the final energy/mana, macro and comparison
paths work in game, including the Bagshui comparison ownership fix. The energy adaptation retains
the original five-second mana sweep and two-second regen baseline; it adopts
phase rejection/native movement, not pfUI's separate configurable FSR bar or its
server-specific agility/regen-aura modifier calculation. Theme candidates remain
unimplemented.
## Sources and coverage
- [ClassicAPI API.md, commit c0b403ba05a917176fa680cbfc96c619c98d46d6](https://github.com/brues-code/ClassicAPI/blob/c0b403ba05a917176fa680cbfc96c619c98d46d6/docs/API.md).
Downloaded the pinned document after resolving current master. Reviewed its
system/function catalog and the contracts relevant to this addon. Its roughly
1.15 MB includes APIs for systems this addon does not implement.
- [brues-code/pfUI, commit 560343bd746e00898606cf756bdfe533a4823110](https://github.com/brues-code/pfUI/tree/560343bd746e00898606cf756bdfe533a4823110).
Fetched origin and verified the clean reference checkout matches current
master. Indexed its 90 module/API Lua files and examined the implementations
overlapping ShaguTweaks, including native timing, events, macros, tooltips,
inventories, maps, unit frames, skinning, and helper APIs.
- Indexed ShaguTweaks' 83 module and seven library files, plus its API/helpers,
loader, compatibility files and current TOC. Read the relevant implementations
and traced consumers for the findings below. API-symbol inventories are only
discovery aids: a wrapper can already use a native API elsewhere.
| Overlap | Result |
| --- | --- |
| Action bars, range, reagents and macros | Range subscriptions already adopted; macro ownership is the next substantial cleanup opportunity. |
| Player/target frames, health/power, portraits | Routing already separated; energy tick correctness and portrait event routing remain candidates. |
| Cast, swing, loss-of-control, cooldown, totem and roll displays | Swing native timing adopted; several other displays still animate/count down in Lua. |
| Item/tooltip/vendor, bags and mail/trade rarity | Many native item/tooltip APIs already used; equipment comparison can be simplified. Preserve working rarity behavior. |
| Chat, social/player cache | Native identity and cache interfaces already integrated; no justification to delete the Lua cache wholesale. |
| Minimap, map and coordinate helpers | Mostly visibility-gated/throttled; many updates genuinely depend on cursor/unit movement. Profile before further changes. |
| Golden/Dark UI and addon compatibility | Optional candidates listed separately below. Keep the tested implementation intact. |
| Auction replacement, equipment manager, full raid/unit-frame replacement, Glue/login, TTS and similar systems | Not equivalent to current addon features; do not import simply because pfUI or ClassicAPI offers them. |
## Recommended correctness and ownership work
### 1. Energy/mana tick: strongest next candidate
Current source: `mods/unitframes-energy-tick.lua:21` and its event/update handlers.
Reference: pfUI `modules/energytick.lua`.
Three concrete issues are visible in the source:
- The unit filter runs before event dispatch. ClassicAPI's
PLAYER_ENTERING_WORLD now carries `true` in arg1 on a fresh login; the filter
rejects it as not equal to `"player"`, skipping initialization. Reload's arg1
is nil, so this need not reproduce the same way after /reload.
- `lastMana` is seeded on world entry but not on UNIT_DISPLAYPOWER. Changing
forms can compare an energy value with the previous mana sample or vice versa.
- Any mana loss sets a five-second sweep. A drain such as Mana Burn does not
establish the same five-second-rule condition as paying for a spell. Any
sufficiently large gain can also reset the regen sweep, including a potion
or unrelated mana proc. The spark's initial width/height are captured once,
so later standalone/Big Health relayout also deserves testing.
pfUI separates power-mode samples, spell-cost/FSR tracking and the periodic regen
phase. It also uses an invisible native status-bar duration to move the spark.
Adapt the model to the existing player mana bar rather than importing pfUI's
unit-frame layout or assuming every Octo/Turtle regen rule matches its comments.
Benefit: correctness first, plus removal of the spark's Lua per-frame movement.
Risk: medium for a full rewrite; low for the login-payload/unit-filter fix alone.
Test mana spending, drains, potions/procs, channels, full mana, form switching,
and Big Health/standalone layouts. Event-filter initialization can be fixed
independently of the broader native-timer conversion.
### 2. Native macro ownership instead of repeated manual scans
Current sources: `mods/macro-icons.lua:18`, `:47`, `:113`;
`mods/macro-tweaks.lua:17` and its slash registrations.
Current ClassicAPI handles native macro conditions, #showtooltip/#show, spell
and item icons, range, cooldown, usability, count and current castsequence step.
It also exposes C_Macro.GetMacroIcon and C_Macro.SetMacroDisplay. The current
Macro Icons module rebuilds a 36-slot macro cache and scans visible action bars
on several events, parses bodies, resolves spellbook slots and may rewrite icons.
There is a concrete behavior difference: ClassicAPI preserves a manually chosen
macro icon and substitutes spell/item artwork only for the question-mark icon.
Our resolver can replace that chosen artwork with GetSpellTexture when the
specified spell resolves. This is a source-level difference, not a reproduced
user complaint. Conditional/item macros already fall back to GetActionTexture
when our parser cannot resolve them; do not claim every conditional macro fails.
Proposed migration: let native #showtooltip/#show own ordinary macros. Retain
the nonstandard `--showtooltip` / `/pfcast` behavior explicitly if desired,
using SetMacroDisplay where appropriate. Review the global SendChatMessage
metadata filter and old /equip, /use, focus and attack aliases against the
installed native command surface. Finding a native macro command does not prove
that an equivalent interactive chat alias exists in SlashCmdList; test both.
Benefit: less parsing/allocation and fewer competing owners; better consistency
with native macro behavior. Risk: medium, due to existing custom syntax, chosen
icons, macro addons and older supported DLLs. Feature/version detection and
tests must precede deletion. Do not remove Macro Tweaks as one undifferentiated
block. pfUI's current `macroicons.lua` is an icon-picker UI, not the same feature.
### 3. Equip Compare can delegate comparison construction
Current source: `mods/equip-compare.lua:21`, `:53`, `:129`.
Reference: pfUI `modules/eqcompare.lua`.
pfUI calls GameTooltip:SetHyperlinkCompareItem for comparison offsets 1 and 2.
ShaguTweaks still maintains an equipment-type-to-slot table and manually shifts
tooltip lines to insert CURRENTLY_EQUIPPED. A guarded native path could replace
much of that construction while retaining our Shift condition, positioning,
font/color style and third-party tooltip behavior.
Benefit: smaller code and native handling of comparison slots/item instances;
likely little FPS effect. Risk: medium visual/compatibility risk. Test rings,
trinkets, dual wield, two-hand weapons, equipped enchants/random suffixes, bags,
links, vendors and simultaneous tooltip addons. Preserve the actual item link,
not just its item ID, when comparing.
## Native runtime opportunities to prioritize after profiling
| Feature | Current work | Native/pfUI approach | Benefit and risk |
| --- | --- | --- | --- |
| Cooldown Numbers | One visible helper OnUpdate per active cooldown, formatting at 0.1 s, plus alpha synchronization. | pfUI configures native Cooldown countdown fonts/formatters. For stock 1.12 Model indicators, retain the original swipe and consider a duration-text binding for our overlay. | Potentially useful with many cooldowns; medium compatibility risk. Stock Models are not native Cooldown frames, so this is not a blanket method substitution. Preserve GCD suppression, TimeConvert formatting, noCooldownCount/pfCooldownType and other addons' ownership; do not enable a global CVar merely to style ours. |
| Loss of Control | While shown, reselects effects, rebuilds metadata and reapplies theme/text/width every 0.05 s. | pfUI reselects on LOC events plus visible-only UNIT_AURA_DELTA and SPELL_UPDATE_COOLDOWN; native duration/countdown handles display timing. | Reduces repeated aura/list scans and formatting during CC. Medium correctness risk: refreshed CC or an already-locked school can extend without a LOC diff event; expiration/selection also need explicit handling. Keep priority and classic styling. |
| Player/target cast displays | Already discover casts on events; remaining Lua work animates text/fill/spark/alpha while visible. | pfUI uses duration objects, native fill-edge anchors and text bindings. | Conditional saving during casts. Medium risk from pushback, channel updates, fade/preview and SuperWoW fallback. Native cast-duration objects are snapshots, not automatically updated: refresh on delay/channel-update events. Remote channels whose start was unseen can still lack native timing. |
| Totem Timers | Four-slot timed text/color/width updates while totems are up. | GetTotemDuration and duration bindings; scheduled 10/5-second color thresholds and expiry; pfUI uses native cooldown displays. | Shaman-only saving; lower priority for the user's druid. Preserve our bar/tooltip/click design rather than copying pfUI's panel layout. Replacements/destruction/preview must cancel old bindings/timers. |
| Class Portraits | Every portrait event refreshes six frames; target-of-target adds a 0.2 s GUID check through the stock updater. | pfUI's chained frames use UNIT_TARGET subscriptions. Update the relevant portrait on unit events and handle same-name target changes by GUID. | Small conditional saving and clearer ownership. Preserve native portrait refresh hooks: stock code can overwrite custom artwork in the same frame. Validate actual event order before removing the safety path. |
| Group roll bars | One shared visible-only Lua loop queries remaining time for every active roll each frame. | Native durations for the fill with event-driven reset/cancel, as other pfUI displays do. | Low priority: already consolidated and runs only during rolls. Native snapshot timing must account for actual roll changes; profile first. |
No table above promises a measured CPU or frame-rate improvement. Native work
still costs client time, even when it disappears from Lua profiling.
## Golden/Dark UI: optional findings only — do not change now
The current implementation is a compatibility recolorer. pfUI replaces or skins
many widgets with its own artwork/backdrops; copying its skin code would change
the look and bypass the fixes that made parchment, slots, rarity and addon borders
correct. There is no documented single ClassicAPI switch that replaces our theme.
| Candidate | What could improve | Risk | Expected benefit |
| --- | --- | --- | --- |
| Read-only theme inspection | A diagnostic using GetDebugName/GetSourceLocation and region/owner inspection could identify the actual texture/addon behind a missed border, reducing trial and error. It need not change any colors. | Low; keep it on demand. | Maintenance/debugging only, no FPS gain. |
| Coalesce repeated deferred recolors | RecolorOnShow currently applies immediately and schedules another pass. Merge multiple pending requests for the same frame while retaining the immediate and trailing behavior. | Moderate: timing relative to row/frame creation is essential. | Probably small; mainly window creation/show bursts. Profile before considering. |
| Texture-path classification/cache and traversal scratch | Recolor traversals create region/child arrays and repeatedly classify texture paths. A bounded path-only classification cache or carefully scoped reusable arrays could avoid some garbage. Name-based exclusions must remain separate; recursive scratch cannot be shared at the same depth, and reused arrays need correct Lua 5.0 lengths. | Low-to-moderate for narrow path caching; moderate for traversal changes. | Small/unmeasured and mostly startup/window-open work. Existing path/region markers and dynamic texture changes must remain correct. |
| Targeted addon lifecycle hooks | pfUI's skins commonly use addon-load or widget-construction hooks. These might narrow the global CreateFrame wrapper in our recolorer. | High compatibility risk: anonymous or late-created Bagshui/aux/VCB/RollFor widgets can be missed. | Unmeasured; avoid unless the wrapper is actually expensive. |
| Explicit native-menu styling | pfUI styles MenuVariants' default/context menu mixins and selection controls. If a current ClassicAPI-created menu is visibly unthemed, a targeted adapter could extend Golden/Dark coverage. | Moderate; pooled/reused controls and checked/disabled states need testing. | Visual consistency only. This is a potential coverage extension, not a confirmed silver-border defect in the user's build. |
Retain all parchment/slot-edge markers, neutral-color checks, rarity preservation,
TrinketMenu exclusions, default-UI guards and the trailing recolor timing. No
recommendation here requires reopening the working recoloring before profiling.
## Small cleanup and deliberate non-changes
- An internal source-reference scan found no production consumer outside api.lua
for these wrappers: GetNamePlateForUnit, GetDebuffDataByIndex, UnitDebuff,
GetDebuffType, SetOverrideBindingClick, ClearOverrideBindings, GetMapOverlays
and UnitInRange. These are cleanup candidates, not proven safe deletions:
exposed ShaguTweaks APIs may have external consumers. Runtime benefit is
negligible; capability flags may still be needed by other helpers.
- Macro migration could shrink libspell/libtipscan further. Currently the macro
module uses GetSpellIndex and Hunter emulation uses GetSpellInfo. Both
libraries' scanners are lazy. Do not delete them before migrating these users
and deciding how to preserve their public interfaces.
- Keep libnameplate: Golden/Dark compatibility registers its callbacks for
ShaguPlates. It is dormant without consumers and is not an unused library.
- Native HookScript/hooksecurefunc can simplify selected hook sites, but replacing
our helper globally is unsafe without an argument/order audit. Our helper
supports prepend and returns a fixed legacy argument shape; native script
handlers can receive explicit self/event arguments. Styling locks deliberately
replace methods to intercept colors, which a simple posthook does not replace.
- Keep Auto Stance; ClassicAPI Auto Cancel Form handles the opposite operation.
pfUI's older autoshift/error handler is not a better replacement for ours.
- Keep observed/shared healing prediction. The reviewed API has healing bonus
and spell metadata, not a complete authoritative incoming-heal total. Raw DBC
effects alone cannot safely replace the estimator or the Puppeteer source.
- Keep independent Range Color subscriptions and the existing native merchant,
item-cache, loot-history, gossip, map-overlay and player-cache integrations.
Many of the obvious pfUI native implementations are already represented here.
- A cursor-following tooltip still needs movement observation; pfUI also uses a
visible-only follower. World-map coordinates must track moving cursors/units.
Replacing every visible OnUpdate with a ticker is not inherently faster.
- ClassicAPI now documents syntax transpilation, extra Lua library functions and
a raised upvalue limit. That does not supersede this repository's Lua 5.0
coding/testing rules. Do not modernize syntax merely for stylistic cleanup.
- Do not add pfUI's bags, auction/raid-frame replacements, equipment manager,
alternate heal model or other large features solely for parity. Existing
dedicated addons and the user's chosen Blizzard styling remain the design.
## Suggested order at the time of review
1. Energy-tick initialization/form-sample fixes; then decide whether to adopt its
native sweep and more accurate regen/FSR model.
2. Native macro ownership migration with a clear policy for the two custom
syntaxes and chosen macro icons.
3. Native equipment comparison if that behavior change is wanted.
4. Profile the current build to select among cooldown, LOC, cast, portrait and
other runtime candidates. Keep theme candidates documented unless a real
defect or measured bottleneck justifies their risk.
The existing 22 regression suites passed before this documentation-only review.
The later implementation follow-up noted above added three suites (25 total)
for energy/mana, macros and native equipment comparison. Other recommendations
remain unimplemented and have no measured performance benefit.
+173
View File
@@ -0,0 +1,173 @@
# ClassicAPI integration updates
## Current integration (3.8.3, 2026-11-05)
The [ClassicAPI/pfUI overlap review](CLASSICAPI_PFUI_REVIEW_382.md) pins current
source revisions and records optional recoloring improvements. Its energy/mana
tick, macro ownership and native equipment comparison recommendations have
subsequently been implemented; other candidates remain recommendations.
ClassicAPI is required, with a minimum loaded DLL version of 1.18.10 and core
timer/nameplate API checks. Install the DLL and its bundled `!!!ClassicAPI` addon
together and fully restart the client after a DLL update. Optional SuperWoW
support is detected separately. Newer APIs are detected per feature rather than
assumed available from the minimum version.
- Range Color uses native action-range subscriptions/change events when present;
older supported APIs retain a throttled fallback.
- Healing Predictions shares compatible OEVG Puppeteer data when available;
otherwise it uses a lazy ClassicAPI event/observed-heal estimator with live HoT
aura timing. See [the estimator review](HEALING_PREDICTION_REVIEW.md).
- Personal Resource Display mirrors the native player bars without a second
health/power event driver; its health copy shares the existing prediction
estimator. Resource hiding is independent, and Hide Player Name requires it.
The former standalone setting/positions migrate; Hide Player Portrait is retired.
- Castbars query ClassicAPI directly. The unused name-keyed cast mirror and
combat-text parser are removed; SuperWoW supplements missing enemy timing with
a bounded GUID cache. See [the cast review](CAST_TRACKING_REVIEW.md).
- Swing/wand fill and text use native duration objects/bindings when supported,
with cancellable boundary timers and no Lua per-frame driver. Older APIs retain
the throttled path. The implementation follows brues-code/pfUI at `560343b`.
- Health and power events refresh only their respective number overlays;
identity changes/startup retain complete refreshes. Modifier-key checks and
the repeated delayed layout initialization use shared API helpers.
- Energy/mana ticks handle login/form payloads and use cast-cost-driven FSR with
a separately observed regen phase and native spark movement where supported.
Ordinary macro icons are left to ClassicAPI; only custom legacy hints need
our adapter. Missing interactive command aliases use native macro parsing.
- Native equipment comparison builds equipped headers/slot selection/stat
differences from full item links, retaining our Shift and positioning rules.
- Legacy debuff timing and nameplate discovery have been removed. Auto Dismount
is provided by ClassicAPI; ShaguTweaks Auto Stance remains distinct from Auto
Cancel Form. Theme recoloring remains unchanged by the latest cleanup.
These focused reviews do not constitute a complete audit of all changes between
ClassicAPI releases. Current automated checks and their limits are listed below.
## Historical review (2026-10-31)
The following sections record the integration written against v1.17.6 and the
subsequent v1.17.7–v1.18.13 review. They describe that snapshot, not the current
feature list. Debuff Timer was removed in 3.8 in favor of
[TargetFrameBuff](https://octowow.st/git/OEVG/TargetFrameBuff), and its unused
library was removed in 3.8.1. See [migration notes](EXTRAS_MERGE.md#removed-later).
### Changes at that time
- **Health correctness:** `UnitHealthMissing` returns a deficit in the same units
as the health getters. It can be a percentage, so it is no longer used to infer
absolute health. Direct health and the existing combat-estimation cache remain.
- **Chat identity:** `GetPlayerInfoByGUID` supplies the name in return position 6.
The synchronous class fallback now checks that name against the requested player.
- **Cooldown rendering:** Target Debuff Timer creates native `Cooldown` frames on
ClassicAPI 1.17.1+, retaining `Model` frames on older builds. Numeric timers and
the existing timer dispatcher are preserved.
- **Unit event filtering:** health/power overlays register only player, target and
pet unit events; the health estimator and target castbar register only target
unit events. Older clients without `RegisterUnitEvent` use plain registration
and their existing handler-side checks.
- **Chat payload:** spam filtering and passive sender collection read
`GetCurrentChatEvent` when available. The filter uses local payload values and
does not overwrite shared `arg1`/`arg2` globals. The older global path remains.
- **Player-cache duplication:** when native persistence is already enabled and
`C_PlayerCache.RememberPlayer` succeeds, new chat-only sightings do not also
create ShaguTweaks SavedVariables records. Native class-only lookups can avoid
creating another record. Existing records, raw levels and social history stay
intact. With persistence off or native storage unavailable, the original Lua
cache behavior remains. The addon never enables native persistence itself.
### Reviewed since v1.17.6
156 ClassicAPI commits (v1.17.7 to v1.18.13) were read against the modules in
this addon.
**Changed here**
- **Debuff Timer:** the native `Cooldown` frame it creates now calls
`SetHideCountdownNumbers(true)` when available. ClassicAPI can draw its own
countdown numbers when the `countdownForCooldowns` CVar is `1` (default `0`),
which would have doubled the timer text this module draws itself.
**Worth knowing, no code change**
- **Nameplate crash fix (v1.18.10):** world-tick subscribers now run after the
UI draw. Before this, an addon that restyled a nameplate from
`NAME_PLATE_CREATED` or a `C_Timer` callback could crash the client. No
crash was traced to this addon, but v1.18.10 or newer is recommended for the
Nameplates modules.
- **Range Color:** since v1.18.0, `C_Spell.IsSpellInRange` returns `nil` for a
unit the spell cannot be cast at. The module already treats `nil` as "no
range to fail", so it needs nothing.
- **Cooldown Numbers:** action, bag and spellbook cooldowns are still 1.12
indicator Models, and `CooldownFrame_SetTimer` still serves them. The
module's hook is still required; the native countdown numbers do not replace
it.
- **Filtered unit events:** `RegisterUnitEvent` now also filters custom events
that ClassicAPI fires itself (v1.18.9). The vanilla events used here were
already filtered.
**Not adopted**
- `UNIT_POWER_UPDATE`, `UNIT_POWER_FREQUENT`, `UNIT_MAXPOWER` (v1.18.10): the
legacy `UNIT_MANA` / `UNIT_ENERGY` / `UNIT_RAGE` events still fire and the
overlays depend on their payloads. Switching would add a code path without a
measured gain.
- `UNIT_AURA_DELTA` and aura instance IDs (v1.17.10): the debuff library
refreshes on `UNIT_AURA` for the target and reads auras by index, so it does
not need change sets.
- `UNIT_TARGET`, `UnitFacing`, `wipe`, the `Cooldown` countdown formatter and
the other new APIs: no module here needs them.
At the time of that historical review, its changes had not been tested in game.
## Measuring performance
On current ClassicAPI, the following prints ShaguTweaks' Lua memory in KiB:
```lua
/script UpdateAddOnMemoryUsage(); DEFAULT_CHAT_FRAME:AddMessage(GetAddOnMemoryUsage("ShaguTweaks"))
```
Memory readings include uncollected objects. For comparisons, collect through
GentleGC first and measure at the same point after login and after playing.
Texture/native allocations are not equivalent to this Lua-memory counter.
For a short CPU investigation:
1. Run `/console scriptProfile 1`, then `/reload`.
2. Run `/script ResetCPUUsage()` and play through a representative encounter.
3. Read the total CPU time in milliseconds:
`/script UpdateAddOnCPUUsage(); DEFAULT_CHAT_FRAME:AddMessage(GetAddOnCPUUsage("ShaguTweaks"))`
4. Run `/console scriptProfile 0`, then `/reload` when finished.
Profiling adds overhead. Compare the same workload and measurement duration;
the cumulative CPU number alone is not an FPS measurement. Profiling is not
turned on by this addon.
## Verification
Changed/new Lua files are parse-checked with a real Lua 5.0 interpreter. The
current 25 mocked regression suites cover UI styling/rarity, options search,
unit-frame layouts, action-bar behavior/movement, runtime guards, range-event
ownership, cast tracking, healing prediction, shared startup/modifier helpers,
health/power routing, native/fallback swing and energy timers, macro ownership
and equipment comparison. Run them together with
`sh tests/run_all.sh ../tools/lua5.0/lua5.0` from the addon root; see
[tests/README.md](../tests/README.md).
These checks do not replace in-game rendering, DLL event-order and combat tests.
The user confirmed the range/healing/cast changes and the final energy/mana,
macro and equipment-comparison paths in game, including stable Bagshui comparisons
after the ownership recovery fix. Automated coverage does not establish every
possible addon/client combination. No memory/FPS
improvement is claimed without a live measurement.
## Primary references
- [ClassicAPI v1.18.13](https://github.com/brues-code/ClassicAPI/releases/tag/v1.18.13)
- [API reference (v1.18.13)](https://github.com/brues-code/ClassicAPI/blob/v1.18.13/docs/API.md)
- [ClassicAPI v1.17.6](https://github.com/brues-code/ClassicAPI/releases/tag/v1.17.6)
- [Nameplate crash fix](https://github.com/brues-code/ClassicAPI/commit/4eb214f)
- [Native cooldown rendering](https://github.com/brues-code/ClassicAPI/commit/b83bb5b52b11ffd51dd59aa7465b1185222f2bd4)
- [Reliable chat payload](https://github.com/brues-code/ClassicAPI/commit/b5aed30a5db5ef033dd9a6619d8f74310787c4c9)
- [Native player-name lookup](https://github.com/brues-code/ClassicAPI/commit/c7bb90895f8802ba837833d5ad1f9acbe0afece4)
+78
View File
@@ -0,0 +1,78 @@
# ShaguTweaks Extras ClassicAPI: what moved where
Since version 3.7 the modules of Dusk's [ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) are part of this addon, so you no longer need to install it. This page lists where every module went, what was left out and why.
Copied from Extras commit `8e14bb4` (2026-09-18). Credits and licenses are in `THIRD_PARTY_NOTICES.md`.
## What to do when updating
From 3.8.6, Cooldown Numbers uses ClassicAPI's native countdown text when
`countdownForCooldowns` is enabled. It preserves ShaguTweaks' colors, rounding
and duration threshold. The addon leaves that global CVar unchanged; when it is
off, or the required native APIs are unavailable, the existing Lua renderer
continues to work. Legacy Model indicators retain their sweep and use a native
child for numbers when supported.
For custom-module authors: 3.8.6 removes unused `ShaguTweaks.L` data sections
`bagtypes`, `itemtypes`, `hunterpaging`, `critters`, `dyndebuffs`, `judgements`,
`interrupts`, `resurrections`, `spells`, `debuffs` and `totems`. The `class` and
`customcast` sections remain. Custom modules using the removed tables must
supply their own data or use client APIs.
1. Install this addon (3.7 or newer).
2. Disable or delete the `ShaguTweaks-extras` folder in `Interface\AddOns`. While it stays enabled, its older copies replace the built-in ones, and a message in chat reminds you at login.
3. Your on/off choices carry over, because the modules kept their names.
## Where the modules went
Open the settings with **Esc → Advanced Options**.
| Module | Now in category |
|---|---|
| Dragonflight Gryphons | Action Bar |
| Floating Actionbar | Action Bar |
| Hide Macro Text | Action Bar |
| Mouseover Right, Mouseover Right 2 | Action Bar |
| Reagent Counter | Action Bar |
| Center Vertical Actionbar | Action Bar |
| Show Bags, Show Micro Menu | Action Bar |
| Center Text Input Box | Chat & Social |
| Block NPC Spam | Chat & Social |
| Enable Text Shadow | Chat & Social |
| Chat Timestamps | Chat & Social |
| World Chat Hider | Chat & Social |
| Loot Quality | Loot |
| Macro Icons, Macro Tweaks | **Macro** (new category) |
| Buy Em All | **Merchant** (new category) |
| MiniMap Framerate & Latency | Minimap & World Map |
| Reveal World Map | Minimap & World Map |
| Cursor Tooltip, Hide Combat Tooltip, Metric Range | Tooltip & Items |
| Unit Frame Abbreviated Names | Unit Frames |
The textures these modules use (the gryphons and the XP bar) moved to this addon's `img` folder.
## Left out, and why
| Module | Reason |
|---|---|
| **Raid Frames**, with all its options (Show Aggro Indicators, Show Combat Feedback, Use Compact Layout, Show Dispel Indicators, Show Group Headers, Show Healing Predictions, Hide Party Frames, Use As Party Frames) | Not wanted in this fork. I use Puppeteer for Raid/Party/Focus frames. |
| **Bag Item Click** and **Bag Search Bar** | [Bagshui](https://octowow.st/git/OEVG/Bagshui) already has a search box and sends items to trade, the auction house and mail on right-click. |
| **Auction Alt-Buy** | [aux](https://octowow.st/git/Dusk/aux-addon-ClassicAPI) already buys out or cancels with Alt+click on a row. |
## Removed later
| Module | Version | Reason |
|---|---|---|
| **Key-Down Casting** and **Alt Self-Cast** | 3.7.1 | ClassicAPI now has the game's own `ActionButtonUseKeyDown` setting. Type `/console ActionButtonUseKeyDown 1` once instead. Details in the [changelog](../CHANGELOG.md). |
| **Debuff Timer** | 3.8 | [TargetFrameBuff](https://octowow.st/git/OEVG/TargetFrameBuff) already provides target buff/debuff countdowns with ClassicAPI. Use its timer settings instead of running overlapping timers in ShaguTweaks. |
| **Auto Dismount** | 3.8.1 | ClassicAPI provides Auto Dismount, Auto Stand and Auto Cancel Form. |
| **AoE Loot** | 3.8.6 | Removed in favor of a dedicated area-looting addon. Use [AutoAreaLoot](https://github.com/rotaruandrei93/AutoAreaLoot) for this functionality; check its README for required dependencies. The old ShaguTweaks setting is retired automatically. |
| **Nameplate Castbar**, **Nameplate Scale** and **Nameplate Class Colors** | 3.8 | Enabling these together with another nameplate addon can cause conflicts because both addons modify the same nameplates. Use one dedicated nameplate addon instead; alternatives are listed below. |
### Nameplate alternatives
Choose one to handle your nameplates:
- [BlizzNameplatesPlus](https://github.com/Wurmschwanz/BlizzNameplatesPlus) for a similar Blizzard-like appearance.
- [UnitPlates](https://github.com/Redbu11dev/UnitPlates) for more detailed nameplates.
- [ShaguPlates](https://github.com/Dinkleberrrg/ShaguPlates) for extensive customization.
+70
View File
@@ -0,0 +1,70 @@
# Healing prediction review — 2026-11-04
## Reviewed versions
- ShaguTweaks: local 3.8.2 test build based on `dfe8cea` (3.8.1).
- OEVG's private [Puppeteer](https://octowow.st/git/OEVG/Puppeteer):
`ee273a93ef7f6d3f60376b601e27a9ec2f49595f`. Fetched `origin/main` and verified
the clean local checkout matched it. This review does not use upstream
OldManAlpha Puppeteer as a substitute.
## Implemented in ShaguTweaks
- Prefer Puppeteer's public `PTHealPredict.GetIncomingHealing(guid)` and
`HookUpdates(callback)` when present and player GUID formats match. Use the
provider's total once, including its HoTs. Do not add its direct-heal subtotal
or ShaguTweaks estimates to it.
- Do not start ShaguTweaks' estimator event listener, combat-log processing,
HealComm reception/broadcasting or expiry polling while this shared provider
is active. A guarded callback is registered once per provider; repeated
enable/disable cycles do not accumulate listeners. Puppeteer owns its data
and its roster of tracked units; this integration does not modify either.
- Without that provider, keep direct cast estimates separate from HoT tick
samples. Learn periodic amounts only for the two displayed units, by caster
GUID and spell ID. Observe native aura timing/caster identity and removal;
unknown amounts or unknown timing are not invented.
- The displayed HoT contribution is **one upcoming tick per active HoT**,
matching Puppeteer's current total. It is not the sum of every future tick.
Rejuvenation and Regrowth can coexist; Regrowth's direct cast is not
confused with its periodic amount. Stopping a direct cast does not remove HoTs.
- Preserve learned values across form changes. Real gear/talent/level changes
invalidate the standalone player's own estimates. Reuse native positional
aura getters where possible; scan on events rather than on every frame.
- Expiry work stops when no prediction remains. Explicit Lua 5.0 array-length
bookkeeping prevents stale entries after repeated HoT expiry/refresh cycles.
## Improvements identified in private Puppeteer (not modified here)
1. **Caster-specific HoT identity.** `AddHot` stores
`IncomingHots[target][spellName]`; `UpdateCacheHot` and `RemoveHoT` use the
same name-only identity. If a server permits same-name HoTs from multiple
casters, the later one overwrites the earlier entry. The ClassicAPI aura
removal handler also supplies no caster identity to `RemoveHoT`. Prefer
caster GUID plus spell ID/aura instance, with tests for refresh/remove ordering.
2. **Native expiry rather than a fixed emergency timeout.** The normal path
removes a HoT on an aura-removal event, but the cleanup fallback drops all
HoTs after 25 seconds. Actual aura expiry would improve correctness when
events are missed and avoid treating every effect as the same duration.
3. **Persistent cache validity.** `PTHealCache` and `PTPlayerHealCache` retain
estimates across sessions; observed values converge gradually. A gear,
talent, level or build signature for the local player's estimates would
prevent an old build's values being used until enough new samples arrive.
Avoid invalidating on form-only spellbook/inventory notifications.
4. **Callback removal API.** `HookUpdates` appends a callback but exposes no
corresponding removal function. A token/unsubscribe API would allow addon
integrations to detach cleanly. ShaguTweaks currently registers once and
guards disabled callbacks.
5. **Clarify HoT semantics.** `GetIncomingHealing` adds each HoT's stored tick
amount, not all remaining ticks. The UI/help should make this explicit;
changing to total remaining healing would be a separate behavior change.
## Validation and limits
Lua 5.0 mocks exercise direct/periodic separation, multiple casters,
refresh/removal/expiry, gear changes, shared totals, provider absence/GUID
mismatch, late activation and disabled callbacks. Neither estimator promises
server-authoritative heal amounts. Unknown/cold caches, healing modifiers,
critical heals and remote visibility can affect estimates. Standalone reception
supports explicit-duration vanilla HealComm direct-heal messages; compact/group
protocols are not implemented. Native rendering, real provider event ordering,
and CPU/memory costs still require in-game verification.
+55
View File
@@ -0,0 +1,55 @@
# Modern power bars
Modern Class Colors uses precolored retail resource fills, with frame-specific variants of ModernPowerMana.tga, ModernPowerRage.tga, ModernPowerFocus.tga and ModernPowerEnergy.tga. Each bar uses white vertex tint (1, 1, 1), preserving the native artwork's colors and gradient. Flat PowerBarColor tints multiplied by the old grayscale texture made mana too dark.
## Verified source
- Blizzard client build 12.1.5.70448, interface/hud/uiunitframe2x.blp, FileDataID 4642466.
- Download: https://wago.tools/api/casc/4642466?version=12.1.5.70448
- Atlas coordinates: https://github.com/Ketho/BlizzardInterfaceResources/blob/f3b334b33bd19fa6a0250f7922ecfa83db7fe356/Resources/AtlasInfo.lua
- Retail application uses the resource atlas and white tint: https://github.com/Gethe/wow-ui-source/blob/live/Interface/AddOns/Blizzard_UnitFrame/Mainline/UnitFrame.lua
## Frame-specific fitting
Player bars use the player artwork. Warlock mana uses the rectangular ClassResource variant seen in retail warlock frames; rogue/druid energy uses the corresponding energy variant. Other player resources retain the standard player artwork. These are visual choices for the Vanilla client; retail specialization/resource mechanics are not imported.
Target bars use Target artwork. Pet and target-of-target bars use the smaller TargetofTarget artwork. Party bars use Party artwork from interface/hud/uipartyframe.blp (FileDataID 4681512), downloaded from https://wago.tools/api/casc/4681512?version=12.1.5.70448 . Both sheets use the same build and pinned atlas metadata above.
The crops include the native dark edges and endcaps. Bilinear resizing (instead of Lanczos) avoids sharpening overshoot, and export dimensions are closer to the smaller Vanilla bars. White vertex tint is retained. A solid black BACKGROUND texture behind each styled power bar prevents the underlying Vanilla backing from tinting transparent edges and provides a neutral empty portion; it hides for missing units and unknown resource types. Frame sizes, positions, health artwork and gold text are unchanged. Animated retail sparks, retail frame borders and masks are not included, so a pixel-perfect match to every BetterBlizzFrames profile is not promised.
Exact source crops use exclusive right/bottom edges:
| Asset | Sheet | Left | Top | Right | Bottom | Export |
| --- | --- | --- | --- | --- | --- | --- |
| ModernPowerEnergy.tga | uiunitframe2x | 1667 | 479 | 1915 | 499 | 128x16 |
| ModernPowerFocus.tga | uiunitframe2x | 387 | 501 | 635 | 521 | 128x16 |
| ModernPowerMana.tga | uiunitframe2x | 637 | 501 | 885 | 521 | 128x16 |
| ModernPowerRage.tga | uiunitframe2x | 1137 | 501 | 1385 | 521 | 128x16 |
| ModernPowerEnergyClass.tga | uiunitframe2x | 1637 | 501 | 1885 | 521 | 128x16 |
| ModernPowerManaClass.tga | uiunitframe2x | 387 | 523 | 635 | 543 | 128x16 |
| ModernPowerEnergyTarget.tga | uiunitframe2x | 1666 | 415 | 1934 | 435 | 256x16 |
| ModernPowerFocusTarget.tga | uiunitframe2x | 387 | 457 | 655 | 477 | 256x16 |
| ModernPowerManaTarget.tga | uiunitframe2x | 657 | 457 | 925 | 477 | 256x16 |
| ModernPowerRageTarget.tga | uiunitframe2x | 1197 | 457 | 1465 | 477 | 256x16 |
| ModernPowerEnergySmall.tga | uiunitframe2x | 1607 | 151 | 1755 | 165 | 128x16 |
| ModernPowerFocusSmall.tga | uiunitframe2x | 1757 | 151 | 1905 | 165 | 128x16 |
| ModernPowerManaSmall.tga | uiunitframe2x | 1857 | 337 | 2005 | 351 | 128x16 |
| ModernPowerRageSmall.tga | uiunitframe2x | 1163 | 441 | 1311 | 455 | 128x16 |
| ModernPowerEnergyParty.tga | uipartyframe | 70 | 179 | 144 | 186 | 128x8 |
| ModernPowerFocusParty.tga | uipartyframe | 146 | 179 | 220 | 186 | 128x8 |
| ModernPowerManaParty.tga | uipartyframe | 1 | 191 | 75 | 198 | 128x8 |
| ModernPowerRageParty.tga | uipartyframe | 153 | 191 | 227 | 198 | 128x8 |
BetterBlizzFrames can retain the native game artwork when custom power textures are disabled, or replace it with a user-selected SharedMedia texture. These assets reproduce native retail fills, not an arbitrary BetterBlizzFrames saved profile.
Unknown/custom resource types retain their original texture and tint. Shapeshifts and target changes reselect the appropriate resource texture. Disabling Modern and reloading restores stock behavior. Gold numbers are unchanged.
Restart WoW after installation so new texture files are available. Lua 5.1 mock checks cover all four resource transitions, frame-family and class-variant selection, white tint enforcement, pet/party bars, missing-unit and unknown-resource background cleanup, disabled behavior and repeated initialization. Actual in-game rendering remains to be checked.
## Rectangular mana slots
All Modern mana bars now use ModernPowerManaClass.tga, the native rectangular retail mana fill. The regular retail target/small/party art has angled transparent corners that leave wedges inside Vanilla's rectangular mana slots. Sharing the rectangular fill fixes that gap for all classes and supported frames, while preserving white tint, the retail blue gradient, frame dimensions and text. Other resources retain their frame-specific variants.
## Druid secondary mana bar
PlayerFrameAlternatePowerBar uses the same rectangular light-blue mana fill and white tint. Its resource type is fixed to mana while the primary player bar changes to rage or energy. This is independent of the white-number option. The temporary deep-blue tint experiment was reverted.
+90
View File
@@ -0,0 +1,90 @@
# Performance notes
## Player cache (`ShaguTweaks_player_cache`)
The account-wide player cache feeds Social Colors, Chat Levels and the
nameplate/chat class colors. It is read at every login and `/reload` and
written at every logout, so its size is a direct load-time and memory cost.
Earlier versions let it grow without limit; one account reached 4.1 MB.
`libs/libunitscan.lua` now owns expiry and runs it once per session, at the
first loading screen:
| Entry | Kept for |
|---|---|
| Player whose class is known | 180 days after last sighting |
| Player whose class was never learned | 90 days after last sighting |
| Whole cache | at most 10,000 players; the oldest sightings go first |
- Every sighting refreshes the age: mouseover, target, nameplate, party,
raid, guild roster, friend list, `/who` and chat.
- Guild members and friends are refreshed by their roster events every
session, so they do not expire while they stay in the guild or friend list.
- Entries saved before this change have no age. They are dated from the
cache's existing grace start (`players_cleanup`), as the old Social Colors
cleanup did. The first login after the update can remove many entries.
- A player's GUID is saved only while the class is unknown. Such entries are
resolved later with `GetPlayerInfoByGUID` when the engine knows the GUID.
The separate 180-day cleanup in Social Colors is removed. It ran only every
180 days and only while that module was enabled.
## Event handling
- Chat: a speaker whose class is known and who spoke within the hour needs
no GUID lookup or write.
- Guild roster: a full rescan runs at most once a minute unless the member
count changed. Many addons request the roster every few seconds.
- Mouseover, target, nameplate and group sightings skip `UnitGUID` when
`UnitClass` already gave the class.
- A name missing from the cache queries ClassicAPI's player cache once
instead of twice.
## Range Color
3.8.2 uses native action-range subscriptions and change events when available.
Visibility and paging maintain balanced references. Older supported APIs retain
the independent 0.2-second timer introduced by the earlier fix; Blizzard's own
range timer remains untouched.
## Health/power numbers and swing timers (3.8.2)
Health events refresh only health text, and power events refresh only power
text, including the alternate druid power display. Target/pet identity changes
and startup still refresh both.
Swing/wand timers use native duration objects and countdown bindings when
available. Cancellable timers cover due/grace and Hunter warning boundaries;
range and action-state events update colors. The native path has no Lua
OnUpdate driver. Older APIs retain a throttled fallback, with numeric-tenth
comparison before formatting text. Current recoloring behavior is unchanged.
These are reductions in Lua work, not measured FPS claims. Follow the
[final 3.8.2 validation/measurement plan](PERFORMANCE_TEST_382.md) for A/B results.
## Map and portrait checks
- WorldMap Class Colors updates its icons ten times a second, and only while
the world map or battlefield minimap is open. It used to run a check on
every frame with both maps closed.
- Class Portraits checks the target of target five times a second, plus at
once when the stock frame reports a new name. It used to call `UnitGUID`
on every frame.
## Not changed
- Enlarged Minimap still checks the buff anchors every frame while the
default buff frame is shown. The stock `BuffFrame_Enchant_OnUpdate` moves
the buff frame back on every frame, so a less frequent check would let the
buffs jump under the minimap. It writes only when an anchor has actually
moved. The check is a child of `BuffFrame`, so it stops when a buff addon
such as VCB hides the default buffs (the stock re-anchoring stops then too).
- Health Numbers and Big Health use one-shot deferred updates only.
- Both castbars update only while visible; Energy Tick hides at full mana
out of combat.
- Free Slot Count counts slots once per `BAG_UPDATE_DELAYED`. Disable it if a
bag addon already shows the count.
Nothing here has been measured in game yet. To compare before and after, see
[Measuring performance](CLASSICAPI_UPDATES.md#measuring-performance).
+93
View File
@@ -0,0 +1,93 @@
# 3.8.2 final validation and measurement
Only the player can run these checks in the game. No performance gain has been
measured by the automated tests. The live game folder was not modified.
## First verify behavior
- Real Health Numbers: take damage, heal, spend/regenerate power, switch targets
and summon/dismiss a pet. Druid forms should show the appropriate resource and
update the separate mana display. Check Big Health and standalone layouts.
- Swing/Wand Timer: attack, stop, move out of range, change targets, and change
weapons. Dual-wield bars and wand Shoot should reset and stop correctly.
- Druid Maul or Warrior Heroic Strike/Cleave: queue and cancel, checking colors.
Hunter Auto Shot: check the last-half-second red warning if a Hunter is available.
- Hold Ctrl+Shift+Alt during a swing: preview labels remain stable, countdowns
resume on release, and drag/reset/saved positions still work after reload.
- Energy & Mana Tick: test a fresh login, Cat/Bear/caster transitions, spending
mana, channel completion/cancellation, a potion and a drain if practical.
Verify the spark after the first observed gain, its five-second cast-cost
sweep, and resizing with Big Health/standalone bars.
- Macros: test bare/explicit/conditional #showtooltip, item macros, castsequence,
chosen icons and any legacy --showtooltip or /pfcast display hints you use.
Edit/delete a custom hint and reload; check for stale overrides. Check missing
/equip, /use, focus and attack aliases without replacing another addon's aliases.
- Shift-compare rings, trinkets and weapons from bags/vendors/chat links; check
one empty paired slot, enchanted/random-suffix items, Shift release, hover
changes and tooltip closure. Compare with other tooltip addons enabled.
## Find the remaining bottlenecks
Profile the current build first. The purpose is to identify expensive workloads
and then narrow them down to modules/functions, not to prove the recent changes
made the addon faster. Older builds are not required for this investigation.
Keep the same character, location, target, addon list, UI settings and frame-rate
limit. Enable Real Health Numbers and the applicable Swing/Wand Timer in both.
Warm up for two minutes after loading; do not include loading or option changes
in a measured run. Keep GentleGC and other client mods configured identically.
Enable profiling, then reload:
```
/console scriptProfile 1
/reload
```
Start each sample with:
```
/script ResetCPUUsage(); STPerfStart=GetTime()
```
Perform the chosen scenario for about 60 seconds, then print average addon Lua
CPU milliseconds per elapsed second:
```
/script UpdateAddOnCPUUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.3f ms/s over %.1f s",GetAddOnCPUUsage("ShaguTweaks")/(GetTime()-STPerfStart),GetTime()-STPerfStart))
```
Scenarios: (1) idle with no windows open; (2) steady melee/wand attacks;
(3) repeated damage/healing and power changes, including druid form changes;
(4) repeated map, inventory, mail and auction-window interactions. Record which
windows were used and how many times. Repeat each three times on the current
build and compare medians. Repeating comparable activity matters more than
identical duration alone.
These addon totals are the first triage step, not module/function attribution.
For workloads with meaningful cost, follow with focused function CPU profiling
and, where practical, repeat the same scenario with one relevant option disabled
(reload after changing options). That separates module cost from unrelated
activity and tells us where a code change might be worthwhile. Respect the
decision to preserve the working theme recoloring behavior.
At the same point after warm-up and after a fixed test session, sample Lua memory:
```
/script UpdateAddOnMemoryUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.1f KiB",GetAddOnMemoryUsage("ShaguTweaks")))
```
Memory includes uncollected objects; compare at equivalent GentleGC collection
points. Do not force collections during CPU samples. Native textures/DLL memory
and total garbage production are not represented by this single memory number.
Send the build, scenario, enabled options, three CPU results and memory readings.
Those results guide the next targeted investigation. Lower addon Lua
CPU does not by itself prove higher FPS: native timer work moves out of Lua,
profiling adds overhead, and other addons/client rendering may dominate. Do not
use GetFrameCPUUsage for this comparison. Afterward, turn profiling off:
```
/console scriptProfile 0
/reload
```
+119
View File
@@ -0,0 +1,119 @@
# 3.8.3 profiling: current bottlenecks and theme cost
Only the player can run these checks in the game. No performance gain has been
measured by the automated tests. The live game folder was not modified.
## Focused theme comparison
First measure the current configuration, then repeat with both recoloring options
off, Golden UI only, and Darkened UI only. Reload after each setting change.
Use the same addons, character, location, FPS limit and GentleGC configuration.
For every mode, warm up for two minutes and open the same windows once before
sampling. Then take three 60-second idle samples and three 60-second samples
opening/closing the same map, bags, mail and auction windows the same number of
times. Record CPU ms/s, memory before/after, and any hitches. Measure initial
window creation separately from repeated openings if only the first opening hitches.
| Mode | Scenario | CPU ms/s, three runs | Memory before/after | Hitches |
| --- | --- | --- | --- | --- |
| Both themes off | Idle / windows | | | |
| Golden UI only | Idle / windows | | | |
| Darkened UI only | Idle / windows | | | |
The reported 6.2 MB versus 6.9 MB is approximately 0.7 MB extra retained Lua
memory. These two snapshots do not measure allocation rate, CPU cost or a leak.
After several identical window-opening cycles and natural GC cycles, check
whether memory settles or keeps rising. Do not manually collect during CPU runs.
Also profile the current build in combat/forms/healing as below. Theme comparisons
isolate one option's cost; they do not establish gains over an older release.
## First verify behavior
- Real Health Numbers: take damage, heal, spend/regenerate power, switch targets
and summon/dismiss a pet. Druid forms should show the appropriate resource and
update the separate mana display. Check Big Health and Personal Resource Display layouts.
- Swing/Wand Timer: attack, stop, move out of range, change targets, and change
weapons. Dual-wield bars and wand Shoot should reset and stop correctly.
- Druid Maul or Warrior Heroic Strike/Cleave: queue and cancel, checking colors.
Hunter Auto Shot: check the last-half-second red warning if a Hunter is available.
- Hold Ctrl+Shift+Alt during a swing: preview labels remain stable, countdowns
resume on release, and drag/reset/saved positions still work after reload.
- Energy & Mana Tick: test a fresh login, Cat/Bear/caster transitions, spending
mana, channel completion/cancellation, a potion and a drain if practical.
Verify the spark after the first observed gain, its five-second cast-cost
sweep, and resizing with Big Health and Personal Resource Display bars.
- Macros: test bare/explicit/conditional #showtooltip, item macros, castsequence,
chosen icons and any legacy --showtooltip or /pfcast display hints you use.
Edit/delete a custom hint and reload; check for stale overrides. Check missing
/equip, /use, focus and attack aliases without replacing another addon's aliases.
- Shift-compare rings, trinkets and weapons from bags/vendors/chat links; check
one empty paired slot, enchanted/random-suffix items, Shift release, hover
changes and tooltip closure. Compare with other tooltip addons enabled.
## Find the remaining bottlenecks
Profile the current build first. The purpose is to identify expensive workloads
and then narrow them down to modules/functions, not to prove the recent changes
made the addon faster. Older builds are not required for this investigation.
Keep the same character, location, target, addon list, UI settings and frame-rate
limit. Enable Real Health Numbers and the applicable Swing/Wand Timer in both.
Warm up for two minutes after loading; do not include loading or option changes
in a measured run. Keep GentleGC and other client mods configured identically.
Enable profiling, then reload:
```
/console scriptProfile 1
/reload
```
Start each sample with:
```
/script ResetCPUUsage(); STPerfStart=GetTime()
```
Perform the chosen scenario for about 60 seconds, then print average addon Lua
CPU milliseconds per elapsed second:
```
/script UpdateAddOnCPUUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.3f ms/s over %.1f s",GetAddOnCPUUsage("ShaguTweaks")/(GetTime()-STPerfStart),GetTime()-STPerfStart))
```
Scenarios: (1) idle with no windows open; (2) steady melee/wand attacks;
(3) repeated damage/healing and power changes, including druid form changes;
(4) repeated map, inventory, mail and auction-window interactions. Record which
windows were used and how many times. Repeat each three times on the current
build and compare medians. Repeating comparable activity matters more than
identical duration alone.
These addon totals are the first triage step, not module/function attribution.
For workloads with meaningful cost, follow with focused function CPU profiling
and, where practical, repeat the same scenario with one relevant option disabled
(reload after changing options). That separates module cost from unrelated
activity and tells us where a code change might be worthwhile. Respect the
decision to preserve the working theme recoloring behavior.
At the same point after warm-up and after a fixed test session, sample Lua memory:
```
/script UpdateAddOnMemoryUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.1f KiB",GetAddOnMemoryUsage("ShaguTweaks")))
```
Memory includes uncollected objects; compare at equivalent GentleGC collection
points. Do not force collections during CPU samples. Native textures/DLL memory
and total garbage production are not represented by this single memory number.
Send the build, scenario, enabled options, three CPU results and memory readings.
Those results guide the next targeted investigation. Lower addon Lua
CPU does not by itself prove higher FPS: native timer work moves out of Lua,
profiling adds overhead, and other addons/client rendering may dominate. Do not
use GetFrameCPUUsage for this comparison. Afterward, turn profiling off:
```
/console scriptProfile 0
/reload
```
+52
View File
@@ -0,0 +1,52 @@
# UI option hotfix review — 2026-11-03
Scope: the 3.7.3 mail, trade, Golden UI/Darkened UI, aux/AuxCraft and Bagshui
compatibility changes. This is a focused review, not an audit of every module.
## Changes from the user-tested build
- Coalesce full outgoing-mail refreshes into one pending callback. Use the
existing feature-detected `API.Defer` bridge to ClassicAPI's native
`C_Timer.After(0, callback)`; retain the addon queue on older builds.
Skip the callback's work if the mail window has closed. Individual TurtleMail
attachments still update synchronously when their textures change.
- Remove the redundant full trade-update hook. Blizzard's full update already
calls both individually hooked slot-update functions for all seven slots.
- Preserve marked rarity colors and alpha when another addon reapplies a
backdrop; common-white rarity borders remain white under either palette.
- Keep the slot-art matcher limited to string texture paths.
- Remove a redundant boolean expression and expand a compressed refresh block.
The temporary diagnostic slash command was already removed.
## Performance assessment
The reviewed paths add no continuous OnUpdate driver. Work runs on item changes,
window shows, frame creation, or one-shot callbacks. The native timer is used for
scheduling, not recurring polling. New callbacks reuse their functions.
The palettes still traverse the frame tree at initialization and wrap frame
creation and border setters for compatible windows. This has a real startup and
UI-creation cost. Rarity updates also perform item lookups and border operations.
Four narrow texture strips are allocated once per slot background that needs
recoloring and then reused. The inbox also uses four parchment crops. Extra
regions consume memory and rendering work even though they need no Lua polling.
Removing the redundant trade hook avoids a second 14-slot rarity pass during a
full trade refresh. Coalescing mail refresh requests avoids duplicate work within
a scheduling interval. Neither improvement is an in-game benchmark.
No FPS, memory, GC or CPU measurements were taken in game. Lua mocks do not model
client rendering or DLL behavior. Use ClassicAPI's per-addon CPU/memory counters
with script profiling enabled for comparable fixed-duration scenarios if a
slowdown is suspected; do not use GetFrameCPUUsage for this comparison.
## Validation and remaining limits
Lua 5.0 checks are documented in `tests/README.md`. They cover qualities 0–5,
mail add/remove ordering, different items on each trade side, cleared slots,
option independence, slot reuse, palette persistence and timer fallback.
OEVG confirmed the preceding build's UI behavior in game. These cleanup changes
still need a short in-game smoke test: mail attachment add/remove, both trade
columns, Bagshui empty slots, and reopening aux/AuxCraft under each palette.
Keep version 3.7.3; no live game files were modified.
+187
View File
@@ -0,0 +1,187 @@
# 🧩 ShaguTweaks — ClassicAPI
A lightweight fork of [ShaguTweaks](https://github.com/shagu/ShaguTweaks) for **Turtle WoW-like servers**.
Focused on **stability, compatibility and performance** while keeping the original ShaguTweaks experience.
> This fork requires ClassicAPI and targets **Turtle WoW-like server/client environments**.
## 🔌 Requirements
- **[ClassicAPI](https://github.com/brues-code/ClassicAPI) — required**
- **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional / recommended**
ClassicAPI is the main API layer. SuperWoW is only used as an additional fallback for some cast/GUID information.
## 📦 Installation
1. Install **ClassicAPI**.
2. Optionally install **SuperWoW**.
3. Rename the addon folder to `ShaguTweaks`.
4. Copy it to `World of Warcraft\Interface\AddOns\ShaguTweaks`.
5. Restart the game.
Settings: **Esc → Advanced Options**.
## ✨ Main changes
- ClassicAPI-backed integration for casts, auras, items and unit data.
- Reduced per-frame work, tooltip scans and repeated UI updates.
- Event-driven target and nameplate castbars where possible.
- Safer GUID-based handling for same-name enemies.
- Better Turtle WoW-like server compatibility for custom items, spells and UI changes.
- Safer hooks and fewer destructive global overrides.
- Expanded Item Rarity Borders support for merchant, quest, mail, trade, crafting and loot frames.
- Added independent Item Rarity Glows using the shared item rarity engine.
- Expanded Movable Unit Frames to handle Player, Target, Party, Minimap, Buffs, Debuffs and Weapon Buffs through one shared Ctrl+Shift mover and alignment grid.
- Restored Real Health Numbers as an independent module, with direct values when available and a lazy Vanilla health estimator fallback for percentage-only targets.
- Unit Frame Big Health is presentation-only again and no longer forces numeric health/power text.
- Various stability fixes across legacy ShaguTweaks modules.
## 🔷 Mods using ClassicAPI directly
- Actionbar Range Color
- AoE Loot
- Auto Dismount
- Chat Tweaks
- Equip Compare
- Free Slots
- Improved Castbar
- Item Rarity Borders
- Item Rarity Glows
- Movable Unit Frames
- Real Health Numbers
- Nameplate Class Colors
- Nameplate Castbar
- No Toggle
- Sell Junk
- Skip Gossip Text
- Enemy Target Castbar
- Tooltip Details
- WorldMap Window
Other modules can also benefit indirectly from ClassicAPI through shared ShaguTweaks libraries and caches.
## ⚙️ Modules
### Action Bar
- Auto Dismount
- Auto Stance
- Actionbar Range Color
- Cooldown Numbers
- Hide Gryphons
- Improved Castbar
- No Toggle Auto-Attack
- Reduced Actionbar
### Unit Frames
- Big Health
- Class Colors
- Class Portraits
- Energy & Mana Tick
- Health Colors
- Movable Unit Frames
- Real Health Numbers
- Enemy Target Castbar
- Target Debuff Timer
### Nameplates
- Nameplate Scale
- Nameplate Class Colors
- Nameplate Castbar
### Tooltip & Items
- Equip Compare
- Free Slots
- Item Rarity Borders
- Item Rarity Glows
- Sell Junk
- Tooltip Details
- Vendor Values
### Chat & Social
- Blue Shaman Class Colors
- Chat Tweaks
- Chat Levels
- Chat Spam Filter
- Social Colors
### Minimap & World Map
- Clean Minimap
- Minimap Clock
- Minimap Square
- Minimap Tweaks
- Minimap Zoom
- Hide Tracking Icon
- WorldMap Class Colors
- WorldMap Coordinates
- WorldMap Window
### Loot
- AoE Loot
- Improved Roll Frames
- Loot Cursor
### Interface
- Darkened UI
- Hide Errors
- Smaller Error Frame
- XP Bar Text
### General
- Super WoW Compatibility
- Turtle WoW Compatibility
- Skip Gossip Text
## 🔧 Compatibility
- ClassicAPI integration
- Turtle WoW-like server integration
- SuperWoW integration
## 🧹 Removed / integrated modules
The following original ShaguTweaks modules are not included as standalone modules because their functionality is either integrated elsewhere or no longer required:
- **Chat Links** → integrated into **Chat Tweaks**
- **Movable Unit Frames Extended** → integrated into **Movable Unit Frames**
## 📜 Project identity & licensing
This is an independent community project. Compatibility with **World of Warcraft**, **Turtle WoW-like environments**, **ClassicAPI**, **SuperWoW**, or other referenced projects does not imply affiliation, endorsement, sponsorship, or ownership by their respective rights holders or maintainers.
**World of Warcraft** and **Blizzard Entertainment** names, marks, and game assets remain the property of their respective rights holders.
For detailed provenance and licensing information, see:
- [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)
- [PROJECT_IDENTITY.md](PROJECT_IDENTITY.md)
- [Docs/ASSET_PROVENANCE.md](Docs/ASSET_PROVENANCE.md)
- [LICENSES/](LICENSES/)
## 🙏 Credits
Original addon and modules by **Shagu**.
Additional maintenance and Turtle WoW work by **paokkerkir**.
ClassicAPI compatibility fork maintained by **Dusk-92**.
Special thanks to **Sandrea** for providing **AoELoot**, adapted here as the **AoE Loot** module.
Special thanks to **Jesper_92** for reporting issues that helped improve the addon.
Additional code and ideas from **pfUI** and **zUI**.
Extended Movable Unit Frames support adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`.
Released under the original **MIT License**.
+5
View File
@@ -11,6 +11,11 @@ apply every listed license to the ShaguTweaks-ClassicAPI repository as a whole.
- `ShaguTweaks-mods-MIT.txt` — MIT license for the adapted - `ShaguTweaks-mods-MIT.txt` — MIT license for the adapted
TokensWorth/ShaguTweaks-mods material documented in TokensWorth/ShaguTweaks-mods material documented in
`THIRD_PARTY_NOTICES.md`. `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 - `ClassicAPI-GPL-3.0.txt` — verbatim license document from the external
ClassicAPI repository. ClassicAPI is not bundled here. ClassicAPI repository. ClassicAPI is not bundled here.
- `SuperWoW-LICENSE.txt` — verbatim upstream license from the external - `SuperWoW-LICENSE.txt` — verbatim upstream license from the external
+21
View File
@@ -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.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2021 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.
+199 -70
View File
@@ -1,17 +1,22 @@
# 🧩 ShaguTweaks — ClassicAPI # 🧩 ShaguTweaks — ClassicAPI
A lightweight fork of [ShaguTweaks](https://github.com/shagu/ShaguTweaks) for **Turtle WoW-like servers**. [![ClassicAPI](https://img.shields.io/badge/ClassicAPI-Required-purple.svg)](https://github.com/brues-code/ClassicAPI)
[![SuperWoW](https://img.shields.io/badge/SuperWoW-Optional-yellow.svg)](https://github.com/balakethelock/SuperWoW)
Focused on **stability, compatibility and performance** while keeping the original ShaguTweaks experience. English-only fork of [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI), based on [ShaguTweaks](https://github.com/shagu/ShaguTweaks), for the **Turtle WoW-like servers**.
> This fork requires ClassicAPI and targets **Turtle WoW-like server/client environments**. Focused on **stability, compatibility, customization, and performance** while keeping the original ShaguTweaks experience.
**Version 3.8.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 [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md) for where each module went and why some were left out. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for removed modules.
## 🔌 Requirements ## 🔌 Requirements
- **[ClassicAPI](https://github.com/brues-code/ClassicAPI) — required**
- **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional / recommended**
ClassicAPI is the main API layer. SuperWoW is only used as an additional fallback for some cast/GUID information. - **[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 required (it includes a crash fix for addons that restyle nameplates). The addon checks the loaded DLL version and core APIs before enabling tweaks, and feature-detects newer range and duration-timer APIs. See [current integration notes and the historical review](Docs/CLASSICAPI_UPDATES.md). ClassicAPI is the main API layer.
- **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional and recommended.** Supplements missing enemy cast timing when Enemy Castbars and Super WoW Compatibility are enabled. Complete ClassicAPI records are used directly without a second cache. See the [cast tracking review](Docs/CAST_TRACKING_REVIEW.md).
- **[ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) — no longer used.** Its modules are built in since 3.7 (see [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md)). Disable or remove it; while it is enabled, its copies replace the built-in ones.
## 📦 Installation ## 📦 Installation
@@ -23,130 +28,245 @@ ClassicAPI is the main API layer. SuperWoW is only used as an additional fallbac
Settings: **Esc → Advanced Options**. Settings: **Esc → Advanced Options**.
## ✨ Main changes ## 📷 Screenshots
Personal Resource Display, independently positioned player name and movable action bars with Golden UI.
![Personal Resource Display and movable action bars](screenshots/personal-resource-display.png)
Golden UI world map with the OctoBus Transports panel.
![Golden UI world map with OctoBus integration](screenshots/golden-world-map.png)
Modifier-key previews for movable action bars, cast bars and personal resources.
![Movable action bars and cast-bar previews](screenshots/movable-bars-preview.png)
Advanced Options with search and grouped settings.
![Advanced Options menu with search and UI mode selection](screenshots/advanced-options-3.8.5.png)
## ✨ What's different in this fork
**Proc & Reactive Spell Highlights** (Action Bar, off): pale light-blue action-button glows for supported procs and reactive spells, including macros. Uses ClassicAPI glow events and native animation where available, with a static fallback. Central artwork is left to Proc Doc or other alert addons. Requires ClassicAPI spell-overlay support; older supported DLLs leave this feature inactive.
**Unit frames**
- **Hide Player Name** (off): hides the name and raid group number while **Hide Player Frame Resources** is enabled. Its checkbox is unavailable without that prerequisite.
- **Unit Frame Energy & Mana Tick** (off): uses native spark movement when available, retains the five-second mana sweep for mana-costing spells, and observes the two-second regen phase separately. Form changes reset power samples; drains and mid-period gains do not falsely start/reset the cast-cost display. The spark also appears on the Personal Resource Display power bar, sharing timing and resizing with either bar.
- **Healing Predictions** (off): a green segment shows incoming direct heals plus one upcoming tick per active HoT, capped at maximum health. Reuses compatible Puppeteer prediction data when available, including its HoTs, without starting a second estimator. Otherwise ClassicAPI supplies cast/aura/combat events: own direct heals need an observed non-critical heal per spell rank each session, and HoTs such as Rejuvenation/Regrowth need an observed tick amount and identified live aura. Form changes preserve estimates; actual gear, talent or level changes invalidate own standalone samples. Other direct heals can arrive through explicit-duration vanilla HealComm messages. Unknown amounts/timing and percentage-only target health are not projected. Supports Big Health and Personal Resource Display. See the [estimator review](Docs/HEALING_PREDICTION_REVIEW.md).
- **Personal Resource Display** (off): independent copies of your health and power bars, leaving the original player frame intact. Hold **Ctrl+Shift+Alt** to drag either bar or resize it with its bottom-right grip; right-click with those keys held to reset that bar. Positions and sizes are saved per character. Big Health for Player gives both bars the enlarged height. Copied bars share native values, textures and colors, plus Healing Predictions when enabled. Replaces Standalone Player Frame and migrates its health/power positions.
- **Hide Player Frame Resources** (off): hides the original health and power bars, leaving a clean rounded portrait and a class-colored name. Independent of Personal Resource Display; use either or both. Ctrl+Shift+Alt-drag moves the name independently; right-click with those keys held resets it beside the portrait. Its position is saved per character. **Movable Unit Frames** moves the portrait; an undetached name follows it. The portrait keeps its native left level badge and resting/combat icons.
- **Modern Class Colors:** optional palette for all nine Vanilla classes, including blue shamans. Listed directly below **Unit Frame Class Colors**.
- **Modern textures:** class-tinted health artwork and precolored rage, energy and focus artwork. Mana uses the light-blue rectangular retail fill on every supported frame, including the druid's separate mana bar in bear/cat forms.
- **Real Health Numbers:** an independent module. Percentage on the left and current health on the right, inset 4 pixels; power numbers remain centered. It uses direct values when available and a lazy Vanilla health estimator for percentage-only targets, so unknown absolute target health stays percentage-only.
- **Number styling:** health, percentage and power numbers copy the corresponding character name's font, size and shadow. Gold is the default; **White Unit Frame Numbers** under Unit Frames switches the numbers to white independently of Modern Class Colors.
- **Unit Frame Big Health for Player** and **Unit Frame Big Health for Target** independently enlarge their respective health bars. The player option also class-colors the player health bar and the Personal Resource Display health bar when enabled. Both are presentation-only and do not force numeric health/power text. The resting pulse is limited to the portrait, and the full status artwork is restored outside resting or in combat.
- **Movable Unit Frames:** Player, Target, Party, Minimap, Buffs, Debuffs and Weapon Buffs share one mover and alignment grid. Hold **Ctrl+Shift+Alt** to drag; right-click while unlocked to reset.
- **Hide Pet Unit Frame:** hides the pet frame below the player frame for pet classes. The stock pet code keeps running, so nothing breaks when the pet is summoned, dismissed or dies.
**Loss of Control Alert and Totem Timers** (both off by default)
- **Loss of Control Alert** (Interface): a large icon, the name of the effect and a countdown above the middle of the screen when you are stunned, feared, confused, charmed, silenced, pacified, disarmed, rooted or locked out of a spell school after an interrupt. If several apply, the most disabling one is shown. Hold Ctrl+Shift+Alt to show a preview and drag it, right-click to reset (like the Swing Timer). Uses ClassicAPI's `C_LossOfControl`. A classic tooltip panel and icon rim follow the default, Darkened or Golden UI palette; the timer fill retains its effect-type color.
- **Totem Timers** (Interface, Shaman only): one icon per totem you have out (Fire, Earth, Water, Air) with the time left, turning yellow at 10 seconds and red at 5. Left-click an icon to target the totem; hover for its tooltip. Hold Ctrl+Shift+Alt to show four preview icons and drag them, right-click to reset. Uses ClassicAPI's `GetTotemInfo` and `PLAYER_TOTEM_UPDATE`.
**Cast-bar movement is always available** (no checkbox): hold Ctrl+Shift+Alt, drag the marker that appears over the cast bar, right-click to reset. When you drag it, the swing bars stay where they are. The position is saved per character and survives other addons re-anchoring the bar; the Swing Timer, Wand Timer and Improved Castbar icon follow it. **Enemy Castbars** (the target's cast bar) works the same way: its marker appears even without a target, and the saved spot is an offset from the target frame, so the bar follows the frame.
**Swing Timer and Wand Timer**
Current ClassicAPI animates the fill and countdown through native duration objects, following pfUI's approach, without a Lua per-frame driver. Older supported APIs keep a throttled fallback. Styling, movement, range/queued-attack colors and Hunter warning timing remain ShaguTweaks' own.
- **Swing Timer:** main-hand and off-hand swing timers styled like the classic cast bar, with the weapon's icon on the left. They sit just above the cast bar until you move them; hold **Ctrl+Shift+Alt** to drag each bar (main hand, off hand and the ranged/wand bar) on its own, and right-click a bar to put just that one back.
- **Wand Timer:** the time until your next wand shot in the same style. It also times Auto Shot for bows, guns and crossbows, turning red during the last half second, when moving would cancel the shot.
- The timers follow the server's real attack timers through ClassicAPI's `PLAYER_SWING` event, including parry haste and on-next-swing abilities. A bar turns gray while the target is out of reach.
- The main-hand bar shows a queued **Heroic Strike** in light blue and **Cleave** in green (warriors), and **Maul** in orange (druids).
- Ported from [brues-code/pfUI](https://github.com/brues-code/pfUI)'s swing timer. Requires ClassicAPI v1.15.10 or newer.
**Tooltips, items and action bars**
- **Equip Compare:** Shift shows native equipped-item comparisons from full item links, including paired slots and stat differences, while retaining our placement. Older APIs keep the previous comparison path.
- **Macro Icons:** current ClassicAPI handles ordinary directives, conditions and chosen icons. This option adds compatibility for legacy `--showtooltip` and `/pfcast` display hints; only conditional custom hints need periodic re-evaluation. **Macro Tweaks** supplies missing interactive command aliases through the native parser and preserves command-history filtering. Other macro parsers/registered aliases keep ownership.
- **Tooltip Details:** restored guild ranks and leader marker; optional modern class colors and health texture. NPC interaction and tooltip-clear handling prevent cached colors leaking between tooltips.
- **Item Rarity Borders** cover merchant, quest, mail (including every TurtleMail attachment), both sides of trade, crafting and loot frames in every UI mode. **Item Rarity Glows** add an independent glow using the same shared rarity engine. **Loot Quality** colors the loot window itself and does not enable mail or trade borders.
- **Range Color:** self/rangeless spells no longer use a misleading legacy fallback; invalid friendly/hostile target checks and stale red/no-mana tint are handled. No additional AoE-specific exceptions have been added. The range check runs five times a second instead of every frame.
- **XP Bar Text:** hovering the experience bar shows current and needed XP like the reputation bar (`1555/3000`), then the percentage and the rested bonus.
- **WorldMap Class Colors:** raid members on the world map and battlefield minimap show their own class color.
- **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** — full list of where each one went and what was left out: [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md)
- 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**
- ClassicAPI-backed integration for casts, auras, items and unit data.
- Reduced per-frame work, tooltip scans and repeated UI updates. - Reduced per-frame work, tooltip scans and repeated UI updates.
- Event-driven target and nameplate castbars where possible. - **Bounded player cache:** the shared player cache expires old entries and holds at most 10,000 players, so the saved variables file no longer grows without limit. See [Docs/PERFORMANCE.md](Docs/PERFORMANCE.md).
- Event-driven target castbars where possible.
- Safer GUID-based handling for same-name enemies. - Safer GUID-based handling for same-name enemies.
- Better Turtle WoW-like server compatibility for custom items, spells and UI changes.
- Safer hooks and fewer destructive global overrides. - Safer hooks and fewer destructive global overrides.
- Expanded Item Rarity Borders support for merchant, quest, mail, trade, crafting and loot frames. - **English-only loading:** only English data and translations are included, avoiding unused locale allocations.
- Added independent Item Rarity Glows using the shared item rarity engine. - Better Turtle WoW-like server compatibility for custom items, spells and UI changes, plus various stability fixes across legacy ShaguTweaks modules.
- Expanded Movable Unit Frames to handle Player, Target, Party, Minimap, Buffs, Debuffs and Weapon Buffs through one shared Ctrl+Shift mover and alignment grid.
- Restored Real Health Numbers as an independent module, with direct values when available and a lazy Vanilla health estimator fallback for percentage-only targets.
- Unit Frame Big Health is presentation-only again and no longer forces numeric health/power text.
- Various stability fixes across legacy ShaguTweaks modules.
## 🔷 Mods using ClassicAPI directly ## 🔷 ClassicAPI integration
- Actionbar Range Color ClassicAPI backs casts, auras, items and unit data: native cooldown support, filtered unit events, corrected chat identity/payload handling, and reduced duplicate player-cache records when native persistence is already enabled. Older fallbacks are retained for clients without the newer features.
- AoE Loot
- Auto Dismount Modules that use ClassicAPI directly:
- Range Color
- Chat Tweaks - Chat Tweaks
- Equip Compare - Equip Compare
- Free Slots - Free Slot Count
- Improved Castbar - Improved Castbar
- Item Rarity Borders - Item Rarity Borders
- Item Rarity Glows - Item Rarity Glows
- Loot History
- Movable Unit Frames - Movable Unit Frames
- Real Health Numbers - Real Health Numbers
- Nameplate Class Colors - No Toggle Auto-Attack
- Nameplate Castbar
- No Toggle
- Sell Junk - Sell Junk
- Skip Gossip Text - Skip Gossip Text
- Enemy Target Castbar - Swing Timer
- Wand Timer
- Loss of Control Alert
- Totem Timers
- Enemy Castbars
- Tooltip Details - Tooltip Details
- WorldMap Window - WorldMap Window
- Buy Em All
- 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. 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).
## ⚙️ Modules ## ⚙️ Modules
Names match the in-game options list. Modules marked *(off)* are disabled until you enable them.
### Action Bar ### Action Bar
- Auto Dismount - Auto Stance (enters a required stance/form; to cast Moonfire from Cat Form, enable ClassicAPI Auto Cancel Form instead)
- Auto Stance - Range Color *(off)*
- Actionbar Range Color
- Cooldown Numbers - Cooldown Numbers
- Hide Gryphons - Hide Gryphons *(off)*
- Improved Castbar - Improved Castbar *(off)*
- Cast-bar movement is built in: hold Ctrl+Shift+Alt to drag it, right-click to reset (no checkbox)
- Swing Timer *(off)*
- Wand Timer *(off)*
- No Toggle Auto-Attack - No Toggle Auto-Attack
- Reduced Actionbar - Reduced Actionbar Size *(off)*
- Show Bags *(off)*
- Show Micro Menu *(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 ### Unit Frames
- Big Health - Unit Frame Big Health for Player *(off)*
- Class Colors - Unit Frame Big Health for Target *(off)*
- Class Portraits - Unit Frame Class Colors *(off)*
- Energy & Mana Tick - Modern Class Colors *(off)*
- Health Colors - Unit Frame Class Portraits *(off)*
- Unit Frame Energy & Mana Tick *(off)*
- Hide Pet Unit Frame *(off)*
- Unit Frame Abbreviated Names *(off)*
- Movable Unit Frames - Movable Unit Frames
- Real Health Numbers - Real Health Numbers *(off)*
- Enemy Target Castbar - White Unit Frame Numbers *(off)*
- Target Debuff Timer - Enemy Castbars
### Nameplates ### Nameplate addons
Nameplate Scale, Class Colors and Castbar were removed in 3.8 because enabling them alongside another nameplate addon can cause conflicts. See [the migration guide](Docs/EXTRAS_MERGE.md#nameplate-alternatives) for BlizzNameplatesPlus, UnitPlates and ShaguPlates alternatives.
- Nameplate Scale
- Nameplate Class Colors
- Nameplate Castbar
### Tooltip & Items ### Tooltip & Items
- Equip Compare - Equip Compare
- Free Slots - Free Slot Count
- Item Rarity Borders - Item Rarity Borders
- Item Rarity Glows - Item Rarity Glows *(off)*
- Sell Junk - Sell Junk
- Tooltip Details - Tooltip Details
- Vendor Values - Vendor Values
- Metric Range
- Cursor Tooltip *(off)*
- Hide Combat Tooltip *(off)*
### Chat & Social ### Chat & Social
- Blue Shaman Class Colors - Blue Shaman Class Colors *(off)*
- Chat Tweaks - Chat Tweaks
- Chat Levels - Chat Levels *(off)*
- Chat Spam Filter - Chat Spam Filter *(off)*
- Social Colors - 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 ### Minimap & World Map
- Clean Minimap - Minimap Clean *(off)*
- Minimap Clock - MiniMap Clock
- Minimap Square - MiniMap Square *(off)*
- Minimap Tweaks - MiniMap Tweaks
- Minimap Zoom - Enlarged Minimap *(off)*
- Hide Tracking Icon - Hide Tracking Icon
- WorldMap Class Colors - WorldMap Class Colors
- WorldMap Coordinates - WorldMap Coordinates
- WorldMap Window - WorldMap Window
- Reveal World Map
- MiniMap Framerate & Latency *(off)*
### Loot ### Loot
- AoE Loot - Improved Roll Frames *(off)*
- Improved Roll Frames
- Loot Cursor - Loot Cursor
- Loot History (`/loothistory`)
- Loot Quality *(off)*
### Macro
- Macro Icons
- Macro Tweaks
### Merchant
- Buy Em All
### Interface ### Interface
- Darkened UI - **UI:** dropdown below the Advanced Options search bar, with **Default**, **Darkened** and **Golden** (Default initially). Select one mode and press Okay to apply/reload. Existing theme selections carry over; old saves with both enabled retain Darkened priority. Both themed modes recolor supported addon borders and minimap buttons. Golden uses gold borders, tooltips and dialogs like WoW Forever while retaining default text colors. TrinketMenu keeps its own styling.
- Hide Errors
- Smaller Error Frame Golden and Darkened UI keep the original NPC quest/gossip parchment, including its torn edges, while styling the outer frame. No stationery overlay is used on those panels.
- XP Bar Text
When **Unit Frame Big Health for Target** is disabled, Golden UI's NPC target name header uses an opaque native window-title texture matching the map/quest titles. Player targets and your own player frame retain their native/class-colored backgrounds. The header updates when switching targets.
- Hide Errors *(off)*
- Smaller Error Frame *(off)*
- Loss of Control Alert *(off)*
- Totem Timers *(off, Shaman)*
- XP Bar Text (current/needed XP, percentage and rested bonus on hover)
### General ### General
- Super WoW Compatibility - Super WoW Compatibility
- Turtle WoW Compatibility - Turtle WoW Compatibility
- Skip Gossip Text - Skip Gossip Text *(off)*
## 🔧 Compatibility
- ClassicAPI integration
- Turtle WoW-like server integration
- SuperWoW integration
## 🧹 Removed / integrated modules ## 🧹 Removed / integrated modules
@@ -154,6 +274,7 @@ The following original ShaguTweaks modules are not included as standalone module
- **Chat Links** → integrated into **Chat Tweaks** - **Chat Links** → integrated into **Chat Tweaks**
- **Movable Unit Frames Extended** → integrated into **Movable Unit Frames** - **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 ## 📜 Project identity & licensing
@@ -163,10 +284,14 @@ This is an independent community project. Compatibility with **World of Warcraft
For detailed provenance and licensing information, see: For detailed provenance and licensing information, see:
- [CHANGELOG.md](CHANGELOG.md)
- [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) - [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)
- [PROJECT_IDENTITY.md](PROJECT_IDENTITY.md) - [PROJECT_IDENTITY.md](PROJECT_IDENTITY.md)
- [Docs/ASSET_PROVENANCE.md](Docs/ASSET_PROVENANCE.md) - [Docs/ASSET_PROVENANCE.md](Docs/ASSET_PROVENANCE.md)
- [LICENSES/](LICENSES/) - [Docs/MODERN_MANA.md](Docs/MODERN_MANA.md)
- [Docs/PERFORMANCE.md](Docs/PERFORMANCE.md) — player cache limits and other performance notes
- [Docs/UPSTREAM_README.md](Docs/UPSTREAM_README.md) — the Dusk fork's README, kept for reference
- [LICENSES/](LICENSES)
## 🙏 Credits ## 🙏 Credits
@@ -176,12 +301,16 @@ Additional maintenance and Turtle WoW work by **paokkerkir**.
ClassicAPI compatibility fork maintained by **Dusk-92**. ClassicAPI compatibility fork maintained by **Dusk-92**.
Special thanks to **Sandrea** for providing **AoELoot**, adapted here as the **AoE Loot** module. The former **AoE Loot** module was removed in 3.8.6. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for the recommended replacement.
Special thanks to **Jesper_92** for reporting issues that helped improve the addon. Special thanks to **Jesper_92** for reporting issues that helped improve the addon.
Additional code and ideas from **pfUI** and **zUI**. Additional code and ideas from **pfUI** and **zUI**.
Extended Movable Unit Frames support adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`. 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 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**. Released under the original **MIT License**.
+50 -23
View File
@@ -2,30 +2,16 @@
## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI] ## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI]
## Author: Shagu ## Author: Shagu
## Notes: A collection of tweaks and enhancements to the Default UI ## Notes: A collection of tweaks and enhancements to the Default UI
## Notes-ruRU: Коллекция настроек и улучшений стандартного пользовательского интерфейса. ## Version: 3.8.7
## Version: GIT
## Dependencies: !!!ClassicAPI ## Dependencies: !!!ClassicAPI
## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache ## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache
## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache ## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache
# locales # locales
locales\locales_deDE.lua
locales\locales_enUS.lua locales\locales_enUS.lua
locales\locales_esES.lua
locales\locales_frFR.lua
locales\locales_koKR.lua
locales\locales_ruRU.lua
locales\locales_zhCN.lua
# translations # translations
locales\translations_deDE.lua
locales\translations_enUS.lua locales\translations_enUS.lua
locales\translations_esES.lua
locales\translations_frFR.lua
locales\translations_koKR.lua
locales\translations_ruRU.lua
locales\translations_zhCN.lua
locales\translations_zhTW.lua
# basic # basic
main.lua main.lua
@@ -37,7 +23,6 @@ config.lua
libs\libtipscan.lua libs\libtipscan.lua
libs\libspell.lua libs\libspell.lua
libs\libcast.lua libs\libcast.lua
libs\libdebuff.lua
libs\libunitscan.lua libs\libunitscan.lua
libs\libnameplate.lua libs\libnameplate.lua
libs\libpredict.lua libs\libpredict.lua
@@ -57,23 +42,18 @@ mods\minimap-clean.lua
mods\minimap-zoom.lua mods\minimap-zoom.lua
mods\minimap-tracking.lua mods\minimap-tracking.lua
mods\actionbar-range-color.lua mods\actionbar-range-color.lua
mods\actionbar-proc-highlights.lua
mods\free-slots.lua mods\free-slots.lua
mods\xpbar-text.lua mods\xpbar-text.lua
mods\nameplate-scale.lua
mods\sell-junk.lua mods\sell-junk.lua
mods\unitframes-classcolor.lua mods\unitframes-classcolor.lua
mods\vendor-values.lua mods\vendor-values.lua
mods\worldmap-coordinates.lua mods\worldmap-coordinates.lua
mods\worldmap-window.lua mods\worldmap-window.lua
mods\nameplate-classcolor.lua
mods\nameplate-castbar.lua
mods\move-unitframes.lua mods\move-unitframes.lua
mods\target-castbar.lua mods\target-castbar.lua
mods\target-debufftimer.lua
mods\social-colors.lua mods\social-colors.lua
mods\item-colors.lua mods\item-colors.lua
mods\health-color.lua
mods\auto-dismount.lua
mods\auto-stance.lua mods\auto-stance.lua
mods\tooltip-details.lua mods\tooltip-details.lua
mods\hide-errors.lua mods\hide-errors.lua
@@ -84,20 +64,67 @@ mods\hide-gryphons.lua
mods\worldmap-colors.lua mods\worldmap-colors.lua
mods\unitframes-bighealth.lua mods\unitframes-bighealth.lua
mods\health-numbers.lua mods\health-numbers.lua
mods\unitframes-healing-prediction.lua
mods\unitframes-classportrait.lua mods\unitframes-classportrait.lua
mods\unitframes-hide-player-name.lua
mods\unitframes-hide-player-resources.lua
mods\unitframes-personal-resource-display.lua
mods\unitframes-energy-tick.lua mods\unitframes-energy-tick.lua
mods\unitframes-hide-pet.lua
mods\cooldown-numbers.lua mods\cooldown-numbers.lua
mods\turtle-wow.lua mods\turtle-wow.lua
mods\superwow.lua mods\superwow.lua
mods\reduced-actionbar.lua mods\reduced-actionbar.lua
mods\actionbar-move.lua
mods\actionbar-grid.lua
mods\no-toggle.lua mods\no-toggle.lua
mods\aoe-loot.lua
mods\loot-cursor.lua mods\loot-cursor.lua
mods\loot-history.lua
mods\loss-of-control.lua
mods\totem-timers.lua
mods\improved-roll-frames.lua mods\improved-roll-frames.lua
mods\improved-castbar.lua mods\improved-castbar.lua
mods\castbar-move.lua
mods\swing-timer.lua
mods\smaller-errors-frame.lua mods\smaller-errors-frame.lua
mods\skip-gossip.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\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 # compatibility
compat-dragonflightui.lua compat-dragonflightui.lua
compat-guda.lua compat-guda.lua
+114 -3
View File
@@ -33,9 +33,74 @@ from `TokensWorth/ShaguTweaks-mods`:
- Copyright (c) 2022 GryllsAddons - Copyright (c) 2022 GryllsAddons
- License: MIT - 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 The preserved license text is stored in
`LICENSES/ShaguTweaks-mods-MIT.txt`. `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`,
`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
pfUI's swing timer module and restyled, and the Loot History window in
`mods/loot-history.lua` is ported from pfUI's loot history module and rebuilt
with stock 1.12 widgets:
- Upstream modules: `modules/swingtimer.lua`, `modules/loothistory.lua`
- Source: https://github.com/brues-code/pfUI
- Copyright (c) 2016-2021 Eric Mauser (Shagu) and contributors
- License: MIT
The preserved license text is stored in `LICENSES/pfUI-MIT.txt`.
## ClassicAPI ## ClassicAPI
ClassicAPI is a required external runtime dependency used by ClassicAPI is a required external runtime dependency used by
@@ -52,12 +117,13 @@ ClassicAPI binaries or source are distributed as part of this addon.
## AoELoot ## AoELoot
`mods/aoe-loot.lua` is adapted from the standalone AoELoot addon provided to Before 3.8.6, `mods/aoe-loot.lua` was adapted from the standalone AoELoot
the project by Sandrea: addon provided to the project by Sandrea. The module has since been removed;
this historical attribution remains for earlier releases:
- Original addon title: `AoELoot` - Original addon title: `AoELoot`
- Original metadata credit: Sandrea / ChatGPT - Original metadata credit: Sandrea / ChatGPT
- Adapted module: `mods/aoe-loot.lua` - Former adapted module: `mods/aoe-loot.lua`
The supplied package did not include an upstream URL or a separate license The supplied package did not include an upstream URL or a separate license
file. This notice records the known provenance and preserves attribution; it file. This notice records the known provenance and preserves attribution; it
@@ -127,3 +193,48 @@ documentation summarizes them.
When replacing or substantially rewriting third-party-derived material, update When replacing or substantially rewriting third-party-derived material, update
the provenance record rather than erasing the earlier history. the provenance record rather than erasing the earlier history.
## OEVG2 customization and BetterBlizzFrames texture
This repository imports a source snapshot of Dusk/ShaguTweaks-ClassicAPI downloaded on 2026-10-24 and adds English-only loading and unit-frame customizations described in README.md.
`img/BlizzardRetailBar.tga` was obtained from:
https://github.com/Bodify/BetterBlizzFrames/blob/main/media/blizzTex/BlizzardRetailBar.tga
BetterBlizzFrames' repository license states **All Rights Reserved**, copyright (c) 2023-2026 Bodify. This asset is not covered by this repository's original ShaguTweaks MIT license. The source project's notice does not establish separate provenance or permissions for any underlying game artwork.
## Modern mana bar texture
`img/BlizzardRetailBarCrop2.tga` is copied unchanged from BetterBlizzFrames:
https://github.com/Bodify/BetterBlizzFrames/blob/main/media/blizzTex/BlizzardRetailBarCrop2.tga
Source Git blob: `bb412b3fa538a6c91671d06ccbfd1e820f4c2876`.
BetterBlizzFrames: All Rights Reserved, Copyright (c) 2023-2026 Bodify.
This asset retains its original rights and is not relicensed under MIT.
The mana tint follows BetterBlizzFrames' default `PowerBarColor.MANA` lookup:
RGB `(0, 0, 1)`. The selected texture is its `Blizzard Retail Bar Crop 2` preset.
## Native retail resource artwork
`img/ModernPowerMana.tga`, `img/ModernPowerRage.tga`, `img/ModernPowerFocus.tga` and `img/ModernPowerEnergy.tga` are cropped/resized exports of Blizzard's `interface/hud/uiunitframe2x.blp` (FileDataID 4642466), client build 12.1.5.70448, retrieved from https://wago.tools/api/casc/4642466?version=12.1.5.70448 .
Matching atlas metadata and conversion details are recorded in `Docs/MODERN_MANA.md`. These are Blizzard game assets, not original artwork or assets relicensed under the software's MIT license. The previous BetterBlizzFrames grayscale asset attribution remains part of the history.
### Frame-specific resource exports
Additional img/ModernPower*Class.tga, *Target.tga, *Small.tga and *Party.tga variants use the matching Blizzard atlas crops documented in Docs/MODERN_MANA.md. Player/target/small variants come from FileDataID 4642466; party variants come from FileDataID 4681512, interface/hud/uipartyframe.blp. Both were retrieved for build 12.1.5.70448 from wago.tools. All resource exports now use bilinear resizing to compact power-of-two TGA dimensions. These remain Blizzard game artwork; no MIT relicensing is asserted.
## Additional ClassicAPI implementation reference (3.8.2)
brues-code/pfUI at commit 560343bd746e00898606cf756bdfe533a4823110 was reviewed for native action-range subscription ownership and the separation of incoming-heal estimation from frame overlays. Source: https://github.com/brues-code/pfUI . The focused ShaguTweaks implementations retain their own frame and module architecture.
The swing/wand timer also adapts that commit's `modules/swingtimer.lua` native duration/text-binding, fill-edge anchoring and cancellable boundary-timer approach (MIT, Shagu and contributors). ShaguTweaks retains its classic artwork, movement, colors, grace behavior and older-API fallback.
Energy/mana tick phase locking and native fill-edge spark movement were also
adapted from that commit's `modules/energytick.lua`; native equipment comparison
was informed by `modules/eqcompare.lua` (MIT, Shagu and contributors). Existing
ShaguTweaks presentation and older-API paths remain separate.
+114 -54
View File
@@ -7,6 +7,16 @@ ShaguTweaks.API = ShaguTweaks.API or {}
local API = ShaguTweaks.API local API = ShaguTweaks.API
API.classicapi_version = tonumber(_G.CLASSIC_API_VERSION) or 0 API.classicapi_version = tonumber(_G.CLASSIC_API_VERSION) or 0
API.minimum_version = 11810
API.minimum_version_text = "1.18.10"
API.IsSupportedRuntime = function()
-- The addon dependency checks the companion addon, not the loaded DLL.
return (tonumber(_G.CLASSIC_API_VERSION) or 0) >= API.minimum_version
and type(_G.C_Timer) == "table" and type(_G.C_Timer.After) == "function"
and type(_G.C_EventUtils) == "table" and type(_G.C_EventUtils.IsEventValid) == "function"
and type(_G.C_NamePlate) == "table" and type(_G.C_NamePlate.GetNamePlateForUnit) == "function"
and _G.C_EventUtils.IsEventValid("NAME_PLATE_UNIT_ADDED")
end
API.classicapi = type(_G.C_NamePlate) == "table" API.classicapi = type(_G.C_NamePlate) == "table"
or type(_G.C_Spell) == "table" or type(_G.C_Spell) == "table"
or type(_G.C_UnitAuras) == "table" or type(_G.C_UnitAuras) == "table"
@@ -50,9 +60,6 @@ API.containeritems = type(_G.C_Container) == "table"
API.containeriteminfo = type(_G.C_Container) == "table" API.containeriteminfo = type(_G.C_Container) == "table"
and type(_G.C_Container.GetContainerItemInfo) == "function" and type(_G.C_Container.GetContainerItemInfo) == "function"
API.containeropenable = type(_G.C_Container) == "table"
and type(_G.C_Container.IsContainerItemOpenable) == "function"
API.items = type(_G.C_Item) == "table" API.items = type(_G.C_Item) == "table"
API.iteminfo = API.items and type(_G.C_Item.GetItemInfo) == "function" API.iteminfo = API.items and type(_G.C_Item.GetItemInfo) == "function"
API.iteminfoinstant = API.items API.iteminfoinstant = API.items
@@ -80,6 +87,19 @@ API.eventutils = type(_G.C_EventUtils) == "table"
API.timer = type(_G.C_Timer) == "table" API.timer = type(_G.C_Timer) == "table"
and type(_G.C_Timer.After) == "function" and type(_G.C_Timer.After) == "function"
API.durationtimers = type(_G.C_DurationUtil) == "table"
and type(_G.C_DurationUtil.CreateDuration) == "function"
and type(_G.C_DurationUtil.CreateDurationTextBinding) == "function"
and type(_G.C_StringUtil) == "table"
and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function"
and type(_G.C_Timer) == "table" and type(_G.C_Timer.NewTimer) == "function"
and type(_G.Enum) == "table" and type(_G.Enum.StatusBarInterpolation) == "table"
and type(_G.Enum.StatusBarTimerDirection) == "table"
and type(_G.Enum.NumericRuleFormatRounding) == "table"
and _G.Enum.StatusBarInterpolation.Immediate ~= nil
and _G.Enum.StatusBarTimerDirection.ElapsedTime ~= nil
and _G.Enum.NumericRuleFormatRounding.Down ~= nil
API.regionmouseover = _G.UIParent API.regionmouseover = _G.UIParent
and type(_G.UIParent.IsMouseOver) == "function" and type(_G.UIParent.IsMouseOver) == "function"
@@ -122,19 +142,9 @@ API.loothistoryevents = API.loothistory and API.eventutils
and _G.C_EventUtils.IsEventValid("LOOT_HISTORY_FULL_UPDATE") and _G.C_EventUtils.IsEventValid("LOOT_HISTORY_FULL_UPDATE")
API.lootrollitemid = type(_G.GetLootRollItemID) == "function" API.lootrollitemid = type(_G.GetLootRollItemID) == "function"
API.aoeloot = type(_G.C_Loot) == "table"
and type(_G.C_Loot.GetNearbyLootableUnits) == "function"
and type(_G.C_Loot.IsScanInProgress) == "function"
and type(_G.C_Loot.LootAllCorpses) == "function"
API.overridebindings = type(_G.SetOverrideBindingClick) == "function" API.overridebindings = type(_G.SetOverrideBindingClick) == "function"
and type(_G.ClearOverrideBindings) == "function" and type(_G.ClearOverrideBindings) == "function"
API.playerstate = type(_G.IsMounted) == "function"
and type(_G.Dismount) == "function"
and type(_G.GetShapeshiftFormID) == "function"
and type(_G.CancelShapeshiftForm) == "function"
API.unitguid = type(_G.UnitGUID) == "function" API.unitguid = type(_G.UnitGUID) == "function"
API.unittoken = type(_G.UnitTokenFromGUID) == "function" API.unittoken = type(_G.UnitTokenFromGUID) == "function"
API.tooltipunit = _G.GameTooltip API.tooltipunit = _G.GameTooltip
@@ -150,6 +160,10 @@ API.tooltipsetitem = API.tooltipitem
API.unitrange = type(_G.UnitInRange) == "function" API.unitrange = type(_G.UnitInRange) == "function"
API.macrospell = type(_G.GetMacroSpell) == "function" API.macrospell = type(_G.GetMacroSpell) == "function"
API.macrodisplay = type(_G.C_Macro) == "table"
and type(_G.C_Macro.SetMacroDisplay) == "function"
and type(_G.C_Macro.GetMacroIcon) == "function"
API.nativemacrocommands = API.macrodisplay and type(_G.RunMacroText) == "function"
API.attackverbs = type(_G.StartAttack) == "function" API.attackverbs = type(_G.StartAttack) == "function"
and type(_G.StopAttack) == "function" and type(_G.StopAttack) == "function"
API.focus = type(_G.FocusUnit) == "function" API.focus = type(_G.FocusUnit) == "function"
@@ -225,6 +239,24 @@ API.GetChannelInfo = function(unit)
isTradeSkill, notInterruptible, spellID, true isTradeSkill, notInterruptible, spellID, true
end end
-- Native cast durations are snapshots: fetch again after pushback/channel
-- updates. Synthetic/SuperWoW timing uses a separate reusable object, never
-- mutating a duration returned by the engine.
API.GetCastDuration = function(unit, channel, startTime, endTime, fallback)
if not API.durationtimers then return end
local query = channel and _G.UnitChannelDuration or _G.UnitCastingDuration
if type(query) == "function" then
local duration = query(unit)
if duration and math.abs(duration:GetStartTime() - startTime) < .001
and math.abs(duration:GetEndTime() - endTime) < .001 then
return duration, fallback
end
end
fallback = fallback or _G.C_DurationUtil.CreateDuration()
fallback:SetTimeSpan(startTime, endTime)
return fallback, fallback
end
API.IsSpellInRange = function(spell, unit) API.IsSpellInRange = function(spell, unit)
if API.spellrange then if API.spellrange then
return _G.C_Spell.IsSpellInRange(spell, unit) return _G.C_Spell.IsSpellInRange(spell, unit)
@@ -373,6 +405,38 @@ API.Defer = function(callback)
end end
end end
-- Layout consumers must wait for world entry, then for other modules' first
-- OnUpdates, before restoring/detaching frames. Each request is one-shot and
-- releases its scripts when finished. The standalone layout needs a second
-- update without native timers; the action-bar layout only needs the first.
-- Schedule during module enable, before the first PLAYER_ENTERING_WORLD.
API.AfterWorldReady = function(callback, extraFrame)
local ready = CreateFrame("Frame")
local function Finish()
ready:SetScript("OnUpdate", nil)
ready:Hide()
callback()
end
ready:RegisterEvent("PLAYER_ENTERING_WORLD")
ready:SetScript("OnEvent", function()
ready:UnregisterAllEvents()
ready:SetScript("OnEvent", nil)
ready:SetScript("OnUpdate", function()
ready:SetScript("OnUpdate", nil)
ready:Hide()
if API.timer then
API.Defer(callback)
elseif extraFrame then
ready:SetScript("OnUpdate", Finish)
ready:Show()
else
callback()
end
end)
end)
return ready
end
API.IsMouseOver = function(region, topOffset, bottomOffset, leftOffset, rightOffset) API.IsMouseOver = function(region, topOffset, bottomOffset, leftOffset, rightOffset)
if API.regionmouseover and region then if API.regionmouseover and region then
return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset) return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset)
@@ -388,30 +452,6 @@ API.GetContainerNumFreeSlots = function(bag)
return 0, 0 return 0, 0
end end
API.IsContainerItemOpenable = function(bag, slot)
if API.containeropenable then
return _G.C_Container.IsContainerItemOpenable(bag, slot)
end
end
API.GetNearbyLootableUnits = function()
if API.aoeloot then
return _G.C_Loot.GetNearbyLootableUnits()
end
end
API.IsLootScanInProgress = function()
return API.aoeloot and _G.C_Loot.IsScanInProgress() or false
end
API.LootAllCorpses = function(maxCount)
if API.aoeloot then
if maxCount then return _G.C_Loot.LootAllCorpses(maxCount) end
return _G.C_Loot.LootAllCorpses()
end
return false
end
-- ClassicAPI updates its own left/right modifier bitmap before firing -- ClassicAPI updates its own left/right modifier bitmap before firing
-- MODIFIER_STATE_CHANGED. Prefer that state over vanilla's merged Win32 key -- MODIFIER_STATE_CHANGED. Prefer that state over vanilla's merged Win32 key
-- state when the richer ClassicAPI functions are available. -- state when the richer ClassicAPI functions are available.
@@ -451,6 +491,12 @@ API.IsAltKeyDown = function()
return false return false
end end
-- All frame movers/previews use the same three-key chord. Individual key
-- queries above retain ClassicAPI's left/right state and vanilla fallback.
API.AreMoveModifiersDown = function()
return API.IsShiftKeyDown() and API.IsControlKeyDown() and API.IsAltKeyDown()
end
-- Direct item IDs avoid a class of Turtle WoW custom-item failures caused by -- Direct item IDs avoid a class of Turtle WoW custom-item failures caused by
-- relying exclusively on legacy item-link parsing. -- relying exclusively on legacy item-link parsing.
local function GetItemIDFromLink(link) local function GetItemIDFromLink(link)
@@ -756,23 +802,6 @@ API.SellAllJunkItems = function()
end end
end end
API.IsMounted = function()
return API.playerstate and _G.IsMounted() or false
end
API.Dismount = function()
if API.playerstate then return _G.Dismount() end
end
API.GetShapeshiftFormID = function()
if API.playerstate then return _G.GetShapeshiftFormID() end
return 0
end
API.CancelShapeshiftForm = function()
if API.playerstate then return _G.CancelShapeshiftForm() end
end
API.UnitGUID = function(unit) API.UnitGUID = function(unit)
if API.unitguid then if API.unitguid then
return _G.UnitGUID(unit) return _G.UnitGUID(unit)
@@ -824,3 +853,34 @@ API.UnitInRange = function(unit)
return false, false return false, false
end end
-- Native cooldown rendering landed in ClassicAPI v1.17.1.
API.nativecooldowns = API.classicapi_version >= 11701
API.RegisterUnitEvent = function(frame, eventName, unit1, unit2, unit3)
if type(frame.RegisterUnitEvent) == "function" then
return frame:RegisterUnitEvent(eventName, unit1, unit2, unit3)
end
return frame:RegisterEvent(eventName)
end
API.GetCurrentChatEvent = function()
if type(_G.GetCurrentChatEvent) == "function" then
return _G.GetCurrentChatEvent()
end
end
-- Never opt into persistent storage on the user's behalf.
API.HasPersistentPlayerCache = function()
return API.playercache and type(_G.C_PlayerCache.IsEnabled) == "function"
and type(_G.C_PlayerCache.RememberPlayer) == "function"
and _G.C_PlayerCache.IsEnabled()
end
API.RememberPlayerInfo = function(name, class, guid)
if API.HasPersistentPlayerCache() and name and class and guid then
return _G.C_PlayerCache.RememberPlayer(guid, name, class) and true or false
end
return false
end
+6 -2
View File
@@ -98,14 +98,18 @@ Block("MiniMap Clock")
Block("MiniMap Tweaks") Block("MiniMap Tweaks")
Block("MiniMap Square") Block("MiniMap Square")
Block("Movable Unit Frames") Block("Movable Unit Frames")
Block("Personal Resource Display")
Block("Hide Player Frame Resources")
Block("Hide Player Name")
-- DragonflightUI replaces the stock player/target bars and provides its own -- DragonflightUI replaces the stock player/target bars and provides its own
-- health/power text. Keep the real ShaguTweaks modules disabled there even -- health/power text. Keep the real ShaguTweaks modules disabled there even
-- though Real Health Numbers is once again a genuine core module. -- though Real Health Numbers is once again a genuine core module.
Block("Real Health Numbers") Block("Real Health Numbers")
Block("Unit Frame Big Health") Block("Healing Predictions")
Block("Unit Frame Big Health for Player")
Block("Unit Frame Big Health for Target")
Block("Reduced Actionbar Size") Block("Reduced Actionbar Size")
Block("Unit Frame Class Colors") Block("Unit Frame Class Colors")
Block("Unit Frame Health Colors")
Block("Unit Frame Class Portraits") Block("Unit Frame Class Portraits")
Block("Enemy Castbars") Block("Enemy Castbars")
+229 -12
View File
@@ -4,6 +4,19 @@ local GetExpansion = ShaguTweaks.GetExpansion
local mod = math.mod or mod local mod = math.mod or mod
local current_config = {} local current_config = {}
local darkKey, goldKey = T["Darkened UI"], T["Golden UI"]
local modeKey = T["UI:"]
local modeLabels = { T["Default"], T["Darkened"], T["Golden"] }
local modeOption = { title = modeKey, description = T["UI mode: Default, Darkened UI or Golden UI."], category = T["Interface"] }
local function SelectedMode()
return current_config[darkKey] == 1 and 2 or current_config[goldKey] == 1 and 3 or 1
end
local function SelectMode(mode, dropdown)
current_config[darkKey] = mode == 2 and 1 or 0
current_config[goldKey] = mode == 3 and 1 or 0
UIDropDownMenu_SetSelectedID(dropdown, mode)
UIDropDownMenu_SetText(modeLabels[mode], dropdown)
end
local max_width = 500 local max_width = 500
local max_height = 680 local max_height = 680
@@ -12,6 +25,7 @@ settings:Hide()
table.insert(UISpecialFrames, "AdvancedSettingsGUI") table.insert(UISpecialFrames, "AdvancedSettingsGUI")
settings:SetScript("OnHide", function() settings:SetScript("OnHide", function()
if settings.search then settings.search:ClearFocus() end
ShowUIPanel(GameMenuFrame) ShowUIPanel(GameMenuFrame)
UpdateMicroButtons() UpdateMicroButtons()
end) end)
@@ -34,10 +48,14 @@ settings:SetBackdrop({
settings.scrollframe = CreateFrame('ScrollFrame', 'AdvancedSettingsGUIScrollframe', settings, 'UIPanelScrollFrameTemplate') settings.scrollframe = CreateFrame('ScrollFrame', 'AdvancedSettingsGUIScrollframe', settings, 'UIPanelScrollFrameTemplate')
settings.scrollframe:SetWidth(max_width - 50) settings.scrollframe:SetWidth(max_width - 50)
settings.scrollframe:SetPoint('CENTER', settings, -16, 15) settings.scrollframe:SetPoint('CENTER', settings, -16, -21)
settings.scrollframe:Hide() settings.scrollframe:Hide()
settings.container = CreateFrame("Frame", "AdvancedSettingsGUIContainer", settings) settings.container = CreateFrame("Frame", "AdvancedSettingsGUIContainer", settings)
settings.empty = settings.container:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
settings.empty:SetPoint("TOP", settings.container, "TOP", 0, -24)
settings.empty:SetText(T["No matching options."])
settings.empty:Hide()
settings.title = CreateFrame("Frame", "AdvancedSettingsGUITtitle", settings) settings.title = CreateFrame("Frame", "AdvancedSettingsGUITtitle", settings)
settings.title:SetPoint("TOP", settings, "TOP", 0, 12) settings.title:SetPoint("TOP", settings, "TOP", 0, 12)
@@ -109,25 +127,181 @@ settings.defaults:SetScript("OnClick", function()
settings:defaults() settings:defaults()
end) end)
settings.version = CreateFrame("Button", "AdvancedSettingsGUIVersion", settings, "GameMenuButtonTemplate")
settings.version:SetWidth(80)
settings.version:SetPoint("LEFT", settings.defaults, "RIGHT", 4, 0)
settings.version:SetText("v" .. (GetAddOnMetadata("ShaguTweaks", "Version") or "unknown"))
settings.projectLink = CreateFrame("Frame", "ShaguTweaksProjectLink", settings)
local projectLink = settings.projectLink
projectLink:SetWidth(420)
projectLink:SetHeight(110)
projectLink:SetPoint("CENTER", settings, "CENTER", 0, 0)
projectLink:SetFrameStrata("DIALOG")
projectLink:SetFrameLevel(settings:GetFrameLevel() + 10)
projectLink:EnableMouse(true)
projectLink:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 },
})
projectLink:Hide()
table.insert(UISpecialFrames, "ShaguTweaksProjectLink")
projectLink.label = projectLink:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
projectLink.label:SetPoint("TOP", projectLink, "TOP", 0, -16)
projectLink.label:SetText(T["Press Ctrl+C to copy the link."])
projectLink.edit = CreateFrame("EditBox", "ShaguTweaksProjectLinkEdit", projectLink, "InputBoxTemplate")
projectLink.edit:SetWidth(370)
projectLink.edit:SetHeight(24)
projectLink.edit:SetPoint("TOP", projectLink, "TOP", 0, -33)
projectLink.edit:SetAutoFocus(false)
projectLink.edit:SetScript("OnEscapePressed", function() projectLink:Hide() end)
projectLink.edit:SetScript("OnEnterPressed", function() projectLink:Hide() end)
projectLink:SetScript("OnHide", function() projectLink.edit:ClearFocus() end)
projectLink.close = CreateFrame("Button", nil, projectLink, "GameMenuButtonTemplate")
projectLink.close:SetWidth(90)
projectLink.close:SetPoint("BOTTOM", projectLink, "BOTTOM", 0, 12)
projectLink.close:SetText(CLOSE)
projectLink.close:SetScript("OnClick", function() projectLink:Hide() end)
settings.version:SetScript("OnClick", function()
local color = ShaguTweaks.UIBorder
if color then projectLink:SetBackdropBorderColor(color.r, color.g, color.b, 1) end
projectLink:Show()
projectLink.edit:SetText("https://octowow.st/git/OEVG/ShaguTweaks-ClassicAPI")
projectLink.edit:SetFocus()
projectLink.edit:HighlightText()
end)
local searchIndex = {}
local function MatchesSearch(module, title, category, query)
if query == "" then return true end
if not searchIndex[module] then
local text = title .. " " .. category .. " " .. (module.description or "")
if module.sliders then
for _, slider in ipairs(module.sliders) do
text = text .. " " .. (slider.label or slider.key)
end
end
searchIndex[module] = string.lower(text)
end
return string.find(searchIndex[module], query, 1, true) ~= nil
end
settings.searchLabel = settings:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
settings.searchLabel:SetPoint("TOPLEFT", settings, "TOPLEFT", 22, -36)
settings.searchLabel:SetText(T["Search:"])
settings.search = CreateFrame("EditBox", "AdvancedSettingsGUISearch", settings, "InputBoxTemplate")
settings.search:SetPoint("TOPLEFT", settings, "TOPLEFT", 78, -30)
settings.search:SetWidth(max_width - 139)
settings.search:SetHeight(24)
settings.search:SetAutoFocus(false)
settings.search:SetText("")
settings.search:SetScript("OnEscapePressed", function() this:ClearFocus() end)
settings.search:SetScript("OnEnterPressed", function() this:ClearFocus() end)
settings.search:SetScript("OnTextChanged", function()
settings.query = string.lower(string.gsub(string.gsub(this:GetText() or "", "^%s+", ""), "%s+$", ""))
settings.scrollframe:SetVerticalScroll(0)
local scrollbar = _G.AdvancedSettingsGUIScrollframeScrollBar
if scrollbar then scrollbar:SetValue(0) end
settings:load()
end)
settings.searchClear = CreateFrame("Button", "AdvancedSettingsGUISearchClear", settings, "UIPanelButtonTemplate")
settings.searchClear:SetPoint("LEFT", settings.search, "RIGHT", 8, 0)
settings.searchClear:SetWidth(24)
settings.searchClear:SetHeight(22)
settings.searchClear:SetText("X")
settings.searchClear:SetScript("OnClick", function()
settings.search:SetText("")
settings.search:ClearFocus()
end)
settings.UpdateOptionAvailability = function(self)
if not settings.entries then return end
for title, button in pairs(settings.entries) do
local module = ShaguTweaks.mods[title]
if module and module.requires then
local available = true
for _, required in ipairs(module.requires) do
if current_config[required] ~= 1 then available = false end
end
button.dependencyMet = available
if available then button:Enable() else button:Disable() end
local label = _G["AdvancedSettingsGUI" .. title .. "Text"]
if label then
if available then label:SetTextColor(1, .82, 0)
else label:SetTextColor(.5, .5, .5) end
end
end
end
end
settings.load = function(self) settings.load = function(self)
-- never use more than 3/4 of the screen size -- never use more than 3/4 of the screen size
max_height = math.min(UIParent:GetHeight()/UIParent:GetScale()*0.75, 680) max_height = math.min(UIParent:GetHeight()/UIParent:GetScale()*0.75, 680)
-- update window sizing according to screen -- update window sizing according to screen
settings:SetHeight(max_height) settings:SetHeight(max_height)
settings.scrollframe:SetHeight(max_height - 80) settings.scrollframe:SetHeight(max_height - 152)
settings.container:SetHeight(max_height - 30) settings.container:SetHeight(max_height - 30)
settings.entries = settings.entries or {} settings.entries = settings.entries or {}
-- Reuse widgets, hiding old results before building the filtered layout.
for _, button in pairs(settings.entries) do button:Hide() end
for _, panel in pairs(settings.category or {}) do
panel:Hide()
if panel.button then panel.button:Hide() end
if panel.title then panel.title:Hide() end
end
local expansion = ShaguTweaks:GetExpansion() local expansion = ShaguTweaks:GetExpansion()
-- sort all configs into categories -- sort all configs into categories
local gui = {} local gui = {}
for title, module in pairs(ShaguTweaks.mods) do for title, module in pairs(ShaguTweaks.mods) do
if module.expansions[expansion] then if title ~= darkKey and title ~= goldKey and module.expansions[expansion] then
local category = module.category or T["General"] local category = module.category or T["General"]
gui[category] = gui[category] or {} if MatchesSearch(module, title, category, settings.query or "") then
gui[category][title] = module gui[category] = gui[category] or {}
gui[category][title] = module
end
end
end
local hasMode = expansion == "vanilla" and (ShaguTweaks.mods[darkKey] or ShaguTweaks.mods[goldKey])
if hasMode then
local holder = settings.entries["ui-mode"]
if not holder then
holder = CreateFrame("Frame", nil, settings)
holder:SetWidth(160); holder:SetHeight(32)
holder.label = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
holder.label:SetPoint("LEFT", holder, "LEFT", 0, 0)
holder.label:SetText(modeKey)
local dropdown = CreateFrame("Frame", "AdvancedSettingsGUIUIMode", holder, "UIDropDownMenuTemplate")
dropdown:SetPoint("LEFT", holder, "LEFT", 22, -2)
UIDropDownMenu_SetWidth(90, dropdown)
UIDropDownMenu_Initialize(dropdown, function()
for i, label in ipairs(modeLabels) do
local choice = i
local info = { text = label, checked = SelectedMode() == i,
func = function() SelectMode(choice, dropdown) end }
UIDropDownMenu_AddButton(info)
end
end)
settings.modeDropdown = dropdown
settings.entries["ui-mode"] = holder
holder:SetScript("OnHide", function() if CloseDropDownMenus then CloseDropDownMenus() end end)
end
holder:ClearAllPoints()
holder:SetPoint("TOP", settings, "TOP", 0, -56)
holder:Show()
UIDropDownMenu_SetSelectedID(settings.modeDropdown, SelectedMode())
UIDropDownMenu_SetText(modeLabels[SelectedMode()], settings.modeDropdown)
local color = ShaguTweaks.UIBorder
if color then
for _, suffix in ipairs({"Left", "Middle", "Right"}) do
local texture = _G["AdvancedSettingsGUIUIMode" .. suffix]
if texture then texture:SetVertexColor(color.r, color.g, color.b, 1) end
end
end end
end end
@@ -149,6 +323,9 @@ settings.load = function(self)
-- add category background -- add category background
settings.category = settings.category or {} settings.category = settings.category or {}
settings.category[category] = settings.category[category] or CreateFrame("Frame", nil, settings.container) settings.category[category] = settings.category[category] or CreateFrame("Frame", nil, settings.container)
settings.category[category]:Show()
settings.category[category]:ClearAllPoints()
settings.category[category].buttons = {}
if not previous then if not previous then
settings.category[category]:SetPoint("TOPLEFT", settings.container, "TOPLEFT", spacing, -spacing - topspace) settings.category[category]:SetPoint("TOPLEFT", settings.container, "TOPLEFT", spacing, -spacing - topspace)
@@ -196,6 +373,7 @@ settings.load = function(self)
end end
settings.category[category].button = settings.category[category].button or CreateFrame("Button", nil, settings.container) settings.category[category].button = settings.category[category].button or CreateFrame("Button", nil, settings.container)
settings.category[category].button:Show()
settings.category[category].button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 0, entrysize-4) settings.category[category].button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 0, entrysize-4)
settings.category[category].button:SetWidth(22) settings.category[category].button:SetWidth(22)
settings.category[category].button:SetHeight(22) settings.category[category].button:SetHeight(22)
@@ -205,6 +383,7 @@ settings.load = function(self)
end) end)
settings.category[category].title = settings.category[category].title or CreateFrame("Button", nil, settings.container) settings.category[category].title = settings.category[category].title or CreateFrame("Button", nil, settings.container)
settings.category[category].title:Show()
settings.category[category].title:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 22, entrysize-4) settings.category[category].title:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 22, entrysize-4)
settings.category[category].title:SetWidth(200) settings.category[category].title:SetWidth(200)
settings.category[category].title:SetHeight(entrysize) settings.category[category].title:SetHeight(entrysize)
@@ -225,7 +404,12 @@ settings.load = function(self)
settings.category[category]:SetBackdropBorderColor(.2,.2,.2,1) settings.category[category]:SetBackdropBorderColor(.2,.2,.2,1)
else else
settings.category[category]:SetBackdropColor(.2,.2,.2,1) settings.category[category]:SetBackdropColor(.2,.2,.2,1)
settings.category[category]:SetBackdropBorderColor(.5,.5,.5,1) local border = ShaguTweaks.GoldMode and ShaguTweaks.UIBorder
if border then
settings.category[category]:SetBackdropBorderColor(border.r, border.g, border.b, 1)
else
settings.category[category]:SetBackdropBorderColor(.5,.5,.5,1)
end
end end
-- add category title -- add category title
@@ -234,7 +418,19 @@ settings.load = function(self)
settings.category[category].text:SetAllPoints() settings.category[category].text:SetAllPoints()
settings.category[category].text:SetText(category) settings.category[category].text:SetText(category)
for title, module in ShaguTweaks.spairs(entries) do local orderedTitles = {}
for title in ShaguTweaks.spairs(entries) do
if title ~= "Modern Class Colors" then table.insert(orderedTitles, title) end
end
if entries["Modern Class Colors"] then
local position = table.getn(orderedTitles) + 1
for i, title in ipairs(orderedTitles) do
if title == T["Unit Frame Class Colors"] then position = math.min(i + 2, position); break end
end
table.insert(orderedTitles, position, "Modern Class Colors")
end
for _, title in ipairs(orderedTitles) do
local module = entries[title]
if not settings.entries[title] then if not settings.entries[title] then
settings.entries[title] = CreateFrame("CheckButton", "AdvancedSettingsGUI" .. title, settings.category[category], "OptionsCheckButtonTemplate") settings.entries[title] = CreateFrame("CheckButton", "AdvancedSettingsGUI" .. title, settings.category[category], "OptionsCheckButtonTemplate")
settings.entries[title]:SetHeight(24) settings.entries[title]:SetHeight(24)
@@ -248,6 +444,8 @@ settings.load = function(self)
settings.category[category].buttons[button] = true settings.category[category].buttons[button] = true
button.title = title button.title = title
button:Show()
button:ClearAllPoints()
button:SetChecked(current_config[title] == 1 and true or nil) button:SetChecked(current_config[title] == 1 and true or nil)
button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", mod(entry, 2) == 1 and 17 or 17+200, math.ceil(entry/2-1)*-entrysize-spacing/2) button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", mod(entry, 2) == 1 and 17 or 17+200, math.ceil(entry/2-1)*-entrysize-spacing/2)
@@ -268,11 +466,16 @@ settings.load = function(self)
end) end)
button:SetScript("OnClick", function() button:SetScript("OnClick", function()
if this.dependencyMet == false then
this:SetChecked(current_config[this.title] == 1 and true or nil)
return
end
if this:GetChecked() then if this:GetChecked() then
current_config[this.title] = 1 current_config[this.title] = 1
else else
current_config[this.title] = 0 current_config[this.title] = 0
end end
settings:UpdateOptionAvailability()
end) end)
text:SetText(title) text:SetText(title)
entry = entry + 1 entry = entry + 1
@@ -291,6 +494,8 @@ settings.load = function(self)
end end
local slider = settings.entries[sliderKey] local slider = settings.entries[sliderKey]
slider:Show()
slider:ClearAllPoints()
settings.category[category].buttons[slider] = true settings.category[category].buttons[slider] = true
local yOffset = -(height + spacing / 2 + 16 + (sidx - 1) * sliderHeight) local yOffset = -(height + spacing / 2 + 16 + (sidx - 1) * sliderHeight)
@@ -333,22 +538,32 @@ settings.load = function(self)
required_height = required_height + height + spacing required_height = required_height + height + spacing
end end
settings:UpdateOptionAvailability()
-- set container size to required height -- set container size to required height
if not next(gui) then
if hasMode and MatchesSearch(modeOption, modeKey, modeOption.category, settings.query or "" ) then
settings.empty:Hide()
else settings.empty:Show() end
required_height = 64
else
settings.empty:Hide()
end
settings.container:SetHeight(required_height) settings.container:SetHeight(required_height)
if required_height < max_height then if required_height + 132 <= max_height then
-- reduce base frame if possible -- reduce base frame if possible
settings:SetHeight(required_height + 60) settings:SetHeight(required_height + 132)
settings.container:SetParent(settings) settings.container:SetParent(settings)
settings.container:ClearAllPoints() settings.container:ClearAllPoints()
settings.container:SetPoint("CENTER", settings, 0, 20) settings.container:SetPoint("TOPLEFT", settings, "TOPLEFT", 10, -86)
settings.container:SetWidth(max_width - 20) settings.container:SetWidth(max_width - 20)
settings.scrollframe:Hide() settings.scrollframe:Hide()
elseif required_height > max_height then else
-- set up scrollframe when needed -- set up scrollframe when needed
settings.container:SetParent(settings.scrollframe) settings.container:SetParent(settings.scrollframe)
settings.container:SetHeight(settings.scrollframe:GetHeight()) settings.container:ClearAllPoints()
settings.container:SetPoint("TOPLEFT", settings.scrollframe, "TOPLEFT", 0, 0)
settings.container:SetWidth(settings.scrollframe:GetWidth() + 20) settings.container:SetWidth(settings.scrollframe:GetWidth() + 20)
settings.scrollframe:SetScrollChild(settings.container) settings.scrollframe:SetScrollChild(settings.container)
@@ -380,6 +595,8 @@ settings:SetScript("OnShow", function()
for k, v in pairs(ShaguTweaks_config) do for k, v in pairs(ShaguTweaks_config) do
current_config[k] = v current_config[k] = v
end end
-- Existing saves may contain both old toggles; retain Darkened precedence.
if current_config[darkKey] == 1 then current_config[goldKey] = 0 end
-- copy overwrites into a separate table so Cancel discards slider changes -- copy overwrites into a separate table so Cancel discards slider changes
current_config.overwrites = {} current_config.overwrites = {}
for k, v in pairs(ShaguTweaks_config.overwrites or {}) do for k, v in pairs(ShaguTweaks_config.overwrites or {}) do
+51 -12
View File
@@ -76,18 +76,57 @@ ShaguTweaks.HookScript = function(f, script, func)
end) end)
end end
ShaguTweaks.HookAddonOrVariable = function(addon, func) -- One event listener for deferred addon/global initialization. Deliberately
local lurker = CreateFrame("Frame", nil) -- retain the old three triggers rather than invoking already-ready hooks inline.
lurker.func = func do
lurker:RegisterEvent("ADDON_LOADED") local listener, draining
lurker:RegisterEvent("VARIABLES_LOADED") local pending, ready, checked = {}, {}, {}
lurker:RegisterEvent("PLAYER_ENTERING_WORLD") local function Drain()
lurker:SetScript("OnEvent",function() if draining then return end
if IsAddOnLoaded(addon) or _G[addon] then draining = true
this:func() local previousThis, previousEvent = this, event
this:UnregisterAllEvents() local a1,a2,a3,a4,a5,a6,a7,a8,a9 = arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9
local remaining = 0
local count = table.getn(pending)
for i = 1, count do
local entry = pending[i]
local available = checked[entry.name]
if available == nil then
available = (IsAddOnLoaded(entry.name) or _G[entry.name]) and true or false
checked[entry.name] = available
end
if available then table.insert(ready, entry)
else remaining = remaining + 1; pending[remaining] = entry end
end end
end) for i = count, remaining + 1, -1 do pending[i] = nil end
table.setn(pending, remaining) -- Lua 5.0 caches lengths used by table.insert.
for name in pairs(checked) do checked[name] = nil end
-- Dispatch only the snapshot: callbacks may register more work/load addons.
for i = 1, table.getn(ready) do
local callback = ready[i].callback
ready[i] = nil
this, event = listener, previousEvent
arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9 = a1,a2,a3,a4,a5,a6,a7,a8,a9
listener.func = callback
local ok, failure = pcall(callback, listener)
listener.func = nil
if not ok then pcall(geterrorhandler(), failure) end
end
table.setn(ready, 0)
this, event = previousThis, previousEvent
arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9 = a1,a2,a3,a4,a5,a6,a7,a8,a9
draining = nil
if not next(pending) then listener:UnregisterAllEvents() end
end
ShaguTweaks.HookAddonOrVariable = function(addon, func)
if not listener then listener = CreateFrame("Frame", nil); listener:SetScript("OnEvent", Drain) end
if not next(pending) then
listener:RegisterEvent("ADDON_LOADED")
listener:RegisterEvent("VARIABLES_LOADED")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
end
table.insert(pending, { name = addon, callback = func })
end
end end
ShaguTweaks.hooksecurefunc = function(tbl, name, func, prepend) ShaguTweaks.hooksecurefunc = function(tbl, name, func, prepend)
@@ -452,4 +491,4 @@ ShaguTweaks.GetItemLinkByName = function(name)
return CacheItemLink(name, hyperLink) return CacheItemLink(name, hyperLink)
end end
end end
end end
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
+54 -371
View File
@@ -1,14 +1,10 @@
local _G = ShaguTweaks.GetGlobalEnv() local _G = ShaguTweaks.GetGlobalEnv()
local L = ShaguTweaks.L local L = ShaguTweaks.L
local API = ShaguTweaks.API local API = ShaguTweaks.API
local GetExpansion = ShaguTweaks.GetExpansion
local libtipscan = ShaguTweaks.libtipscan
local libspell = ShaguTweaks.libspell local libspell = ShaguTweaks.libspell
local libtipscan = ShaguTweaks.libtipscan
local hooksecurefunc = ShaguTweaks.hooksecurefunc local hooksecurefunc = ShaguTweaks.hooksecurefunc
local cmatch = ShaguTweaks.cmatch if ShaguTweaks.GetExpansion() ~= "vanilla" then return end
-- return instantly if we're not on a vanilla client
if GetExpansion() ~= "vanilla" then return end
local valid_units = {} local valid_units = {}
valid_units["pet"] = true valid_units["pet"] = true
@@ -32,372 +28,68 @@ for i=1,4 do valid_units["partypet" .. i .. "target"] = true end
for i=1,40 do valid_units["raid" .. i .. "target"] = true end for i=1,40 do valid_units["raid" .. i .. "target"] = true end
for i=1,40 do valid_units["raidpet" .. i .. "target"] = true end for i=1,40 do valid_units["raidpet" .. i .. "target"] = true end
local lastcasttex, lastrank, _ valid_units.focus = true
local scanner valid_units.focustarget = true
local function GetScanner()
if not scanner then
scanner = libtipscan:GetScanner("libcast")
end
return scanner
end
local libcast = CreateFrame("Frame", "ShaguTweaksEnemyCast") local libcast = CreateFrame("Frame", "ShaguTweaksEnemyCast")
ShaguTweaks.libcast = libcast
local player = UnitName("player") local player = UnitName("player")
local _, playerClass = UnitClass("player") local _, playerClass = UnitClass("player")
local useClassicCasts = API and API.casts local useClassicCasts = API and API.casts
local needsCustomHunter = useClassicCasts and playerClass == "HUNTER" local needsCustomHunter = useClassicCasts and playerClass == "HUNTER"
local lastrank
local scanner
local function GetScanner()
if not scanner then scanner = libtipscan:GetScanner("libcast") end
return scanner
end
libcast.db = {}
if needsCustomHunter then libcast.db[player] = {} end
ShaguTweaks.UnitChannelInfo = _G.UnitChannelInfo or function(unit) function libcast:Prune(now)
-- ClassicAPI is authoritative for real unit tokens. Fall through to the for key, cast in pairs(self.db) do
-- legacy database only for the player's custom Hunter cast emulation or for if key ~= player and (not cast.start or not cast.casttime or cast.start + cast.casttime/1000 <= now) then self.db[key] = nil end
-- name/GUID keys maintained for compatibility.
if valid_units[unit] and useClassicCasts then
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill =
API.GetChannelInfo(unit)
if cast then
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end
if unit ~= "player" then return end
end end
end
unit = valid_units[unit] and UnitName(unit) or unit local function ReadCached(key, channel)
local cast = key and libcast.db[key]
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill if not cast or not cast.cast or not cast.start or not cast.casttime then return end
local db = libcast.db[unit] if cast.start + cast.casttime/1000 <= GetTime() then
if key == player then cast.cast = nil else libcast.db[key] = nil end
if db and db.cast and db.start + db.casttime / 1000 > GetTime() then return
if not db.channel then return end
cast = db.cast
nameSubtext = db.rank
text = ""
texture = db.icon
startTime = db.start * 1000
endTime = startTime + db.casttime
isTradeSkill = nil
elseif db then
db.cast = nil
db.rank = nil
db.start = nil
db.casttime = nil
db.icon = nil
db.channel = nil
end end
if (cast.channel and true or false) ~= channel then return end
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill return cast.cast, cast.rank or "", "", cast.icon, cast.start*1000,
cast.start*1000 + cast.casttime, nil
end
ShaguTweaks.UnitCastingInfo = function(unit)
if valid_units[unit] then
local name, rank, text, icon, startTime, endTime, trade = API.GetCastInfo(unit)
if name then return name, rank, text, icon, startTime, endTime, trade end
-- A missing native cast is authoritative. Only the local Hunter's custom
-- emulation may fill a native gap; never consult a name-keyed remote cache.
if unit ~= "player" or not needsCustomHunter then return end
unit = player
end
return ReadCached(unit, false)
end
ShaguTweaks.UnitChannelInfo = function(unit)
if valid_units[unit] then
return API.GetChannelInfo(unit)
end
return ReadCached(unit, true)
end end
ShaguTweaks.UnitCastingInfo = _G.UnitCastingInfo or function(unit) -- No ClassicAPI remote event mirror or combat-text parser. Official castbars
if valid_units[unit] and useClassicCasts then -- query ClassicAPI directly; optional SuperWoW owns timed GUID records only.
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = if needsCustomHunter then
API.GetCastInfo(unit)
if cast then
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end
if unit ~= "player" then return end
end
unit = valid_units[unit] and UnitName(unit) or unit
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
local db = libcast.db[unit]
if db and db.cast and db.start + db.casttime / 1000 > GetTime() then
if db.channel then return end
cast = db.cast
nameSubtext = db.rank or ""
text = ""
texture = db.icon
startTime = db.start * 1000
endTime = startTime + db.casttime
isTradeSkill = nil
elseif db then
db.cast = nil
db.rank = nil
db.start = nil
db.casttime = nil
db.icon = nil
db.channel = nil
end
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end
function libcast:AddAction(mob, spell, channel)
if not mob or not spell then return nil end
local data = L["spells"][spell]
if data and data.t then
local casttime = data.t
local icon = data.icon and string.format("%s%s", "Interface\\Icons\\", data.icon) or nil
-- add cast action to the database
if not self.db[mob] then self.db[mob] = {} end
self.db[mob].cast = spell
self.db[mob].rank = nil
self.db[mob].start = GetTime()
self.db[mob].casttime = casttime
self.db[mob].icon = icon
self.db[mob].channel = channel
return true
end
return nil
end
function libcast:RemoveAction(mob, spell)
if self.db[mob] and ( L["interrupts"][spell] ~= nil or spell == "INTERRUPT" ) then
-- remove cast action to the database
self.db[mob].cast = nil
self.db[mob].rank = nil
self.db[mob].start = nil
self.db[mob].casttime = nil
self.db[mob].icon = nil
self.db[mob].channel = nil
return true
end
return nil
end
-- main data
libcast.db = { [player] = {} }
-- Current ClassicAPI can maintain the legacy name-keyed libcast cache from
-- structured unit spell events. This preserves the public ShaguTweaks
-- UnitCastingInfo/UnitChannelInfo fallback without parsing every combat-text
-- line. Older ClassicAPI builds keep the historical parser below.
local classicRemoteEvents = API and API.casts and API.eventutils
and _G.C_EventUtils
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_START")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_STOP")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_CHANNEL_START")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_CHANNEL_STOP")
if classicRemoteEvents then
local remoteEvents = {
"UNIT_SPELLCAST_START",
"UNIT_SPELLCAST_STOP",
"UNIT_SPELLCAST_FAILED",
"UNIT_SPELLCAST_INTERRUPTED",
"UNIT_SPELLCAST_DELAYED",
"UNIT_SPELLCAST_CHANNEL_START",
"UNIT_SPELLCAST_CHANNEL_STOP",
"UNIT_SPELLCAST_CHANNEL_UPDATE",
}
for index = 1, table.getn(remoteEvents) do
local eventName = remoteEvents[index]
if _G.C_EventUtils.IsEventValid(eventName) then
libcast:RegisterEvent(eventName)
end
end
else
libcast:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF")
libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_BUFF")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF")
end
-- Full legacy player tracking is only needed when ClassicAPI cast data is
-- unavailable. On current ClassicAPI, only Hunters keep the stop/fail events
-- required to end the Aimed Shot / Multi-Shot custom cast emulation.
if not useClassicCasts then
libcast:RegisterEvent("SPELLCAST_START")
libcast:RegisterEvent("SPELLCAST_STOP") libcast:RegisterEvent("SPELLCAST_STOP")
libcast:RegisterEvent("SPELLCAST_FAILED") libcast:RegisterEvent("SPELLCAST_FAILED")
libcast:RegisterEvent("SPELLCAST_INTERRUPTED") libcast:RegisterEvent("SPELLCAST_INTERRUPTED")
libcast:RegisterEvent("SPELLCAST_DELAYED") libcast:SetScript("OnEvent", function()
libcast:RegisterEvent("SPELLCAST_CHANNEL_START") local cast = libcast.db[player]
libcast:RegisterEvent("SPELLCAST_CHANNEL_STOP") if cast then cast.cast = nil end
libcast:RegisterEvent("SPELLCAST_CHANNEL_UPDATE") lastrank = nil
elseif needsCustomHunter then end)
libcast:RegisterEvent("SPELLCAST_STOP")
libcast:RegisterEvent("SPELLCAST_FAILED")
libcast:RegisterEvent("SPELLCAST_INTERRUPTED")
end
local function ClearClassicRemote(unit)
if not unit or unit == "player" then return end
local name = UnitName(unit)
if not name then return end
local cast = libcast.db[name]
if cast then
cast.cast = nil
cast.rank = nil
cast.start = nil
cast.casttime = nil
cast.icon = nil
cast.channel = nil
end
end
local function UpdateClassicRemote(unit, channel)
if not unit or unit == "player" then return end
local name = UnitName(unit)
if not name then return end
local spell, rank, text, icon, startTime, endTime
if channel then
spell, rank, text, icon, startTime, endTime = API.GetChannelInfo(unit)
else
spell, rank, text, icon, startTime, endTime = API.GetCastInfo(unit)
end
if not spell or not startTime or not endTime then
ClearClassicRemote(unit)
return
end
local cast = libcast.db[name]
if not cast then
cast = {}
libcast.db[name] = cast
end
cast.cast = spell
cast.rank = rank
cast.start = startTime / 1000
cast.casttime = endTime - startTime
cast.icon = icon
cast.channel = channel and true or nil
end
libcast:SetScript("OnEvent", function()
-- Keep the public legacy cache current from ClassicAPI without combat text.
if classicRemoteEvents and strfind(event, "UNIT_SPELLCAST_", 1) then
if arg1 == "player" then return end
if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_DELAYED" then
UpdateClassicRemote(arg1, false)
elseif event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
UpdateClassicRemote(arg1, true)
elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_FAILED"
or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_CHANNEL_STOP"
then
ClearClassicRemote(arg1)
end
return
end
-- Fill database with player casts
if event == "SPELLCAST_START" then
local icon = L["spells"][arg1] and L["spells"][arg1].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg1].icon) or lastcasttex
-- add cast action to the database
this.db[player].cast = arg1
this.db[player].rank = lastrank
this.db[player].start = GetTime()
this.db[player].casttime = arg2
this.db[player].icon = icon
this.db[player].channel = nil
if not L["spells"][arg1] or not L["spells"][arg1].icon or not L["spells"][arg1].t then
L["spells"][arg1] = L["spells"][arg1] or { }
L["spells"][arg1].icon = L["spells"][arg1].icon or icon
L["spells"][arg1].t = L["spells"][arg1].t or arg2
end
lastcasttex, lastrank = nil, nil
elseif event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
if this.db[player] and not this.db[player].channel then
-- remove cast action to the database
this.db[player].cast = nil
this.db[player].rank = nil
this.db[player].rank = nil
this.db[player].start = nil
this.db[player].casttime = nil
this.db[player].icon = nil
this.db[player].channel = nil
else
lastcasttex, lastrank = nil, nil
end
elseif event == "SPELLCAST_DELAYED" then
if this.db[player].cast then
this.db[player].start = this.db[player].start + arg1/1000
end
elseif event == "SPELLCAST_CHANNEL_START" then
-- add cast action to the database
this.db[player].cast = arg2
this.db[player].rank = lastrank
this.db[player].start = GetTime()
this.db[player].casttime = arg1
this.db[player].icon = L["spells"][arg2] and L["spells"][arg2].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg2].icon) or lastcasttex
this.db[player].channel = true
lastcasttex, lastrank = nil, nil
elseif event == "SPELLCAST_CHANNEL_STOP" then
if this.db[player] and this.db[player].channel then
-- remove cast action to the database
this.db[player].cast = nil
this.db[player].rank = nil
this.db[player].start = nil
this.db[player].casttime = nil
this.db[player].icon = nil
this.db[player].channel = nil
end
elseif event == "SPELLCAST_CHANNEL_UPDATE" then
if this.db[player].cast then
this.db[player].start = -this.db[player].casttime/1000 + GetTime() + arg1/1000
end
-- Fill database with environmental casts
elseif arg1 then
local mob, spell, _
-- (.+) begins to cast (.+).
mob, spell = cmatch(arg1, SPELLCASTOTHERSTART)
if libcast:AddAction(mob, spell) then return end
-- (.+) begins to perform (.+).
mob, spell = cmatch(arg1, SPELLPERFORMOTHERSTART)
if libcast:AddAction(mob, spell) then return end
-- (.+) gains (.+).
mob, spell = cmatch(arg1, AURAADDEDOTHERHELPFUL)
if libcast:RemoveAction(mob, spell) then return end
-- (.+) is afflicted by (.+).
mob, spell = cmatch(arg1, AURAADDEDOTHERHARMFUL)
if libcast:RemoveAction(mob, spell) then return end
-- Your (.+) hits (.+) for (%d+).
spell, mob = cmatch(arg1, SPELLLOGSELFOTHER)
if libcast:RemoveAction(mob, spell) then return end
-- Your (.+) crits (.+) for (%d+).
spell, mob = cmatch(arg1, SPELLLOGCRITSELFOTHER)
if libcast:RemoveAction(mob, spell) then return end
-- (.+)'s (.+) %a hits (.+) for (%d+).
_, spell, mob = cmatch(arg1, SPELLLOGOTHEROTHER)
if libcast:RemoveAction(mob, spell) then return end
-- (.+)'s (.+) %a crits (.+) for (%d+).
_, spell, mob = cmatch(arg1, SPELLLOGCRITOTHEROTHER)
if libcast:RemoveAction(mob, spell) then return end
-- You interrupt (.+)'s (.+).
mob, spell = cmatch(arg1, SPELLINTERRUPTSELFOTHER)
if libcast:RemoveAction(mob, spell) then return end
-- (.+) interrupts (.+)'s (.+).
_, mob, spell = cmatch(arg1, SPELLINTERRUPTOTHEROTHER)
if libcast:RemoveAction(mob, spell) then return end
end
end)
--[[ Custom Casts --[[ Custom Casts
Enable Castbars for spells that don't have a castbar by default Enable Castbars for spells that don't have a castbar by default
(e.g Multi-Shot and Aimed Shot) (e.g Multi-Shot and Aimed Shot)
@@ -508,15 +200,10 @@ local function CastCustom(spell)
end end
end end
if not useClassicCasts or needsCustomHunter then
hooksecurefunc("UseContainerItem", function(id, index)
lastcasttex = GetContainerItemInfo(id, index)
end)
hooksecurefunc("CastSpell", function(id, bookType) hooksecurefunc("CastSpell", function(id, bookType)
local spellName, rank = GetSpellName(id, bookType) local spellName, rank = GetSpellName(id, bookType)
if not GetCustomCast(spellName) then return end
lastrank = rank lastrank = rank
lastcasttex = GetSpellTexture(id, bookType)
if GetSpellCooldown(id, bookType) ~= 0 then if GetSpellCooldown(id, bookType) ~= 0 then
CastCustom(spellName) CastCustom(spellName)
@@ -555,12 +242,11 @@ hooksecurefunc("UseAction", function(slot, target, button)
-- CastSpellByName. Do not guess it from the macro's first cast line. -- CastSpellByName. Do not guess it from the macro's first cast line.
return return
elseif actionType == "item" then elseif actionType == "item" then
lastcasttex = GetActionTexture(slot)
lastrank = nil lastrank = nil
return return
elseif actionType == "spell" and actionID then elseif actionType == "spell" and actionID then
local spellName, rank, icon = API.GetSpellInfo(actionID) local spellName, rank = API.GetSpellInfo(actionID)
lastcasttex = icon or GetActionTexture(slot) if not GetCustomCast(spellName) then return end
lastrank = rank lastrank = rank
if not IsCurrentAction(slot) then return end if not IsCurrentAction(slot) then return end
CastCustom(spellName) CastCustom(spellName)
@@ -575,10 +261,7 @@ hooksecurefunc("UseAction", function(slot, target, button)
tip:SetAction(slot) tip:SetAction(slot)
local spellName, rank = tip:Line(1) local spellName, rank = tip:Line(1)
lastcasttex = GetActionTexture(slot)
lastrank = rank lastrank = rank
CastCustom(spellName) CastCustom(spellName)
end) end)
end end
ShaguTweaks.libcast = libcast
-378
View File
@@ -1,378 +0,0 @@
local _G = ShaguTweaks.GetGlobalEnv()
local L = ShaguTweaks.L
local API = ShaguTweaks.API
local GetExpansion = ShaguTweaks.GetExpansion
local libtipscan = ShaguTweaks.libtipscan
local libspell = ShaguTweaks.libspell
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local QueueFunction = ShaguTweaks.QueueFunction
local cmatch = ShaguTweaks.cmatch
-- return instantly if we're not on a vanilla client
if GetExpansion() ~= "vanilla" then return end
-- fix a typo (missing $) in ruRU capture index
if GetLocale() == "ruRU" then
SPELLREFLECTSELFOTHER = gsub(SPELLREFLECTSELFOTHER, "%%2s", "%%2%$s")
end
local libdebuff = CreateFrame("Frame", "ShaguTweaksDebuffsScanner", UIParent)
local scanner
local function GetScanner()
if not scanner then
scanner = libtipscan:GetScanner("libdebuff")
end
return scanner
end
local _, class = UnitClass("player")
local lastspell
local useClassicAuraTiming = API and API.aurapositional and API.UnitDebuff
function libdebuff:GetDuration(effect, rank)
if L["debuffs"][effect] then
local rank = rank and tonumber((string.gsub(rank, RANK .. " ", ""))) or 0
local rank = L["debuffs"][effect][rank] and rank or libdebuff:GetMaxRank(effect)
local duration = L["debuffs"][effect][rank]
if effect == L["dyndebuffs"]["Rupture"] then
-- Rupture: +2 sec per combo point
duration = duration + GetComboPoints()*2
elseif effect == L["dyndebuffs"]["Kidney Shot"] then
-- Kidney Shot: +1 sec per combo point
duration = duration + GetComboPoints()*1
elseif effect == L["dyndebuffs"]["Demoralizing Shout"] then
-- Booming Voice: 10% per talent
local _,_,_,_,count = GetTalentInfo(2,1)
if count and count > 0 then duration = duration + ( duration / 100 * (count*10)) end
elseif effect == L["dyndebuffs"]["Shadow Word: Pain"] then
-- Improved Shadow Word: Pain: +3s per talent
local _,_,_,_,count = GetTalentInfo(3,4)
if count and count > 0 then duration = duration + count * 3 end
elseif effect == L["dyndebuffs"]["Frostbolt"] then
-- Permafrost: +1s per talent
local _,_,_,_,count = GetTalentInfo(3,7)
if count and count > 0 then duration = duration + count end
elseif effect == L["dyndebuffs"]["Gouge"] then
-- Improved Gouge: +.5s per talent
local _,_,_,_,count = GetTalentInfo(2,1)
if count and count > 0 then duration = duration + (count*.5) end
end
return duration
else
return 0
end
end
function libdebuff:UpdateDuration(unit, unitlevel, effect, duration)
if not unit or not effect or not duration then return end
unitlevel = unitlevel or 0
if libdebuff.objects[unit] and libdebuff.objects[unit][unitlevel] and libdebuff.objects[unit][unitlevel][effect] then
libdebuff.objects[unit][unitlevel][effect].duration = duration
end
end
function libdebuff:GetMaxRank(effect)
local max = 0
for id in pairs(L["debuffs"][effect]) do
if id > max then max = id end
end
return max
end
function libdebuff:UpdateUnits()
TargetDebuffButton_Update()
end
function libdebuff:AddPending(unit, unitlevel, effect, duration)
if not unit then return end
if not L["debuffs"][effect] then return end
duration = duration or libdebuff:GetDuration(effect)
if duration > 0 and libdebuff.pending[3] ~= effect then
libdebuff.pending[1] = unit
libdebuff.pending[2] = unitlevel or 0
libdebuff.pending[3] = effect
libdebuff.pending[4] = duration
end
end
function libdebuff:RemovePending()
libdebuff.pending[1] = nil
libdebuff.pending[2] = nil
libdebuff.pending[3] = nil
libdebuff.pending[4] = nil
end
function libdebuff:PersistPending(effect)
if not libdebuff.pending[3] then return end
if libdebuff.pending[3] == effect or ( effect == nil and libdebuff.pending[3] ) then
libdebuff:AddEffect(libdebuff.pending[1], libdebuff.pending[2], libdebuff.pending[3], libdebuff.pending[4])
libdebuff:RemovePending()
end
end
function libdebuff:RevertLastAction()
lastspell.start = lastspell.start_old
lastspell.start_old = nil
libdebuff:UpdateUnits()
end
function libdebuff:AddEffect(unit, unitlevel, effect, duration, deferUpdate)
if not unit or not effect then return end
unitlevel = unitlevel or 0
if not libdebuff.objects[unit] then libdebuff.objects[unit] = {} end
if not libdebuff.objects[unit][unitlevel] then libdebuff.objects[unit][unitlevel] = {} end
if not libdebuff.objects[unit][unitlevel][effect] then libdebuff.objects[unit][unitlevel][effect] = {} end
-- save current effect as lastspell
lastspell = libdebuff.objects[unit][unitlevel][effect]
libdebuff.objects[unit][unitlevel][effect].effect = effect
libdebuff.objects[unit][unitlevel][effect].start_old = libdebuff.objects[unit][unitlevel][effect].start
libdebuff.objects[unit][unitlevel][effect].start = GetTime()
libdebuff.objects[unit][unitlevel][effect].duration = duration or libdebuff:GetDuration(effect)
if not deferUpdate then
libdebuff:UpdateUnits()
end
end
-- ClassicAPI exposes aura identity and timing directly. The old periodic
-- combat-text parser only exists to reconstruct off-target applications on
-- older ClassicAPI builds; current ClassicAPI does not register those noisy
-- events at all.
if not useClassicAuraTiming then
libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE")
libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE")
libdebuff:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER")
libdebuff:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
libdebuff:RegisterEvent("SPELLCAST_STOP")
end
-- Target changes and aura updates are still useful for the rare fallback case
-- where ClassicAPI knows an aura's duration but could not observe its original
-- application and therefore has no expirationTime.
libdebuff:RegisterEvent("PLAYER_TARGET_CHANGED")
libdebuff:RegisterEvent("UNIT_AURA")
-- register seal handler
if class == "PALADIN" then
libdebuff:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS")
end
-- Remove Pending
libdebuff.rp = { SPELLIMMUNESELFOTHER, IMMUNEDAMAGECLASSSELFOTHER,
SPELLMISSSELFOTHER, SPELLRESISTSELFOTHER, SPELLEVADEDSELFOTHER,
SPELLDODGEDSELFOTHER, SPELLDEFLECTEDSELFOTHER, SPELLREFLECTSELFOTHER,
SPELLPARRIEDSELFOTHER, SPELLLOGABSORBSELFOTHER }
libdebuff.objects = {}
libdebuff.pending = {}
-- Gather Data by Events
libdebuff:SetScript("OnEvent", function()
-- paladin seal refresh
if event == "CHAT_MSG_COMBAT_SELF_HITS" then
local hit = cmatch(arg1, COMBATHITSELFOTHER)
local crit = cmatch(arg1, COMBATHITCRITSELFOTHER)
if hit or crit then
for seal in L["judgements"] do
local name = UnitName("target")
local level = UnitLevel("target")
if name and libdebuff.objects[name] then
if level and libdebuff.objects[name][level] and libdebuff.objects[name][level][seal] then
libdebuff:AddEffect(name, level, seal)
elseif libdebuff.objects[name][0] and libdebuff.objects[name][0][seal] then
libdebuff:AddEffect(name, 0, seal)
end
end
end
end
-- Add Combat Log
elseif event == "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE" or event == "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE" then
local unit, effect = cmatch(arg1, AURAADDEDOTHERHARMFUL)
if unit and effect then
local unitlevel = UnitName("target") == unit and UnitLevel("target") or 0
if not libdebuff.objects[unit] or not libdebuff.objects[unit][unitlevel] or not libdebuff.objects[unit][unitlevel][effect] then
libdebuff:AddEffect(unit, unitlevel, effect)
end
end
-- Add Missing Buffs by Iteration
elseif ( event == "UNIT_AURA" and arg1 == "target" ) or event == "PLAYER_TARGET_CHANGED" then
local unit = UnitName("target")
if not unit then return end
local unitlevel = UnitLevel("target") or 0
local changed = false
for i=1, 16 do
local effect, rank, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff("target", i)
-- abort when no further debuff was found
if not texture then break end
if effect and effect ~= "" and (not timeleft or timeleft < 0) then
-- Exact ClassicAPI timing needs no legacy cache at all. Only construct
-- an estimate when expirationTime is genuinely unavailable.
if not libdebuff.objects[unit] or not libdebuff.objects[unit][unitlevel] or not libdebuff.objects[unit][unitlevel][effect] then
local fallbackDuration = duration and duration > 0 and duration or nil
libdebuff:AddEffect(unit, unitlevel, effect, fallbackDuration, true)
changed = true
end
end
end
if changed then
libdebuff:UpdateUnits()
end
-- Update Pending Spells
elseif event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" or event == "CHAT_MSG_SPELL_SELF_DAMAGE" then
-- Most combat messages cannot affect libdebuff. Avoid running the whole
-- miss/resist/immune pattern set unless there is state to repair.
if not libdebuff.pending[3] and not (lastspell and lastspell.start_old) then
return
end
for _, msg in pairs(libdebuff.rp) do
local effect = cmatch(arg1, msg)
if effect and libdebuff.pending[3] == effect then
-- instant removal of the pending spell
libdebuff:RemovePending()
return
elseif effect and lastspell and lastspell.start_old and lastspell.effect == effect then
-- late removal of debuffs (e.g hunter arrows as they hit late)
libdebuff:RevertLastAction()
return
end
end
elseif event == "SPELLCAST_STOP" then
if libdebuff.pending[3] then
QueueFunction(libdebuff.PersistPending)
end
end
end)
-- Gather Data by User Actions.
-- Current ClassicAPI gets exact application timing from C_UnitAuras, so these
-- legacy prediction hooks are installed only when that timing surface is absent.
if not useClassicAuraTiming then
hooksecurefunc("CastSpell", function(id, bookType)
local effect, rank = GetSpellName(id, bookType)
if not effect or not L["debuffs"][effect] then return end
local duration = libdebuff:GetDuration(effect, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration)
end)
hooksecurefunc("CastSpellByName", function(effect, target)
if not effect then return end
-- Strip an explicit rank before checking the duration database. Reject
-- ordinary damage/heal casts before doing any spell metadata work.
local _, _, baseEffect, explicitRank = string.find(effect, "(.+)%((.+)%)")
local name = baseEffect or effect
if not L["debuffs"][name] then return end
if libdebuff.pending[3] == name then return end
local rank = explicitRank
if not rank then
_, rank = libspell.GetSpellInfo(name)
end
local duration = libdebuff:GetDuration(name, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), name, duration)
end)
hooksecurefunc("UseAction", function(slot, target, button)
-- Macros are intentionally left to CastSpellByName: conditional macros can
-- select a different spell than their first /cast line.
if API and API.actioninfo and API.GetActionInfo then
local actionType, actionID = API.GetActionInfo(slot)
if actionType == "macro" or actionType == "item" then return end
if actionType == "spell" and actionID then
if not IsCurrentAction(slot) then return end
local effect, rank = API.GetSpellInfo(actionID)
if not effect or not L["debuffs"][effect] then return end
local duration = libdebuff:GetDuration(effect, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration)
return
elseif actionType then
return
end
end
if GetActionText(slot) or not IsCurrentAction(slot) then return end
local tip = GetScanner()
tip:SetAction(slot)
local effect, rank = tip:Line(1)
if not effect or not L["debuffs"][effect] then return end
local duration = libdebuff:GetDuration(effect, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration)
end)
end
function libdebuff:UnitDebuff(unit, id)
local unitname = UnitName(unit)
local unitlevel = UnitLevel(unit)
local effect, texture, stacks, dtype
local duration, timeleft = nil, -1
local rank = nil -- no backport
-- ClassicAPI returns positional aura metadata without allocating a table.
-- When it observed the application, use its exact duration/expiration
-- immediately and bypass the legacy name/level estimate entirely.
if useClassicAuraTiming then
local expirationTime
effect, texture, stacks, dtype, duration, expirationTime =
API.UnitDebuff(unit, id)
if texture and duration and duration > 0
and expirationTime and expirationTime > 0
then
timeleft = expirationTime - GetTime()
if timeleft < 0 then timeleft = 0 end
return effect, rank, texture, stacks, dtype, duration, timeleft
end
end
-- Plain Vanilla / older ClassicAPI compatibility. Also acts as a defensive
-- fallback if a positional API unexpectedly failed to resolve a visible aura.
if not texture then
texture, stacks, dtype = UnitDebuff(unit, id)
end
if texture and (not effect or effect == "") then
local tip = GetScanner()
tip:SetUnitDebuff(unit, id)
effect = tip:Line(1) or ""
end
if libdebuff.objects[unitname] and libdebuff.objects[unitname][unitlevel] and libdebuff.objects[unitname][unitlevel][effect] then
-- clean up cache
if libdebuff.objects[unitname][unitlevel][effect].duration and libdebuff.objects[unitname][unitlevel][effect].duration + libdebuff.objects[unitname][unitlevel][effect].start < GetTime() then
libdebuff.objects[unitname][unitlevel][effect] = nil
else
duration = libdebuff.objects[unitname][unitlevel][effect].duration
timeleft = duration + libdebuff.objects[unitname][unitlevel][effect].start - GetTime()
end
-- no level data
elseif libdebuff.objects[unitname] and libdebuff.objects[unitname][0] and libdebuff.objects[unitname][0][effect] then
-- clean up cache
if libdebuff.objects[unitname][0][effect].duration and libdebuff.objects[unitname][0][effect].duration + libdebuff.objects[unitname][0][effect].start < GetTime() then
libdebuff.objects[unitname][0][effect] = nil
else
duration = libdebuff.objects[unitname][0][effect].duration
timeleft = duration + libdebuff.objects[unitname][0][effect].start - GetTime()
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft
end
ShaguTweaks.libdebuff = libdebuff
+4 -29
View File
@@ -39,8 +39,8 @@ local function StopSampling()
end end
local function StartSampling() local function StartSampling()
libhealth:RegisterEvent("UNIT_COMBAT") ShaguTweaks.API.RegisterUnitEvent(libhealth, "UNIT_COMBAT", "target")
libhealth:RegisterEvent("UNIT_HEALTH") ShaguTweaks.API.RegisterUnitEvent(libhealth, "UNIT_HEALTH", "target")
end end
local function SelectTarget() local function SelectTarget()
@@ -76,27 +76,8 @@ local function GetCachedHealth(unitstr, cur, max)
return cur, max return cur, max
end end
-- ClassicAPI can expose the real health deficit even when stock UnitHealth() -- UnitHealthMissing follows the same units as UnitHealth/UnitHealthMax.
-- still reports a percentage. Use that as an immediate estimate when possible. -- A percentage deficit cannot establish absolute HP; use the combat cache.
-- At 100% there is no deficit to derive a maximum from, so the historical
-- combat estimator/cache remains useful.
local function GetMissingHealthEstimate(unitstr, cur, max)
if max ~= 100 or cur >= 100 then return end
if type(_G.UnitHealthMissing) ~= "function" then return end
local missing = _G.UnitHealthMissing(unitstr)
local lostPercent = 100 - cur
if not missing or missing <= 0 or lostPercent <= 0 then return end
local estimatedMax = ceil(missing / lostPercent * 100)
if estimatedMax < 100 then return end
local estimatedCur = estimatedMax - missing
if estimatedCur < 0 then estimatedCur = 0 end
return estimatedCur, estimatedMax, true
end
function libhealth:GetUnitHealth(unitstr) function libhealth:GetUnitHealth(unitstr)
-- Preserve the old public library behavior for external consumers while -- Preserve the old public library behavior for external consumers while
-- keeping the estimator lazy: the first actual query activates it. -- keeping the estimator lazy: the first actual query activates it.
@@ -112,12 +93,6 @@ function libhealth:GetUnitHealth(unitstr)
return cur, max, true return cur, max, true
end end
local missingCur, missingMax, missingKnown =
GetMissingHealthEstimate(unitstr, cur, max)
if missingKnown then
return missingCur, missingMax, true
end
return GetCachedHealth(unitstr, cur, max) return GetCachedHealth(unitstr, cur, max)
end end
+2 -13
View File
@@ -22,7 +22,6 @@ local function IsNamePlate(frame)
end end
local registry = {} local registry = {}
local lastParentCount = -1
local libnameplate = CreateFrame("Frame", nil, UIParent) local libnameplate = CreateFrame("Frame", nil, UIParent)
ShaguTweaks.libnameplate = libnameplate ShaguTweaks.libnameplate = libnameplate
libnameplate.OnInit = {} libnameplate.OnInit = {}
@@ -78,13 +77,6 @@ local function DiscoverNameplates()
InitializePlate(children[index]) InitializePlate(children[index])
end end
lastParentCount = count
end
local function ScanNameplatesLegacy()
local count = WorldFrame:GetNumChildren()
if count == lastParentCount then return end
DiscoverNameplates()
end end
local eventDriver = CreateFrame("Frame") local eventDriver = CreateFrame("Frame")
@@ -110,10 +102,9 @@ function libnameplate:HasConsumers()
end end
function libnameplate:Enable() function libnameplate:Enable()
if self.enabled then return end if self.enabled or not API.IsSupportedRuntime() or not HasClassicNameplateEvents() then return end
self.enabled = true self.enabled = true
if HasClassicNameplateEvents() then
-- Never enumerate WorldFrame children synchronously from the ClassicAPI -- Never enumerate WorldFrame children synchronously from the ClassicAPI
-- nameplate event path. Defer discovery to the next rendered frame. -- nameplate event path. Defer discovery to the next rendered frame.
ScheduleDiscovery() ScheduleDiscovery()
@@ -124,9 +115,7 @@ function libnameplate:Enable()
end) end)
self:SetScript("OnUpdate", nil) self:SetScript("OnUpdate", nil)
else
self:SetScript("OnUpdate", ScanNameplatesLegacy)
end
end end
function libnameplate:EnableIfNeeded() function libnameplate:EnableIfNeeded()
+259 -564
View File
@@ -1,592 +1,287 @@
-- ClassicAPI supplies cast timing, not incoming heal amounts. Learn our own
-- direct heals and receive explicit-duration vanilla HealComm announcements.
local _G = ShaguTweaks.GetGlobalEnv() local _G = ShaguTweaks.GetGlobalEnv()
local L = ShaguTweaks.L
local API = ShaguTweaks.API local API = ShaguTweaks.API
local GetExpansion = ShaguTweaks.GetExpansion local P = { pending = {}, enabled = false }
local libtipscan = ShaguTweaks.libtipscan ShaguTweaks.libpredict = P
local libspell = ShaguTweaks.libspell local learned, driver, player, sentGUID, sentTarget, sentSpell, activeGUID
local hooksecurefunc = ShaguTweaks.hooksecurefunc local elapsed = 0
local strsplit = ShaguTweaks.strsplit local equipment, equipmentReady = {}, false
local cmatch = ShaguTweaks.cmatch local sharedHooks = {}
local hotAmounts, hotUnits = {}, {}
local senttarget local trackedUnits = {"player", "target"}
local heals, ress, events = {}, {}, {} local function HotTotal(unit)
local state = hotUnits[unit]
local PRAYER_OF_HEALING if not state or state.guid ~= UnitGUID(unit) then return 0 end
do -- Prayer of Healing local total, now = 0, GetTime()
local locales = { for _, hot in ipairs(state.entries) do if hot.expires > now then total = total + hot.amount end end
["deDE"] = "Gebet der Heilung", return total
["enUS"] = "Prayer of Healing",
["esES"] = "Rezo de curación",
["frFR"] = "Prière de soins",
["koKR"] = "치유의 기원",
["ruRU"] = "Молитва исцеления",
["zhCN"] = "治疗祷言",
}
PRAYER_OF_HEALING = locales[GetLocale()] or locales["enUS"]
end end
local function HotPending()
local libpredict = CreateFrame("Frame")
libpredict.enabled = false
libpredict:SetScript("OnEvent", function()
if event == "CHAT_MSG_ADDON" and (arg1 == "HealComm" or arg1 == "CTRA") then
this:ParseChatMessage(arg4, arg2, arg1)
elseif event == "UNIT_HEALTH" then
local name = UnitName(arg1)
if ress[name] and not UnitIsDeadOrGhost(arg1) then
ress[UnitName(arg1)] = nil
end
end
end)
local function UpdateTimeouts()
local now = GetTime() local now = GetTime()
local pending for _, state in pairs(hotUnits) do
for _, hot in ipairs(state.entries) do if hot.expires > now then return true end end
for timestamp in pairs(events) do end
if now >= timestamp then return false
events[timestamp] = nil end
else local function EquipmentChanged()
pending = true if not _G.GetInventoryItemLink then return false end
local changed = false
for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot)
if equipment[slot] ~= link then changed = true end
equipment[slot] = link
end
local ready = equipmentReady
equipmentReady = true
return ready and changed
end
local function Notify(target) if P.changed then P.changed(target) end end
local function Expire()
elapsed = elapsed + (arg1 or 0)
if elapsed < .1 then return end
elapsed = 0
local now, any = GetTime(), false
for target, senders in pairs(P.pending) do
local changed, remaining = false, false
for sender, heal in pairs(senders) do
if heal.expires <= now then senders[sender] = nil; changed = true
else remaining = true; any = true end
end
if not remaining then P.pending[target] = nil end
if changed then Notify(target) end
end
for _, state in pairs(hotUnits) do
for i = table.getn(state.entries), 1, -1 do
if state.entries[i].expires <= now then
table.remove(state.entries, i)
Notify(state.name)
else any = true end
end end
end end
if not any then driver:SetScript("OnUpdate", nil) end
-- No prediction timeout is pending: completely remove OnUpdate until the
-- next AddEvent() instead of waking this library on every rendered frame.
if not pending then
libpredict.timeoutActive = nil
libpredict:SetScript("OnUpdate", nil)
end
end end
function P:HealStop(sender)
function libpredict:ParseComm(sender, msg) if not sender then return end
local msgtype, target, heal, time for target, senders in pairs(self.pending) do
if senders[sender] then
if msg == "Healstop" or msg == "GrpHealstop" then senders[sender] = nil
msgtype = "Stop" if not next(senders) then self.pending[target] = nil end
elseif msg == "Resurrection/stop/" then Notify(target)
msgtype = "RessStop" end
elseif msg then end
local msgobj = {strsplit("/", msg)} if driver and not next(self.pending) and not HotPending() then driver:SetScript("OnUpdate", nil) end
end
if msgobj and msgobj[1] and msgobj[2] then function P:Heal(sender, target, amount, milliseconds)
-- legacy healcomm object amount, milliseconds = tonumber(amount), tonumber(milliseconds)
if msgobj[1] == "GrpHealdelay" or msgobj[1] == "Healdelay" then if not self.enabled or self.external or not sender or not target or target == "" or not amount or not milliseconds
msgtype, time = "Delay", msgobj[2] or amount <= 0 or amount > 10000000 or milliseconds <= 0 or milliseconds > 600000 then return end
self:HealStop(sender)
local senders = self.pending[target]
if not senders then senders = {}; self.pending[target] = senders end
senders[sender] = { amount = amount, expires = GetTime() + milliseconds / 1000 }
Notify(target)
elapsed = 0
driver:SetScript("OnUpdate", Expire)
end
function P:UnitGetIncomingHeals(unit)
if not self.enabled or not UnitExists(unit) or UnitIsDeadOrGhost(unit) then return 0 end
if self.external then
local guid = UnitGUID(unit)
local amount = guid and self.external.GetIncomingHealing(guid)
return type(amount) == "number" and amount > 0 and amount or 0
end
local senders = self.pending[UnitName(unit)]
local sum, now = HotTotal(unit), GetTime()
if senders then
for _, heal in pairs(senders) do if heal.expires > now then sum = sum + heal.amount end end
end
return sum
end
local function RefreshHots(unit)
if unit ~= "player" and unit ~= "target" then return end
local state = hotUnits[unit]
if not state then state = {entries={}}; hotUnits[unit] = state end
state.guid, state.name = UnitGUID(unit), UnitName(unit)
local auras, count, now = _G.C_UnitAuras, 0, GetTime()
if auras and UnitExists(unit) and not UnitIsDeadOrGhost(unit) and UnitCanAssist("player", unit) then
for index = 1, 48 do
local name, duration, expires, source, spell, guid
if type(auras.UnitBuff) == "function" then
local n, _, _, _, d, e, s, _, _, id = auras.UnitBuff(unit, index)
name, duration, expires, source, spell = n, d, e, s, id
guid = source and UnitGUID(source)
end end
if (not name or not guid) and type(auras.GetAuraDataByIndex) == "function" then
if msgobj[1] and msgobj[1] == "Resurrection" and msgobj[2] then local aura = auras.GetAuraDataByIndex(unit, index, "HELPFUL")
msgtype, target = "Ress", msgobj[2] if aura then
end name, duration, expires, spell = aura.name, aura.duration, aura.expirationTime, aura.spellId
guid = aura.sourceGUID or (aura.sourceUnit and UnitGUID(aura.sourceUnit))
if msgobj[1] == "Heal" and msgobj[2] then
msgtype, target, heal, time = "Heal", msgobj[2], msgobj[3], msgobj[4]
end
if msgobj[1] == "GrpHeal" and msgobj[2] then
msgtype, target, heal, time = "Heal", {}, msgobj[2], msgobj[3]
for i=4,8 do
if msgobj[i] then table.insert(target, msgobj[i]) end
end end
end end
elseif select and UnitCastingInfo then if not name then break end
-- latest healcomm local byCaster = guid and hotAmounts[guid]
msgtype = tonumber(string.sub(msg, 1, 3)) local amount = byCaster and byCaster[spell]
if not msgtype then return end if amount and expires and expires > now then
count = count + 1
if msgtype == 0 then local hot = state.entries[count] or {}
msgtype = "Heal" state.entries[count] = hot
heal = tonumber(string.sub(msg, 4, 8)) hot.amount, hot.expires = amount, expires
target = string.sub(msg,9, -1)
local starttime = select(5, UnitCastingInfo(sender))
local endtime = select(6, UnitCastingInfo(sender))
if not starttime or not endtime then return end
time = endtime - starttime
elseif msgtype == 1 then
msgtype = "Stop"
elseif msgtype == 2 then
msgtype = "Heal"
heal = tonumber(string.sub(msg,4, 8))
target = {strsplit(":", string.sub(msg,9, -1))}
local starttime = select(5, UnitCastingInfo(sender))
local endtime = select(6, UnitCastingInfo(sender))
if not starttime or not endtime then return end
time = endtime - starttime
end end
end end
end end
for index = table.getn(state.entries), count+1, -1 do state.entries[index] = nil end
return msgtype, target, heal, time -- Lua 5.0 caches the length used by table.remove; direct slot reuse must
-- update it explicitly, or an expired/refreshed HoT can leave stale entries.
table.setn(state.entries, count)
Notify(state.name)
if count > 0 then elapsed = 0; driver:SetScript("OnUpdate", Expire) end
end end
local function TryShared()
function libpredict:ParseChatMessage(sender, msg, comm) local shared = _G.PTHealPredict
local msgtype, target, heal, time if type(shared) ~= "table" or type(shared.GetIncomingHealing) ~= "function"
or type(shared.HookUpdates) ~= "function" then return false end
if comm == "HealComm" then -- Puppeteer uses SuperWoW GUIDs; never hand it incompatible identifiers.
msgtype, target, heal, time = libpredict:ParseComm(sender, msg) local _, guid = UnitExists("player")
elseif comm == "CTRA" then if not guid or guid ~= UnitGUID("player") then return false end
local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)") P.external, P.source = shared, "Puppeteer"
if cmd == "RES" and ctratarget and ctratarget ~= "" and ctratarget ~= UNKNOWN then learned, activeGUID, sentGUID, sentTarget, sentSpell = nil, nil, nil, nil, nil
msgtype = "Ress" if driver then driver:UnregisterAllEvents(); driver:SetScript("OnUpdate", nil) end
target = ctratarget for name in pairs(P.pending) do P.pending[name] = nil end
elseif cmd == "RESNO" then hotUnits, hotAmounts = {}, {}
msgtype = "RessStop" if not sharedHooks[shared] then
end shared.HookUpdates(function(changedGUID)
end if not P.enabled or P.external ~= shared then return end
for _, unit in ipairs(trackedUnits) do
if msgtype == "Stop" and sender then if UnitGUID(unit) == changedGUID then Notify(UnitName(unit)) end
libpredict:HealStop(sender)
return
elseif msgtype == "RessStop" and sender then
libpredict:RessStop(sender)
return
elseif msgtype == "Delay" and time then
libpredict:HealDelay(sender, time)
elseif msgtype == "Heal" and target and heal and time then
if type(target) == "table" then
for _, name in pairs(target) do
libpredict:Heal(sender, name, heal, time)
end end
else end)
libpredict:Heal(sender, target, heal, time) sharedHooks[shared] = true
end
Notify(UnitName("player")); Notify(UnitName("target"))
return true
end
local function Broadcast(message)
if _G.AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("HealComm-1.0") then return end
local channel = GetNumRaidMembers() > 0 and "RAID" or GetNumPartyMembers() > 0 and "PARTY"
if channel then SendAddonMessage("HealComm", message, channel) end
end
local function StopOwn(guid)
if guid and activeGUID and guid ~= activeGUID then return end
P:HealStop(player)
if activeGUID then Broadcast("Healstop") end
activeGUID = nil
end
local function RefreshOwn()
if not sentSpell or not learned[sentSpell] then return end
local name, _, _, startTime, endTime = API.UnitCastingInfo("player")
if not name or not startTime or not endTime then return end
local remaining = endTime - GetTime() * 1000
if remaining <= 0 then return end
activeGUID = sentGUID
P:Heal(player, sentTarget or player, learned[sentSpell], remaining)
Broadcast("Heal/" .. (sentTarget or player) .. "/" .. learned[sentSpell] .. "/" .. math.floor(remaining) .. "/")
end
function P:ParseComm(sender, message)
if type(sender) ~= "string" or type(message) ~= "string" then return end
if message == "Healstop" or message == "GrpHealstop" or message == "001F" then self:HealStop(sender); return end
local _, _, target, amount, duration = string.find(message, "^Heal/([^/]+)/(%d+)/(%d+)/$")
if target then self:Heal(sender, target, amount, duration); return end
local _, _, delay = string.find(message, "^Healdelay/(%d+)/$")
delay = tonumber(delay)
if delay and delay <= 60000 then
for name, senders in pairs(self.pending) do
if senders[sender] then senders[sender].expires = senders[sender].expires + delay/1000; Notify(name) end
end end
elseif msgtype == "Ress" then
libpredict:Ress(sender, target)
end end
end end
local function Reset()
function libpredict:AddEvent(time, target) for target in pairs(P.pending) do P.pending[target] = nil; Notify(target) end
events[time] = events[time] or {} for unit, state in pairs(hotUnits) do hotUnits[unit] = nil; Notify(state.name) end
table.insert(events[time], target) if driver then driver:SetScript("OnUpdate", nil) end
activeGUID, sentGUID, sentTarget, sentSpell = nil, nil, nil, nil
if not self.timeoutActive then
self.timeoutActive = true
self:SetScript("OnUpdate", UpdateTimeouts)
end
end end
local function OnEvent()
function libpredict:Heal(sender, target, amount, duration) if event == "ADDON_LOADED" then
if not sender or not target or not amount or not duration then TryShared()
return elseif event == "UNIT_AURA" then
end RefreshHots(arg1)
elseif event == "PLAYER_TARGET_CHANGED" then
local timeout = duration/1000 + GetTime() RefreshHots("target")
heals[target] = heals[target] or {} elseif event == "CHAT_MSG_ADDON" then
heals[target][sender] = { amount, timeout } if arg1 == "HealComm" and (arg3 == "PARTY" or arg3 == "RAID" or arg3 == "BATTLEGROUND") then P:ParseComm(arg4, arg2) end
libpredict:AddEvent(timeout, target) elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then
end if arg2 == "SPELL_PERIODIC_HEAL" and arg4 and arg8 and arg12 and tonumber(arg15)
and tonumber(arg15) > 0 and arg18 ~= true and arg18 ~= 1 then
function libpredict:HealStop(sender) -- Learn only ticks landing on one of the two displayed units. Keep
for ttarget, t in pairs(heals) do -- direct and periodic amounts separate (Regrowth uses both).
for tsender in pairs(heals[ttarget]) do if arg8 == UnitGUID("player") or arg8 == UnitGUID("target") then
if sender == tsender then local amounts = hotAmounts[arg4] or {}
heals[ttarget][tsender] = nil hotAmounts[arg4] = amounts
amounts[arg12] = tonumber(arg15)
if arg8 == UnitGUID("player") then RefreshHots("player") end
if arg8 == UnitGUID("target") then RefreshHots("target") end
end end
end
end
end
function libpredict:HealDelay(sender, delay)
local delay = delay/1000
for target, t in pairs(heals) do
for tsender, amount in pairs(heals[target]) do
if sender == tsender then
amount[2] = amount[2] + delay
libpredict:AddEvent(amount[2], target)
end
end
end
end
function libpredict:Ress(sender, target)
ress[target] = ress[target] or {}
ress[target][sender] = true
end
function libpredict:RessStop(sender)
for ttarget, t in pairs(ress) do
for tsender in pairs(ress[ttarget]) do
if sender == tsender then
ress[ttarget][tsender] = nil
end
end
end
end
function libpredict:UnitGetIncomingHeals(unit)
if not unit or not UnitName(unit) then return 0 end
if UnitIsDeadOrGhost(unit) then return 0 end
local name = UnitName(unit)
local sumheal = 0
if not heals[name] then
return sumheal
else
for sender, amount in pairs(heals[name]) do
if amount[2] <= GetTime() then
heals[name][sender] = nil
else
sumheal = sumheal + amount[1]
end
end
end
return sumheal
end
function libpredict:UnitHasIncomingResurrection(unit)
if not unit or not UnitName(unit) then return nil end
local name = UnitName(unit)
if not ress[name] then
return nil
else
for sender, val in pairs(ress[name]) do
if val == true then
return val
end
end
end
return nil
end
local spell_queue = { "DUMMY", "DUMMYRank 9", "TARGET" }
local realm = GetRealmName()
local player = UnitName("player")
local cache, gear_string = {}, ""
local resetcache = CreateFrame("Frame")
resetcache:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
-- load and initialize previous caches of spell amounts
ShaguTweaks_cache = ShaguTweaks_cache or {}
ShaguTweaks_cache["prediction"] = ShaguTweaks_cache["prediction"] or {}
ShaguTweaks_cache["prediction"][realm] = ShaguTweaks_cache["prediction"][realm] or {}
ShaguTweaks_cache["prediction"][realm][player] = ShaguTweaks_cache["prediction"][realm][player] or {}
ShaguTweaks_cache["prediction"][realm][player]["heals"] = ShaguTweaks_cache["prediction"][realm][player]["heals"] or {}
cache = ShaguTweaks_cache["prediction"][realm][player]["heals"]
return
end
if event == "UNIT_INVENTORY_CHANGED" then
-- skip non-player events
if arg1 and arg1 ~= "player" then return end
local gear = ""
for id = 1, 18 do
gear = gear .. (GetInventoryItemLink("player",id) or "")
end
-- abort when inventory didn't change
if gear == gear_string then return end
gear_string = gear
end
-- flag all cached heals for renewal
for k in pairs(cache) do
if type(cache[k]) == "number" or type(cache[k]) == "string" then
-- migrate old data
local oldval = cache[k]
cache[k] = { [1] = oldval }
end
-- flag for reset
cache[k][2] = true
end
end)
local function UpdateCache(spell, heal, crit)
if not spell or not heal then return end
local heal = tonumber(heal)
if not cache[spell] or cache[spell][2] then
-- skills or equipment changed, save whatever is detected
cache[spell] = cache[spell] or {}
cache[spell][1] = crit and heal*2/3 or heal
cache[spell][2] = crit
elseif not crit and cache[spell][1] < heal then
-- safe the best heal we can get
cache[spell][1] = heal
cache[spell][2] = nil
end
end
-- Gather Data by User Actions. Hooks are installed lazily by libpredict:Enable()
-- so disabling healing predictions leaves no spell hook on the hot path.
local scanner
local hooksInstalled
local useClassicSenderEvents
local function TrackCastSpell(id, bookType)
if not libpredict.sender.enabled then return end
local effect, rank = libspell.GetSpellInfo(id, bookType)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
end
local function TrackCastSpellByName(effect, target)
if not libpredict.sender.enabled then return end
local effect, rank = libspell.GetSpellInfo(effect)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
end
local function TrackUseAction(slot, target, selfcast)
if not libpredict.sender.enabled then return end
-- Conditional macros are resolved by the later CastSpellByName hook. For a
-- direct spell action, ClassicAPI gives us the spellID without a tooltip.
if API and API.actioninfo and API.GetActionInfo then
local actionType, actionID = API.GetActionInfo(slot)
if actionType == "macro" or actionType == "item" then return end
if actionType == "spell" and actionID then
if not IsCurrentAction(slot) then return end
local effect, rank = API.GetSpellInfo(actionID)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
return
elseif actionType then
return return
end end
if arg2 == "SPELL_HEAL" and arg4 == UnitGUID("player") and arg12 and tonumber(arg15)
and tonumber(arg15) > 0 and arg18 ~= true and arg18 ~= 1 then learned[arg12] = tonumber(arg15) end
elseif event == "PLAYER_ENTERING_WORLD" then
if TryShared() then return end
player = UnitName("player"); Reset()
if EquipmentChanged() then for spell in pairs(learned) do learned[spell] = nil end end
RefreshHots("player"); RefreshHots("target")
elseif event == "UNIT_INVENTORY_CHANGED" or event == "PLAYER_LEVEL_UP" or event == "CHARACTER_POINTS_CHANGED" then
if event ~= "UNIT_INVENTORY_CHANGED" or (arg1 == "player" and EquipmentChanged()) then
for spell in pairs(learned) do learned[spell] = nil end
local guid = UnitGUID("player")
if guid then hotAmounts[guid] = nil end
StopOwn()
RefreshHots("player"); RefreshHots("target")
end
elseif arg1 == "player" then
if event == "UNIT_SPELLCAST_SENT" then
sentTarget = arg2 and arg2 ~= "" and (UnitExists(arg2) and UnitName(arg2) or arg2) or player
sentGUID, sentSpell = arg3, arg4
elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_DELAYED" then
if arg2 == sentGUID then RefreshOwn() end
elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_FAILED"
or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_SUCCEEDED" then StopOwn(arg2) end
end end
if GetActionText(slot) or not IsCurrentAction(slot) then return end
scanner = scanner or libtipscan:GetScanner("prediction")
scanner:SetAction(slot)
local effect, rank = scanner:Line(1)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
end end
function P:Enable()
local function InstallSenderHooks()
if hooksInstalled then return end
hooksecurefunc("CastSpell", TrackCastSpell)
hooksecurefunc("CastSpellByName", TrackCastSpellByName)
hooksecurefunc("UseAction", TrackUseAction)
hooksInstalled = true
end
libpredict.sender = CreateFrame("Frame", "ShaguTweaksPredictionSender", UIParent)
libpredict.sender.enabled = false
libpredict.sender.SendHealCommMsg = function(self, msg)
SendAddonMessage("HealComm", msg, "RAID")
SendAddonMessage("HealComm", msg, "BATTLEGROUND")
end
libpredict.sender.SendResCommMsg = function(self, msg)
SendAddonMessage("CTRA", msg, "RAID")
SendAddonMessage("CTRA", msg, "BATTLEGROUND")
end
libpredict.sender:SetScript("OnEvent", function()
if event == "CHAT_MSG_SPELL_SELF_BUFF" then -- vanilla
local spell, _, heal = cmatch(arg1, HEALEDSELFOTHER) -- "Your %s heals %s for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end
return
end
local spell, heal = cmatch(arg1, HEALEDSELFSELF) -- "Your %s heals you for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end
return
end
local spell, heal = cmatch(arg1, HEALEDCRITSELFOTHER) -- "Your %s critically heals %s for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
return
end
local spell, _, heal = cmatch(arg1, HEALEDCRITSELFSELF) -- "Your %s critically heals you for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
return
end
elseif event == "COMBAT_LOG_EVENT_UNFILTERED" and arg2 == "SPELL_HEAL" and arg4 == player then -- tbc
local spell, heal, crit = arg10, arg12, arg13
if spell and heal and crit then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
elseif spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end
end
elseif event == "UNIT_SPELLCAST_SENT" then
if arg1 ~= "player" then return end
if GetExpansion() == "vanilla" and useClassicSenderEvents then
-- ClassicAPI already resolved the actual outgoing spell and target:
-- unit, target, castGUID, spellID, spellName, rank. Use it directly so
-- prediction tracking needs no CastSpell/UseAction hooks at all.
local target = arg2 and arg2 ~= "" and arg2 or UnitName("player")
local spell = arg5
local rank = arg6 or ""
senttarget = target
if spell then
spell_queue[1] = spell
spell_queue[2] = spell .. rank
spell_queue[3] = target
end
else
-- Native TBC layout keeps the target in arg4.
local castTarget = arg4
senttarget = castTarget and castTarget ~= "" and castTarget or nil
end
elseif strfind(event, "SPELLCAST_START", 1) then
local spell, time = arg1, arg2
if strfind(event, "UNIT_", 1) then
if arg1 ~= "player" then return end
if GetExpansion() == "vanilla" and API and API.casts then
local spellname, _, _, _, starttime, endtime = API.GetCastInfo("player")
spell = spellname
time = starttime and endtime and endtime - starttime or nil
else
local spellname, _, _, _, starttime, endtime = UnitCastingInfo("player")
spell = spellname
time = starttime and endtime and endtime - starttime or nil
end
end
if not spell or not time then return end
if spell_queue[1] == spell and cache[spell_queue[2]] then
local sender = player
local target = senttarget or spell_queue[3]
local amount = cache[spell_queue[2]][1]
local casttime = time
if spell == PRAYER_OF_HEALING then
target = sender
for i=1,4 do
if CheckInteractDistance("party"..i, 4) then
libpredict:Heal(player, UnitName("party"..i), amount, casttime)
if GetExpansion() == "vanilla" then -- vanilla
libpredict.sender:SendHealCommMsg("Heal/" .. UnitName("party"..i) .. "/" .. amount .. "/" .. casttime .. "/")
else -- tbc
libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), UnitName("party"..i)))
end
libpredict.sender.healing = true
end
end
end
libpredict:Heal(player, target, amount, casttime)
if GetExpansion() == "vanilla" then -- vanilla
libpredict.sender:SendHealCommMsg("Heal/" .. target .. "/" .. amount .. "/" .. casttime .. "/")
else -- tbc
libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), target))
end
libpredict.sender.healing = true
elseif spell_queue[1] == spell and L["resurrections"][spell] then
local target = senttarget or spell_queue[3]
libpredict:Ress(player, target)
libpredict.sender:SendHealCommMsg("Resurrection/" .. target .. "/start/")
libpredict.sender:SendResCommMsg("RES " .. target)
libpredict.sender.resurrecting = true
end
elseif strfind(event, "SPELLCAST_FAILED", 1) or strfind(event, "SPELLCAST_INTERRUPTED", 1) then
if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end
if libpredict.sender.healing then
libpredict:HealStop(player)
if GetExpansion() == "vanilla" then -- vanilla
libpredict.sender:SendHealCommMsg("HealStop")
else -- tbc
libpredict.sender:SendHealCommMsg("001F")
end
libpredict.sender.healing = nil
elseif libpredict.sender.resurrecting then
local target = senttarget or spell_queue[3]
libpredict:RessStop(player)
libpredict.sender:SendHealCommMsg("Resurrection/stop/")
libpredict.sender:SendResCommMsg("RESNO " .. target)
libpredict.sender.resurrecting = nil
end
senttarget = nil
elseif event == "SPELLCAST_DELAYED" then
if libpredict.sender.healing then
libpredict:HealDelay(player, arg1)
libpredict.sender:SendHealCommMsg("Healdelay/" .. arg1 .. "/")
end
elseif strfind(event, "SPELLCAST_STOP", 1) then
if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end
libpredict:HealStop(player)
senttarget = nil
end
end)
function libpredict:Enable()
if self.enabled then return end if self.enabled then return end
self.enabled = true self.enabled = true
self.sender.enabled = true if TryShared() then return end
self.source = "ClassicAPI + HealComm"
-- incoming HealComm / resurrection tracking player = UnitName("player")
self:RegisterEvent("UNIT_HEALTH") learned = {} -- Session-only: actual gear/talent/level changes invalidate estimates.
self:RegisterEvent("CHAT_MSG_ADDON") if EquipmentChanged() then
self:RegisterEvent("PLAYER_TARGET_CHANGED") local guid = UnitGUID("player")
if guid then hotAmounts[guid] = nil end
-- prediction cache lifecycle
resetcache:RegisterEvent("PLAYER_ENTERING_WORLD")
resetcache:RegisterEvent("SKILL_LINES_CHANGED")
resetcache:RegisterEvent("UNIT_INVENTORY_CHANGED")
if GetExpansion() == "vanilla" then
-- ClassicAPI supplies structured player spellcast events on 1.12. Use
-- those instead of registering both event families and processing each cast
-- twice. Keep the native SPELLCAST_DELAYED event because it carries the
-- exact delay delta consumed by HealDelay().
useClassicSenderEvents = API and API.eventutils and _G.C_EventUtils
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_START")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_STOP")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_FAILED")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_FAILED_QUIET")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_INTERRUPTED")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_SENT")
if useClassicSenderEvents then
self.sender:RegisterEvent("UNIT_SPELLCAST_START")
self.sender:RegisterEvent("UNIT_SPELLCAST_STOP")
self.sender:RegisterEvent("UNIT_SPELLCAST_FAILED")
self.sender:RegisterEvent("UNIT_SPELLCAST_FAILED_QUIET")
self.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
self.sender:RegisterEvent("UNIT_SPELLCAST_SENT")
else
self.sender:RegisterEvent("SPELLCAST_START")
self.sender:RegisterEvent("SPELLCAST_STOP")
self.sender:RegisterEvent("SPELLCAST_FAILED")
self.sender:RegisterEvent("SPELLCAST_INTERRUPTED")
end
self.sender:RegisterEvent("SPELLCAST_DELAYED")
self.sender:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF")
else
-- Native TBC event layout and combat log.
self.sender:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
self.sender:RegisterEvent("UNIT_SPELLCAST_START")
self.sender:RegisterEvent("UNIT_SPELLCAST_STOP")
self.sender:RegisterEvent("UNIT_SPELLCAST_FAILED")
self.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
self.sender:RegisterEvent("UNIT_SPELLCAST_SENT")
end
-- force cache updates
self.sender:RegisterEvent("UNIT_INVENTORY_CHANGED")
self.sender:RegisterEvent("SKILL_LINES_CHANGED")
if not useClassicSenderEvents then
InstallSenderHooks()
end end
driver = driver or CreateFrame("Frame")
driver:SetScript("OnEvent", OnEvent)
driver:RegisterEvent("PLAYER_ENTERING_WORLD")
driver:RegisterEvent("ADDON_LOADED")
driver:RegisterEvent("PLAYER_TARGET_CHANGED")
API.RegisterUnitEvent(driver,"UNIT_AURA","player","target")
driver:RegisterEvent("CHAT_MSG_ADDON")
driver:RegisterEvent("UNIT_INVENTORY_CHANGED")
-- Form changes can produce SPELLS_CHANGED without changing any heal amount.
-- New spell ranks have separate spell IDs and learn independently.
driver:RegisterEvent("PLAYER_LEVEL_UP")
driver:RegisterEvent("CHARACTER_POINTS_CHANGED")
local native = {"UNIT_SPELLCAST_SENT", "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_DELAYED",
"UNIT_SPELLCAST_STOP", "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_FAILED", "UNIT_SPELLCAST_INTERRUPTED",
"COMBAT_LOG_EVENT_UNFILTERED"}
for _, name in ipairs(native) do if _G.C_EventUtils.IsEventValid(name) then driver:RegisterEvent(name) end end
elapsed = 0
RefreshHots("player"); RefreshHots("target")
end
function P:Disable()
self.enabled = false
self.external = nil
if driver then driver:UnregisterAllEvents() end
Reset()
end end
ShaguTweaks.libpredict = libpredict
+216 -70
View File
@@ -5,7 +5,15 @@ local API = ShaguTweaks.API
local units = { players = {}, mobs = {} } local units = { players = {}, mobs = {} }
local queue = { } local queue = { }
local PASSIVE_CACHE_TTL = 90 * 24 * 60 * 60
-- Player cache limits. ShaguTweaks_player_cache is parsed at every login and
-- written at every logout, so its size is a direct load-time and memory cost.
local DAY = 24 * 60 * 60
local PASSIVE_CACHE_TTL = 90 * DAY -- players whose class was never learned
local KNOWN_CACHE_TTL = 180 * DAY -- every other player, counted from last sighting
local MAX_CACHED_PLAYERS = 10000 -- oldest sightings go first above this
local CHAT_REFRESH = 60 * 60 -- re-resolve a known chat speaker at most hourly
local GUILD_SCAN_INTERVAL = 60 -- seconds between full guild roster scans
local libunitscan = CreateFrame("Frame", "ShaguTweaksUnitScan", UIParent) local libunitscan = CreateFrame("Frame", "ShaguTweaksUnitScan", UIParent)
@@ -289,21 +297,46 @@ end
ShaguTweaks.GetPlayerCache = EnsurePlayerCache ShaguTweaks.GetPlayerCache = EnsurePlayerCache
local function EnrichPlayerFromClassicAPI(name) -- Same player name, ignoring case. Compare first so the common exact match
if not name or not API or not API.GetCachedPlayerInfoByName then return end -- does not create two lowercase strings.
local function SameName(a, b)
return a == b or string.lower(a) == string.lower(b)
end
local _, class, _, _, _, _, _, guid = API.GetCachedPlayerInfoByName(name) -- New player records carry a timestamp from the start, so the login cleanup
class = class and class ~= "" and class or nil -- can age every entry, not only chat-only ones.
guid = guid and guid ~= "" and guid or nil local function GetOrCreatePlayer(name)
if not class and not guid then return end local data = units.players[name]
if not data then
data = { lastseen_ts = time() }
units.players[name] = data
end
return data
end
-- Keep ShaguTweaks keyed by the exact name it already knows. This avoids -- Store a class. A GUID is kept only while the class is unknown: it is then
-- duplicate SavedVariable entries if another cache normalizes name casing. -- the one way to learn it later through GetPlayerInfoByGUID. Known players
units.players[name] = units.players[name] or {} -- need no GUID in SavedVariables, and nothing else reads it.
units.players[name].class = class or units.players[name].class local function SetPlayerClass(data, class, guid)
units.players[name].guid = guid or units.players[name].guid if class then data.class = class end
if data.class then
data.guid = nil
elseif guid then
data.guid = guid
end
end
return name -- A class-less entry that kept its GUID: ask the engine's name cache.
local function EnrichPlayerFromStoredGUID(name, data)
if not data or data.class or not data.guid then return end
if not API or not API.GetPlayerInfoByGUID then return end
local _, class, _, _, _, apiName = API.GetPlayerInfoByGUID(data.guid)
if not class or class == "" or not apiName or apiName == "" then return end
if not SameName(apiName, name) then return end
SetPlayerClass(data, class)
return true
end end
local function EnrichPlayerFromCurrentChat(name) local function EnrichPlayerFromCurrentChat(name)
@@ -316,14 +349,12 @@ local function EnrichPlayerFromCurrentChat(name)
local guid = API.GetCurrentChatGUID() local guid = API.GetCurrentChatGUID()
if not guid or guid == "" then return end if not guid or guid == "" then return end
local apiName, class = API.GetPlayerInfoByGUID(guid) local _, class, _, _, _, apiName = API.GetPlayerInfoByGUID(guid)
class = class and class ~= "" and class or nil class = class and class ~= "" and class or nil
if not apiName or apiName == "" or not class then return end if not apiName or apiName == "" or not class then return end
if string.lower(apiName) ~= string.lower(name) then return end if not SameName(apiName, name) then return end
units.players[name] = units.players[name] or {} SetPlayerClass(GetOrCreatePlayer(name), class, guid)
units.players[name].class = class
units.players[name].guid = guid
return name return name
end end
@@ -333,21 +364,41 @@ function ShaguTweaks.GetUnitData(name, active)
EnsurePlayerCache() EnsurePlayerCache()
if not units["players"][name] then local data = units.players[name]
EnrichPlayerFromClassicAPI(name)
if not units["players"][name] then if (not data or not data.class) and not EnrichPlayerFromStoredGUID(name, data) then
EnrichPlayerFromCurrentChat(name) -- ClassicAPI's player cache by name. Query it once: the persistent-storage
-- check below and the plain enrichment both use the same result.
local class, guid, resolvedName, _
if API and API.GetCachedPlayerInfoByName then
_, class, _, _, _, resolvedName, _, guid = API.GetCachedPlayerInfoByName(name)
class = class and class ~= "" and class or nil
guid = guid and guid ~= "" and guid or nil
end end
elseif not units["players"][name].class then
EnrichPlayerFromClassicAPI(name) -- Persistent ClassicAPI data can answer class-only queries without making
if not units["players"][name].class then -- another SavedVariables record. Keep our level and social history fields.
if class and resolvedName and SameName(resolvedName, name)
and API.HasPersistentPlayerCache and API.HasPersistentPlayerCache()
and API.RememberPlayerInfo(resolvedName, class, guid) then
return class, data and data.level, data and data.elite, true
end
-- Keep ShaguTweaks keyed by the exact name it already knows. This avoids
-- duplicate SavedVariable entries if another cache normalizes name casing.
if class or guid then
data = GetOrCreatePlayer(name)
SetPlayerClass(data, class, guid)
end
if not data or not data.class then
EnrichPlayerFromCurrentChat(name) EnrichPlayerFromCurrentChat(name)
data = units.players[name]
end end
end end
if units["players"][name] then if data then
local ret = units["players"][name] return data.class, data.level, data.elite, true
return ret.class, ret.level, ret.elite, true
end end
if units["mobs"][name] then if units["mobs"][name] then
@@ -359,13 +410,28 @@ function ShaguTweaks.GetUnitData(name, active)
end end
end end
local function AddData(db, name, class, level, elite, guid) local function AddData(db, name, class, level, elite, guid, now)
if not name then return end if not name then return end
units[db][name] = units[db][name] or {}
units[db][name].class = class or units[db][name].class local data = units[db][name]
units[db][name].level = level or units[db][name].level if not data then
units[db][name].elite = elite or units[db][name].elite data = {}
units[db][name].guid = guid or units[db][name].guid units[db][name] = data
end
if level then data.level = level end
if elite then data.elite = elite end
if db == "players" then
SetPlayerClass(data, class, guid)
-- Every sighting refreshes the cache age. "lastseen" stays the chat and
-- friend-list date that Social Colors shows; this is only for expiry.
data.lastseen_ts = now or time()
else
if class then data.class = class end
if guid then data.guid = guid end
end
queue[name] = nil queue[name] = nil
end end
@@ -374,6 +440,17 @@ local function RememberPlayer(name)
EnsurePlayerCache() EnsurePlayerCache()
local now = time()
local data = units.players[name]
-- Busy channels repeat the same speakers. A speaker whose class is known
-- and who was already seen in chat within the hour needs no GUID lookup
-- and no write.
if data and data.class and type(data.lastseen) == "number"
and now - data.lastseen < CHAT_REFRESH then
return
end
-- ClassicAPI exposes the GUID of the CHAT_MSG_* sender synchronously. -- ClassicAPI exposes the GUID of the CHAT_MSG_* sender synchronously.
-- Resolve class from the engine's existing name cache only: this is a pure -- Resolve class from the engine's existing name cache only: this is a pure
-- cache read and never sends a /who or any other network query. -- cache read and never sends a /who or any other network query.
@@ -386,26 +463,70 @@ local function RememberPlayer(name)
class = apiClass and apiClass ~= "" and apiClass or nil class = apiClass and apiClass ~= "" and apiClass or nil
end end
units.players[name] = units.players[name] or {} -- Skip only NEW chat-only records after native persistent storage succeeds.
units.players[name].class = class or units.players[name].class -- Existing records may contain levels/history and must retain timestamps.
units.players[name].guid = guid or units.players[name].guid if not data and API and API.RememberPlayerInfo
local now = time() and API.RememberPlayerInfo(name, class, guid) then return end
units.players[name].lastseen = now
units.players[name].lastseen_ts = now data = GetOrCreatePlayer(name)
SetPlayerClass(data, class, guid)
data.lastseen = now
data.lastseen_ts = now
end end
local function PrunePassivePlayers() -- One pass per session, at the first PLAYER_ENTERING_WORLD.
local function PrunePlayers(realmdb)
local players = units.players
local now = time() local now = time()
for name, data in pairs(units.players) do -- Entries written before every sighting carried a timestamp have no age.
-- Normalize any empty legacy/API class token back to an unknown value. -- Date them from the cache's grace start, as the old Social Colors cleanup
if data and data.class == "" then data.class = nil end -- did, so they expire on schedule unless the player is seen again.
local graceStart = tonumber(realmdb.players_cleanup)
if not graceStart then
graceStart = now
realmdb.players_cleanup = now
end
-- Keep every player whose class was learned. Only anonymous/passive chat local count = 0
-- entries expire, and only when they carry our numeric last-seen timestamp. for name, data in pairs(players) do
if data and not data.class and data.lastseen_ts and if type(data) ~= "table" then
now - data.lastseen_ts > PASSIVE_CACHE_TTL then players[name] = nil
units.players[name] = nil else
-- Normalize any empty legacy/API class token back to an unknown value.
if data.class == "" then data.class = nil end
if data.class then data.guid = nil end
local seen = GetPlayerTimestamp(data)
if not seen then
seen = graceStart
data.lastseen_ts = graceStart
end
local ttl = data.class and KNOWN_CACHE_TTL or PASSIVE_CACHE_TTL
if now - seen > ttl then
players[name] = nil
else
count = count + 1
end
end
end
-- Hard cap: drop the oldest sightings. Guild members and friends are
-- refreshed by their roster events every session, so they stay.
if count > MAX_CACHED_PLAYERS then
local stamps, n = {}, 0
for _, data in pairs(players) do
n = n + 1
stamps[n] = GetPlayerTimestamp(data) or 0
end
table.sort(stamps)
local cutoff = stamps[n - MAX_CACHED_PLAYERS]
for name, data in pairs(players) do
if (GetPlayerTimestamp(data) or 0) <= cutoff then
players[name] = nil
end
end end
end end
end end
@@ -428,6 +549,15 @@ local passiveChatEvents = {
} }
local useNameplateUnitEvents = API and API.nameplateevents local useNameplateUnitEvents = API and API.nameplateevents
local prunedThisSession = false
local lastGuildScan, lastGuildCount
-- A GUID is only stored for players whose class is unknown, so skip the call
-- when UnitClass already answered.
local function GUIDIfClassless(unit, class)
if class or not API or not API.UnitGUID then return end
return API.UnitGUID(unit)
end
libunitscan:RegisterEvent("PLAYER_ENTERING_WORLD") libunitscan:RegisterEvent("PLAYER_ENTERING_WORLD")
libunitscan:RegisterEvent("PLAYER_LOGOUT") libunitscan:RegisterEvent("PLAYER_LOGOUT")
@@ -446,6 +576,9 @@ for chatEvent in pairs(passiveChatEvents) do
end end
libunitscan:SetScript("OnEvent", function() libunitscan:SetScript("OnEvent", function()
local event = event
local chatEvent, _, chatSender = API.GetCurrentChatEvent()
if chatEvent then event = chatEvent end
if event == "PLAYER_LOGOUT" then if event == "PLAYER_LOGOUT" then
-- SavedVariables are written after this event. Re-run the migration here so -- SavedVariables are written after this event. Re-run the migration here so
-- even a legacy addon path that recreated ShaguTweaks_cache.players during -- even a legacy addon path that recreated ShaguTweaks_cache.players during
@@ -458,18 +591,21 @@ libunitscan:SetScript("OnEvent", function()
-- Ensure the saved database is already bound even if another module queried -- Ensure the saved database is already bound even if another module queried
-- it earlier while restoring UI state. From here onward SavedVariables are -- it earlier while restoring UI state. From here onward SavedVariables are
-- settled, so future cache reads can skip migration checks completely. -- settled, so future cache reads can skip migration checks completely.
EnsurePlayerCache() local _, realmdb = EnsurePlayerCache()
cacheMigrationFinalized = true cacheMigrationFinalized = true
-- One cheap cleanup pass at login. Known classes are kept forever. -- PLAYER_ENTERING_WORLD also fires at every loading screen. Expire and
PrunePassivePlayers() -- cap the cache once per session only.
if not prunedThisSession then
prunedThisSession = true
PrunePlayers(realmdb)
end
-- update own character details -- update own character details
local name = UnitName("player") local name = UnitName("player")
local _, class = UnitClass("player") local _, class = UnitClass("player")
local level = UnitLevel("player") local level = UnitLevel("player")
local guid = API and API.UnitGUID and API.UnitGUID("player") AddData("players", name, class, level, nil, GUIDIfClassless("player", class))
AddData("players", name, class, level, nil, guid)
elseif event == "NAME_PLATE_UNIT_ADDED" then elseif event == "NAME_PLATE_UNIT_ADDED" then
-- ClassicAPI exposes an exact nameplateN unit token here. Cache players -- ClassicAPI exposes an exact nameplateN unit token here. Cache players
@@ -481,59 +617,70 @@ libunitscan:SetScript("OnEvent", function()
local name = UnitName(unit) local name = UnitName(unit)
local _, class = UnitClass(unit) local _, class = UnitClass(unit)
local level = UnitLevel(unit) local level = UnitLevel(unit)
local guid = API and API.UnitGUID and API.UnitGUID(unit) AddData("players", name, class, level, nil, GUIDIfClassless(unit, class))
AddData("players", name, class, level, nil, guid)
end end
elseif passiveChatEvents[event] then elseif passiveChatEvents[event] then
-- Passive cache only: remember speakers already delivered by the client. -- Passive cache only: remember speakers already delivered by the client.
-- No /who, no TargetByName queue, no timer and no additional server query. -- No /who, no TargetByName queue, no timer and no additional server query.
RememberPlayer(arg2) RememberPlayer(chatSender or arg2)
elseif event == "FRIENDLIST_UPDATE" then elseif event == "FRIENDLIST_UPDATE" then
local name, class, level local name, class, level
local now = time()
for i = 1, GetNumFriends() do for i = 1, GetNumFriends() do
name, level, class = GetFriendInfo(i) name, level, class = GetFriendInfo(i)
class = L["class"][class] or nil class = L["class"][class] or nil
-- friendlist updates due to friend going off-line return level 0, let's not overwrite good older values -- friendlist updates due to friend going off-line return level 0, let's not overwrite good older values
level = level > 0 and level or nil level = level > 0 and level or nil
AddData("players", name, class, level) AddData("players", name, class, level, nil, nil, now)
end end
elseif event == "GUILD_ROSTER_UPDATE" then elseif event == "GUILD_ROSTER_UPDATE" then
local name, class, level, _ -- Many addons request the roster every few seconds. A class and level
for i = 1, GetNumGuildMembers() do -- cache gains nothing from rescanning a large guild each time, so scan
name, _, _, level, class = GetGuildRosterInfo(i) -- at most once a minute unless the member count changed.
class = L["class"][class] or nil local count = GetNumGuildMembers()
AddData("players", name, class, level) local now = time()
if count > 0 and not (lastGuildScan and count == lastGuildCount
and now - lastGuildScan < GUILD_SCAN_INTERVAL) then
lastGuildScan, lastGuildCount = now, count
local name, class, level, _
for i = 1, count do
name, _, _, level, class = GetGuildRosterInfo(i)
class = L["class"][class] or nil
AddData("players", name, class, level, nil, nil, now)
end
end end
elseif event == "RAID_ROSTER_UPDATE" then elseif event == "RAID_ROSTER_UPDATE" then
local name, class, SubGroup, level, _ local name, class, SubGroup, level, _
local now = time()
for i = 1, GetNumRaidMembers() do for i = 1, GetNumRaidMembers() do
name, _, SubGroup, level, class = GetRaidRosterInfo(i) name, _, SubGroup, level, class = GetRaidRosterInfo(i)
class = L["class"][class] or nil class = L["class"][class] or nil
local guid = API and API.UnitGUID and API.UnitGUID("raid" .. i) AddData("players", name, class, level, nil, GUIDIfClassless("raid" .. i, class), now)
AddData("players", name, class, level, nil, guid)
end end
elseif event == "PARTY_MEMBERS_CHANGED" then elseif event == "PARTY_MEMBERS_CHANGED" then
local name, class, level, unit, _ local name, class, level, unit, _
local now = time()
for i = 1, GetNumPartyMembers() do for i = 1, GetNumPartyMembers() do
unit = "party" .. i unit = "party" .. i
_, class = UnitClass(unit) _, class = UnitClass(unit)
name = UnitName(unit) name = UnitName(unit)
level = UnitLevel(unit) level = UnitLevel(unit)
local guid = API and API.UnitGUID and API.UnitGUID(unit) AddData("players", name, class, level, nil, GUIDIfClassless(unit, class), now)
AddData("players", name, class, level, nil, guid)
end end
elseif event == "WHO_LIST_UPDATE" then elseif event == "WHO_LIST_UPDATE" then
local name, class, level, _ local name, class, level, _
local now = time()
for i = 1, GetNumWhoResults() do for i = 1, GetNumWhoResults() do
name, _, level, _, class, _ = GetWhoInfo(i) name, _, level, _, class, _ = GetWhoInfo(i)
class = L["class"][class] or nil class = L["class"][class] or nil
AddData("players", name, class, level) AddData("players", name, class, level, nil, nil, now)
end end
elseif event == "UPDATE_MOUSEOVER_UNIT" or event == "PLAYER_TARGET_CHANGED" then elseif event == "UPDATE_MOUSEOVER_UNIT" or event == "PLAYER_TARGET_CHANGED" then
@@ -543,8 +690,7 @@ libunitscan:SetScript("OnEvent", function()
_, class = UnitClass(scan) _, class = UnitClass(scan)
level = UnitLevel(scan) level = UnitLevel(scan)
name = UnitName(scan) name = UnitName(scan)
local guid = API and API.UnitGUID and API.UnitGUID(scan) AddData("players", name, class, level, nil, GUIDIfClassless(scan, class))
AddData("players", name, class, level, nil, guid)
else else
_, class = UnitClass(scan) _, class = UnitClass(scan)
elite = UnitClassification(scan) elite = UnitClassification(scan)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-86
View File
@@ -1,86 +0,0 @@
ShaguTweaks_translation = ShaguTweaks_translation or {}
ShaguTweaks_translation["deDE"] = {
["Adds a castbar to the nameplate based on combat log estimations."] = nil,
["Adds a “Sell Junk” button to every merchant window, that sells all grey items."] = nil,
["Adds a small 24h clock to the mini map."] = nil,
["Adds class colors to the player, target and party unit frames."] = nil,
["Adds compatibility to Turtle WoW's custom changes."] = nil,
["Adds coordinates to the bottom of the World Map."] = nil,
["Advanced Options"] = nil,
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil,
["Auto Dismount"] = nil,
["Automatically dismounts whenever a spell is casted."] = nil,
["Automatically switch to the required warrior or druid stance on spell cast."] = nil,
["Auto Stance"] = nil,
["Blue Shaman Class Colors"] = nil,
["Change health text color based on its value."] = nil,
["Changes the class color code of shamans to blue, as known from TBC+."] = nil,
["Changes the nameplate health bar color to the class color."] = nil,
["Chat Hyperlinks"] = nil,
["Chat Tweaks"] = nil,
["Clock"] = nil,
["Cooldown Numbers"] = nil,
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil,
["Cursor"] = nil,
["Darkened UI"] = nil,
["Debuff Timer"] = nil,
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil,
["Display the remaining duration as text on every cooldown."] = nil,
["Draw the mini map in a squared shape instead of a round one."] = nil,
["Enemy Castbars"] = nil,
["Equip Compare"] = nil,
["Estimates health numbers, and shows numbers on player, pet and target unit frames."] = nil,
["General"] = nil,
["Hide Errors"] = nil,
["Hide Gryphons"] = nil,
["Hides and ignores all Lua errors produced by broken addons."] = nil,
["Hides the gryphons left and right of the action bar."] = nil,
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = nil,
["Increases the healthbar of the player and target unitframe."] = nil,
["Item Rarity Borders"] = nil,
["Localtime"] = nil,
["Makes all nameplates honor the UI-Scale setting."] = nil,
["MiniMap Clock"] = nil,
["MiniMap Square"] = nil,
["MiniMap Tweaks"] = nil,
["Movable Unit Frames"] = nil,
["N/A"] = nil,
["Nameplate Castbar"] = nil,
["Nameplate Class Colors"] = nil,
["Nameplate Scale"] = nil,
["Nameplates"] = nil,
["Player"] = nil,
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil,
["Real Health Numbers"] = nil,
["Reduced Actionbar Size"] = nil,
["Reduces the actionbar size by removing several items such as the bag panel and microbar"] = nil,
["Replace unitframe portraits with class icons."] = nil,
["Sell Grey Items"] = nil,
["Sell Junk"] = nil,
["Servertime"] = nil,
["Show class colored circles on world and battlefield map."] = nil,
["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil,
["Social & Chat"] = nil,
["Social Colors"] = nil,
["Tooltip Details"] = nil,
["Tooltip & Items"] = nil,
["Turns the entire interface into darker colors."] = nil,
["Turns the world map into a movable window. The map can be scaled with <Ctrl> + Mousewheel."] = nil,
["Turtle WoW Compatibility"] = nil,
["Unit Frame Big Health"] = nil,
["Unit Frame Class Colors"] = nil,
["Unit Frame Class Portraits"] = nil,
["Unit Frame Health Colors"] = nil,
["Unit Frames"] = nil,
["Vendor Values"] = nil,
["WorldMap Class Colors"] = nil,
["WorldMap Coordinates"] = nil,
["WorldMap Window"] = nil,
["World & MiniMap"] = nil,
["Your vendor trash has been sold and you earned"] = nil,
}
+104 -8
View File
@@ -8,8 +8,6 @@ ShaguTweaks_translation["enUS"] = {
["Adds coordinates to the bottom of the World Map."] = nil, ["Adds coordinates to the bottom of the World Map."] = nil,
["Advanced Options"] = nil, ["Advanced Options"] = nil,
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil, ["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil,
["Auto Dismount"] = nil,
["Automatically dismounts whenever a spell is casted."] = nil,
["Automatically switch to the required warrior or druid stance on spell cast."] = nil, ["Automatically switch to the required warrior or druid stance on spell cast."] = nil,
["Auto Stance"] = nil, ["Auto Stance"] = nil,
["Blue Shaman Class Colors"] = nil, ["Blue Shaman Class Colors"] = nil,
@@ -23,7 +21,8 @@ ShaguTweaks_translation["enUS"] = {
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil, ["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil,
["Cursor"] = nil, ["Cursor"] = nil,
["Darkened UI"] = nil, ["Darkened UI"] = nil,
["Debuff Timer"] = nil, ["Golden UI"] = nil,
["Turns the borders of the interface, tooltips and dialogs gold, like the WoW Forever client. Darkened UI takes priority when both are enabled."] = nil,
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil, ["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil,
["Display the remaining duration as text on every cooldown."] = nil, ["Display the remaining duration as text on every cooldown."] = nil,
["Draw the mini map in a squared shape instead of a round one."] = nil, ["Draw the mini map in a squared shape instead of a round one."] = nil,
@@ -33,8 +32,10 @@ ShaguTweaks_translation["enUS"] = {
["General"] = nil, ["General"] = nil,
["Hide Errors"] = nil, ["Hide Errors"] = nil,
["Hide Gryphons"] = nil, ["Hide Gryphons"] = nil,
["Hide Pet Unit Frame"] = nil,
["Hides and ignores all Lua errors produced by broken addons."] = nil, ["Hides and ignores all Lua errors produced by broken addons."] = nil,
["Hides the gryphons left and right of the action bar."] = nil, ["Hides the gryphons left and right of the action bar."] = nil,
["Hides the Pet unitframe below the player in pet classes."] = nil,
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = nil, ["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = nil,
["Increases the healthbar of the player and target unitframe."] = nil, ["Increases the healthbar of the player and target unitframe."] = nil,
["Item Rarity Borders"] = nil, ["Item Rarity Borders"] = nil,
@@ -45,9 +46,6 @@ ShaguTweaks_translation["enUS"] = {
["MiniMap Tweaks"] = nil, ["MiniMap Tweaks"] = nil,
["Movable Unit Frames"] = nil, ["Movable Unit Frames"] = nil,
["N/A"] = nil, ["N/A"] = nil,
["Nameplate Castbar"] = nil,
["Nameplate Class Colors"] = nil,
["Nameplate Scale"] = nil,
["Nameplates"] = nil, ["Nameplates"] = nil,
["Player"] = nil, ["Player"] = nil,
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil, ["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil,
@@ -62,7 +60,8 @@ ShaguTweaks_translation["enUS"] = {
["Show class colors in Who, Guild, Friends and Chat."] = nil, ["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil, ["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil, ["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil, ["Shows an enemy castbar on target unit frame. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."] = nil,
["Shows current and needed XP, percentage and rested bonus while hovering over the experience bar."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil, ["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil, ["Shows the vendor sell values on all item tooltips."] = nil,
["Social & Chat"] = nil, ["Social & Chat"] = nil,
@@ -75,7 +74,6 @@ ShaguTweaks_translation["enUS"] = {
["Unit Frame Big Health"] = nil, ["Unit Frame Big Health"] = nil,
["Unit Frame Class Colors"] = nil, ["Unit Frame Class Colors"] = nil,
["Unit Frame Class Portraits"] = nil, ["Unit Frame Class Portraits"] = nil,
["Unit Frame Health Colors"] = nil,
["Unit Frames"] = nil, ["Unit Frames"] = nil,
["Vendor Values"] = nil, ["Vendor Values"] = nil,
["WorldMap Class Colors"] = nil, ["WorldMap Class Colors"] = nil,
@@ -83,4 +81,102 @@ ShaguTweaks_translation["enUS"] = {
["WorldMap Window"] = nil, ["WorldMap Window"] = nil,
["World & MiniMap"] = nil, ["World & MiniMap"] = nil,
["Your vendor trash has been sold and you earned"] = nil, ["Your vendor trash has been sold and you earned"] = nil,
["Swing Timer"] = nil,
["Wand Timer"] = nil,
["Main Hand"] = nil,
["Off Hand"] = nil,
["Ranged"] = nil,
["Shows main-hand and off-hand swing timers above the cast bar, with the weapon's icon. Hold <Ctrl>, <Shift> and <Alt> to move each bar on its own; right-click a bar to reset it."] = nil,
["Shows the time until your next wand shot (or Auto Shot) above the cast bar, with the weapon's icon."] = nil,
["Transports"] = nil,
["Shows or hides the OctoBus transport timers."] = nil,
["Loot History"] = nil,
["Adds /loothistory: a window with the need, greed and pass rolls of recent group loot and who won each item. Requires ClassicAPI."] = nil,
["Retrieving item information..."] = nil,
["All players passed"] = nil,
["Clear"] = nil,
["No loot rolls yet."] = nil,
["Loot History requires ClassicAPI."] = nil,
["Enemy Castbar"] = nil,
["Movable Cast Bar"] = nil,
["Hold <Ctrl>, <Shift> and <Alt> to move the cast bar; right-click to reset."] = nil,
["Cast Bar"] = nil,
["Loss of Control Alert"] = nil,
["Shows a large icon and countdown when you are stunned, feared, silenced, rooted, disarmed or locked out of a spell school. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."] = nil,
["Stunned"] = nil,
["Feared"] = nil,
["Confused"] = nil,
["Charmed"] = nil,
["Possessed"] = nil,
["Silenced"] = nil,
["Pacified"] = nil,
["Pacified and Silenced"] = nil,
["Disarmed"] = nil,
["Rooted"] = nil,
["Interrupted"] = nil,
["Drag to move, right-click to reset"] = nil,
["Totem Timers"] = nil,
["Shamans: shows the totems you have out as icons with the time left. Left-click targets a totem. Hold <Ctrl>, <Shift> and <Alt> to move them; right-click to reset."] = nil,
["Dragonflight Gryphons"] = nil,
["Replaces actionbar gryphons with the dragonflight version."] = nil,
["Action Bar"] = nil,
["Floating Actionbar"] = nil,
["Removes all background textures and lets the actionbar float."] = nil,
["Hide Macro Text"] = nil,
["Hides macro names on action buttons."] = nil,
["Mouseover Right"] = nil,
["Hide the right actionbar and show it on mouseover."] = nil,
["Mouseover Right 2"] = nil,
["Hide the second right actionbar and show it on mouseover."] = nil,
["Reagent Counter"] = nil,
["Shows a reagent counter on action buttons."] = nil,
["Center Vertical Actionbar"] = nil,
["Center the vertical actionbar on the right side."] = nil,
["Buy Em All"] = nil,
["Shift-click vendor items to choose stack, maximum, or a custom purchase amount."] = nil,
["Merchant"] = nil,
["Center Text Input Box"] = nil,
["Move the chat input box to the center of the screen."] = nil,
["Chat & Social"] = nil,
["Block NPC Spam"] = nil,
["Blocks known repetitive NPC say/yell spam messages."] = nil,
["Enable Text Shadow"] = nil,
["Enable text shadow in all chat frames."] = nil,
["Chat Timestamps"] = nil,
["Add timestamps to chat messages."] = nil,
["Cursor Tooltip"] = nil,
["Attaches default tooltips to the cursor."] = nil,
["Hide Combat Tooltip"] = nil,
["Hides the tooltip in combat. Hold Shift to show it temporarily."] = nil,
["Loot Quality"] = nil,
["Colors the loot frame using the highest item quality in the current loot window."] = nil,
["Loot"] = nil,
["Macro Icons"] = nil,
["Detect showtooltip and spells in macros to use them on action buttons."] = nil,
["Macro"] = nil,
["Macro Tweaks"] = nil,
["Add /equip, /use and modern ClassicAPI macro commands, remove #showtooltip from chat and hide macro commands from history."] = nil,
["Metric Range"] = nil,
["Displays tooltip range units in metres instead of yards."] = nil,
["MiniMap Framerate & Latency"] = nil,
["Adds a small framerate & latency display to the mini map."] = nil,
["Minimap & World Map"] = nil,
["Framerate"] = nil,
["High"] = nil,
["fps"] = nil,
["Low"] = nil,
["Latency"] = nil,
["ms"] = nil,
["Show Bags"] = nil,
["Shows bag and keyring buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the bag bar."] = nil,
["Show Micro Menu"] = nil,
["Shows micro menu buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the micro menu."] = nil,
["Unit Frame Abbreviated Names"] = nil,
["Abbreviates long target and target-of-target names."] = nil,
["World Chat Hider"] = nil,
["Hides World channel messages while inside an instance without leaving the channel."] = nil,
["Reveal World Map"] = nil,
["Reveals unexplored world map areas and shows exploration hints."] = nil,
["Reveal Unexplored"] = nil,
["Exploration Point"] = nil,
} }
-86
View File
@@ -1,86 +0,0 @@
ShaguTweaks_translation = ShaguTweaks_translation or {}
ShaguTweaks_translation["esES"] = {
["Adds a castbar to the nameplate based on combat log estimations."] = nil,
["Adds a “Sell Junk” button to every merchant window, that sells all grey items."] = nil,
["Adds a small 24h clock to the mini map."] = nil,
["Adds class colors to the player, target and party unit frames."] = nil,
["Adds compatibility to Turtle WoW's custom changes."] = nil,
["Adds coordinates to the bottom of the World Map."] = nil,
["Advanced Options"] = nil,
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil,
["Auto Dismount"] = nil,
["Automatically dismounts whenever a spell is casted."] = nil,
["Automatically switch to the required warrior or druid stance on spell cast."] = nil,
["Auto Stance"] = nil,
["Blue Shaman Class Colors"] = nil,
["Change health text color based on its value."] = nil,
["Changes the class color code of shamans to blue, as known from TBC+."] = nil,
["Changes the nameplate health bar color to the class color."] = nil,
["Chat Hyperlinks"] = nil,
["Chat Tweaks"] = nil,
["Clock"] = nil,
["Cooldown Numbers"] = nil,
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil,
["Cursor"] = nil,
["Darkened UI"] = nil,
["Debuff Timer"] = nil,
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil,
["Display the remaining duration as text on every cooldown."] = nil,
["Draw the mini map in a squared shape instead of a round one."] = nil,
["Enemy Castbars"] = nil,
["Equip Compare"] = nil,
["Estimates health numbers, and shows numbers on player, pet and target unit frames."] = nil,
["General"] = nil,
["Hide Errors"] = nil,
["Hide Gryphons"] = nil,
["Hides and ignores all Lua errors produced by broken addons."] = nil,
["Hides the gryphons left and right of the action bar."] = nil,
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = nil,
["Increases the healthbar of the player and target unitframe."] = nil,
["Item Rarity Borders"] = nil,
["Localtime"] = nil,
["Makes all nameplates honor the UI-Scale setting."] = nil,
["MiniMap Clock"] = nil,
["MiniMap Square"] = nil,
["MiniMap Tweaks"] = nil,
["Movable Unit Frames"] = nil,
["N/A"] = nil,
["Nameplate Castbar"] = nil,
["Nameplate Class Colors"] = nil,
["Nameplate Scale"] = nil,
["Nameplates"] = nil,
["Player"] = nil,
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil,
["Real Health Numbers"] = nil,
["Reduced Actionbar Size"] = nil,
["Reduces the actionbar size by removing several items such as the bag panel and microbar"] = nil,
["Replace unitframe portraits with class icons."] = nil,
["Sell Grey Items"] = nil,
["Sell Junk"] = nil,
["Servertime"] = nil,
["Show class colored circles on world and battlefield map."] = nil,
["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil,
["Social & Chat"] = nil,
["Social Colors"] = nil,
["Tooltip Details"] = nil,
["Tooltip & Items"] = nil,
["Turns the entire interface into darker colors."] = nil,
["Turns the world map into a movable window. The map can be scaled with <Ctrl> + Mousewheel."] = nil,
["Turtle WoW Compatibility"] = nil,
["Unit Frame Big Health"] = nil,
["Unit Frame Class Colors"] = nil,
["Unit Frame Class Portraits"] = nil,
["Unit Frame Health Colors"] = nil,
["Unit Frames"] = nil,
["Vendor Values"] = nil,
["WorldMap Class Colors"] = nil,
["WorldMap Coordinates"] = nil,
["WorldMap Window"] = nil,
["World & MiniMap"] = nil,
["Your vendor trash has been sold and you earned"] = nil,
}
-86
View File
@@ -1,86 +0,0 @@
ShaguTweaks_translation = ShaguTweaks_translation or {}
ShaguTweaks_translation["frFR"] = {
["Adds a castbar to the nameplate based on combat log estimations."] = nil,
["Adds a “Sell Junk” button to every merchant window, that sells all grey items."] = nil,
["Adds a small 24h clock to the mini map."] = nil,
["Adds class colors to the player, target and party unit frames."] = nil,
["Adds compatibility to Turtle WoW's custom changes."] = nil,
["Adds coordinates to the bottom of the World Map."] = nil,
["Advanced Options"] = nil,
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil,
["Auto Dismount"] = nil,
["Automatically dismounts whenever a spell is casted."] = nil,
["Automatically switch to the required warrior or druid stance on spell cast."] = nil,
["Auto Stance"] = nil,
["Blue Shaman Class Colors"] = nil,
["Change health text color based on its value."] = nil,
["Changes the class color code of shamans to blue, as known from TBC+."] = nil,
["Changes the nameplate health bar color to the class color."] = nil,
["Chat Hyperlinks"] = nil,
["Chat Tweaks"] = nil,
["Clock"] = nil,
["Cooldown Numbers"] = nil,
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil,
["Cursor"] = nil,
["Darkened UI"] = nil,
["Debuff Timer"] = nil,
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil,
["Display the remaining duration as text on every cooldown."] = nil,
["Draw the mini map in a squared shape instead of a round one."] = nil,
["Enemy Castbars"] = nil,
["Equip Compare"] = nil,
["Estimates health numbers, and shows numbers on player, pet and target unit frames."] = nil,
["General"] = nil,
["Hide Errors"] = nil,
["Hide Gryphons"] = nil,
["Hides and ignores all Lua errors produced by broken addons."] = nil,
["Hides the gryphons left and right of the action bar."] = nil,
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = nil,
["Increases the healthbar of the player and target unitframe."] = nil,
["Item Rarity Borders"] = nil,
["Localtime"] = nil,
["Makes all nameplates honor the UI-Scale setting."] = nil,
["MiniMap Clock"] = nil,
["MiniMap Square"] = nil,
["MiniMap Tweaks"] = nil,
["Movable Unit Frames"] = nil,
["N/A"] = nil,
["Nameplate Castbar"] = nil,
["Nameplate Class Colors"] = nil,
["Nameplate Scale"] = nil,
["Nameplates"] = nil,
["Player"] = nil,
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil,
["Real Health Numbers"] = nil,
["Reduced Actionbar Size"] = nil,
["Reduces the actionbar size by removing several items such as the bag panel and microbar"] = nil,
["Replace unitframe portraits with class icons."] = nil,
["Sell Grey Items"] = nil,
["Sell Junk"] = nil,
["Servertime"] = nil,
["Show class colored circles on world and battlefield map."] = nil,
["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil,
["Social & Chat"] = nil,
["Social Colors"] = nil,
["Tooltip Details"] = nil,
["Tooltip & Items"] = nil,
["Turns the entire interface into darker colors."] = nil,
["Turns the world map into a movable window. The map can be scaled with <Ctrl> + Mousewheel."] = nil,
["Turtle WoW Compatibility"] = nil,
["Unit Frame Big Health"] = nil,
["Unit Frame Class Colors"] = nil,
["Unit Frame Class Portraits"] = nil,
["Unit Frame Health Colors"] = nil,
["Unit Frames"] = nil,
["Vendor Values"] = nil,
["WorldMap Class Colors"] = nil,
["WorldMap Coordinates"] = nil,
["WorldMap Window"] = nil,
["World & MiniMap"] = nil,
["Your vendor trash has been sold and you earned"] = nil,
}
-86
View File
@@ -1,86 +0,0 @@
ShaguTweaks_translation = ShaguTweaks_translation or {}
ShaguTweaks_translation["koKR"] = {
["Adds a castbar to the nameplate based on combat log estimations."] = nil,
["Adds a “Sell Junk” button to every merchant window, that sells all grey items."] = nil,
["Adds a small 24h clock to the mini map."] = nil,
["Adds class colors to the player, target and party unit frames."] = nil,
["Adds compatibility to Turtle WoW's custom changes."] = nil,
["Adds coordinates to the bottom of the World Map."] = nil,
["Advanced Options"] = nil,
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil,
["Auto Dismount"] = nil,
["Automatically dismounts whenever a spell is casted."] = nil,
["Automatically switch to the required warrior or druid stance on spell cast."] = nil,
["Auto Stance"] = nil,
["Blue Shaman Class Colors"] = nil,
["Change health text color based on its value."] = nil,
["Changes the class color code of shamans to blue, as known from TBC+."] = nil,
["Changes the nameplate health bar color to the class color."] = nil,
["Chat Hyperlinks"] = nil,
["Chat Tweaks"] = nil,
["Clock"] = nil,
["Cooldown Numbers"] = nil,
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil,
["Cursor"] = nil,
["Darkened UI"] = nil,
["Debuff Timer"] = nil,
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil,
["Display the remaining duration as text on every cooldown."] = nil,
["Draw the mini map in a squared shape instead of a round one."] = nil,
["Enemy Castbars"] = nil,
["Equip Compare"] = nil,
["Estimates health numbers, and shows numbers on player, pet and target unit frames."] = nil,
["General"] = nil,
["Hide Errors"] = nil,
["Hide Gryphons"] = nil,
["Hides and ignores all Lua errors produced by broken addons."] = nil,
["Hides the gryphons left and right of the action bar."] = nil,
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = nil,
["Increases the healthbar of the player and target unitframe."] = nil,
["Item Rarity Borders"] = nil,
["Localtime"] = nil,
["Makes all nameplates honor the UI-Scale setting."] = nil,
["MiniMap Clock"] = nil,
["MiniMap Square"] = nil,
["MiniMap Tweaks"] = nil,
["Movable Unit Frames"] = nil,
["N/A"] = nil,
["Nameplate Castbar"] = nil,
["Nameplate Class Colors"] = nil,
["Nameplate Scale"] = nil,
["Nameplates"] = nil,
["Player"] = nil,
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil,
["Real Health Numbers"] = nil,
["Reduced Actionbar Size"] = nil,
["Reduces the actionbar size by removing several items such as the bag panel and microbar"] = nil,
["Replace unitframe portraits with class icons."] = nil,
["Sell Grey Items"] = nil,
["Sell Junk"] = nil,
["Servertime"] = nil,
["Show class colored circles on world and battlefield map."] = nil,
["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil,
["Social & Chat"] = nil,
["Social Colors"] = nil,
["Tooltip Details"] = nil,
["Tooltip & Items"] = nil,
["Turns the entire interface into darker colors."] = nil,
["Turns the world map into a movable window. The map can be scaled with <Ctrl> + Mousewheel."] = nil,
["Turtle WoW Compatibility"] = nil,
["Unit Frame Big Health"] = nil,
["Unit Frame Class Colors"] = nil,
["Unit Frame Class Portraits"] = nil,
["Unit Frame Health Colors"] = nil,
["Unit Frames"] = nil,
["Vendor Values"] = nil,
["WorldMap Class Colors"] = nil,
["WorldMap Coordinates"] = nil,
["WorldMap Window"] = nil,
["World & MiniMap"] = nil,
["Your vendor trash has been sold and you earned"] = nil,
}
-86
View File
@@ -1,86 +0,0 @@
ShaguTweaks_translation = ShaguTweaks_translation or {}
ShaguTweaks_translation["ruRU"] = {
["Adds a castbar to the nameplate based on combat log estimations."] = "Добавляет панель применения к индикатору здоровья на основе оценок журнала боя.",
["Adds a “Sell Junk” button to every merchant window, that sells all grey items."] = "Добавляет кнопку «Продать серые предметы» в каждое окно торговца, в котором продаются все серые предметы.",
["Adds a small 24h clock to the mini map."] = "Добавляет небольшие 24-часовые часы на мини-карту.",
["Adds class colors to the player, target and party unit frames."] = "Добавить цвета классов к окнам игрока, цели и отряда группы.",
["Adds compatibility to Turtle WoW's custom changes."] = "Добавляет совместимость с пользовательскими изменениями Turtle WoW.",
["Adds coordinates to the bottom of the World Map."] = "Добавляет координаты внизу карты мира.",
["Advanced Options"] = "Расширенные настройки",
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = "Позволяет прокручивать колесо мыши, включает закрепленные каналы чата и повторяет сообщение при нажатии стрелки вверх.",
["Auto Dismount"] = "Авто-спешивание",
["Automatically dismounts whenever a spell is casted."] = "Автоматическое спешивание при каждом произнесении заклинания.",
["Automatically switch to the required warrior or druid stance on spell cast."] = "Автоматически переключаться на нужную стойку воина или друида при произнесении заклинания.",
["Auto Stance"] = "Авто-стойка",
["Blue Shaman Class Colors"] = "Синий класс шамана",
["Change health text color based on its value."] = "Изменить цвет текста о состоянии здоровья в зависимости от его значения.",
["Changes the class color code of shamans to blue, as known from TBC+."] = "Изменяет цветовой код класса шаманов на синий, как известно из TBC+.",
["Changes the nameplate health bar color to the class color."] = "Изменяет цвет индикатора здоровья на цвет класса.",
["Chat Hyperlinks"] = "Гиперссылки чата",
["Chat Tweaks"] = "Улучшения чата",
["Clock"] = "Часы",
["Cooldown Numbers"] = "Продолжительность перезарядки",
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = "Копирование URL-адреса веб-сайтов из чата, преобразование CLINKов в реальные предметы и обрабатывание ссылок на квесты и игроков.",
["Cursor"] = "Курсор",
["Darkened UI"] = "Затемненный интерфейс",
["Debuff Timer"] = "Таймер дебаффов",
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = "Отображение здоровья, цвета класса, названия гильдии, ранга гильдии и текущей цели во всплывающих подсказках юнитов.",
["Display the remaining duration as text on every cooldown."] = "Отображать оставшуюся продолжительность в виде текста при перезарядке.",
["Draw the mini map in a squared shape instead of a round one."] = "Мини-карта квадратной формы вместо круглой.",
["Enemy Castbars"] = "Панель применения противника",
["Equip Compare"] = "Сравнение экипировки",
["Estimates health numbers, and shows numbers on player, pet and target unit frames."] = "Оценивает количество здоровья и показывает цифры на окнах игрока, питомца и цели.",
["General"] = "Общие",
["Hide Errors"] = "Скрыть ошибки",
["Hide Gryphons"] = "Спрятать грифонов",
["Hides and ignores all Lua errors produced by broken addons."] = "Скрывает и игнорирует все ошибки Lua, вызванные неработающими аддонами.",
["Hides the gryphons left and right of the action bar."] = "Скрывает грифонов слева и справа от панели действий.",
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = "Скрывает ненужные кнопки мини-карты и позволяет масштабировать ее с помощью колеса мыши.",
["Increases the healthbar of the player and target unitframe."] = "Увеличивает полосу здоровья игрока и цели.",
["Item Rarity Borders"] = "Границы редкости предметов",
["Localtime"] = "Местное время",
["Makes all nameplates honor the UI-Scale setting."] = "Все индикаторы здоровья будут соответствовать настройке UI-Scale.",
["MiniMap Clock"] = "Часы на мини-карте",
["MiniMap Square"] = "Квадратная мини-карта",
["MiniMap Tweaks"] = "Улучшения мини-карты",
["Movable Unit Frames"] = "Передвижные окна",
["N/A"] = "Н/Д",
["Nameplate Castbar"] = "Панель применения индикатора здоровья",
["Nameplate Class Colors"] = "Классы индикатора здоровья",
["Nameplate Scale"] = "Масштаб индикаторов здоровья",
["Nameplates"] = nil,
["Player"] = "Игрок",
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = "Окна игрока и цели можно перемещать, удерживая клавиши <Shift> и <Ctrl> вместе.",
["Real Health Numbers"] = "Реальные цифры здоровья",
["Reduced Actionbar Size"] = "Уменьшить панель действий",
["Reduces the actionbar size by removing several items such as the bag panel and microbar"] = "Уменьшает размер панели действий за счет удаления нескольких элементов, таких как панель сумки и микропанель.",
["Replace unitframe portraits with class icons."] = "Заменить портреты окон юнитов значками классов.",
["Sell Grey Items"] = "Продать серые предметы",
["Sell Junk"] = "Продать серые предметы",
["Servertime"] = "Серверное время",
["Show class colored circles on world and battlefield map."] = "Показать цветные круги классов на карте мира и поле боя.",
["Show class colors in Who, Guild, Friends and Chat."] = "Показать цвета классов в разделах «Кто», «Гильдия», «Друзья» и «Чат».",
["Show debuff durations on the target unit frame."] = "Показать продолжительность дебаффов в окне цели.",
["Show item rarity as the border color on bags, bank, character and inspect frames."] = "Показать редкость предмета в качестве цвета рамки на сумках, банке, окне персонажа и осмотра.",
["Shows an enemy castbar on target unit frame."] = "Показать панель применения противника в окне юнита цели.",
["Shows currently equipped items on tooltips while the shift key is pressed."] = "Показывает экипированные в данный момент предметы во всплывающих подсказках, пока нажата клавиша Shift.",
["Shows the vendor sell values on all item tooltips."] = "Показывает стоимость продажи продавцу во всплывающих подсказках ко всем предметам.",
["Social & Chat"] = "Общение и чат",
["Social Colors"] = "Социальные цвета",
["Tooltip Details"] = "Подробные подсказки",
["Tooltip & Items"] = "Подсказки и предметы",
["Turns the entire interface into darker colors."] = "Меняет весь интерфейс в более темные цвета.",
["Turns the world map into a movable window. The map can be scaled with <Ctrl> + Mousewheel."] = "Превращает карту мира в подвижное окно. Карту можно масштабировать с помощью <Ctrl> + Колесо мыши.",
["Turtle WoW Compatibility"] = "Совместимость с Turtle WoW",
["Unit Frame Big Health"] = "Большое здоровье",
["Unit Frame Class Colors"] = "Цветные классы",
["Unit Frame Class Portraits"] = "Классовые портреты",
["Unit Frame Health Colors"] = "Цветное здоровье",
["Unit Frames"] = "Окна юнитов",
["Vendor Values"] = "Стоимость продажи",
["WorldMap Class Colors"] = "Классы на карте мира",
["WorldMap Coordinates"] = "Координаты на карте мира",
["WorldMap Window"] = "Окно карты мира",
["World & MiniMap"] = "Карта мира и мини-карта",
["Your vendor trash has been sold and you earned"] = "Ваш хлам продан, и вы заработали",
}
-86
View File
@@ -1,86 +0,0 @@
ShaguTweaks_translation = ShaguTweaks_translation or {}
ShaguTweaks_translation["zhCN"] = {
["Adds a castbar to the nameplate based on combat log estimations."] = "添加基于战斗日志的施法条。",
["Adds a “Sell Junk” button to every merchant window, that sells all grey items."] = "商人窗口添加:“出售垃圾”按钮,将灰色物品出售给商人。",
["Adds a small 24h clock to the mini map."] = "添加一个24小时的时钟到小地图。",
["Adds class colors to the player, target and party unit frames."] = "添加玩家、目标和队伍单位的职业颜色。",
["Adds compatibility to Turtle WoW's custom changes."] = "添加乌龟服的兼容修改。",
["Adds coordinates to the bottom of the World Map."] = "添加坐标到世界地图的底部。",
["Advanced Options"] = "高级选项",
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = "允许使用鼠标滚轮,启用粘性频道,向上箭头重复消息。",
["Auto Dismount"] = "自动下坐骑",
["Automatically dismounts whenever a spell is casted."] = "当施放任何法术时自动取消坐骑。",
["Automatically switch to the required warrior or druid stance on spell cast."] = "自动切换技能所需姿态(战士/德鲁伊)。",
["Auto Stance"] = "自动切换姿态",
["Blue Shaman Class Colors"] = "蓝色萨满职业颜色",
["Change health text color based on its value."] = "根据值改变血量文本颜色。",
["Changes the class color code of shamans to blue, as known from TBC+."] = "修改了萨满职业的颜色为蓝色,与TBC+相同。",
["Changes the nameplate health bar color to the class color."] = "修改姓名板的血量条颜色为职业颜色。",
["Chat Hyperlinks"] = "聊天超链接",
["Chat Tweaks"] = "聊天增强",
["Clock"] = "时钟",
["Cooldown Numbers"] = "冷却倒计时",
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = "复制聊天中的网址,将CLINK转换为实际的物品、任务和玩家链接。",
["Cursor"] = "鼠标指针",
["Darkened UI"] = "黑暗风格UI",
["Debuff Timer"] = "Debuff计时",
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = "提示框显示当前目标血量、职业颜色、公会名称、公会等级、荣誉排名。",
["Display the remaining duration as text on every cooldown."] = "显示剩余时间的文本,如:10s,1m,1h,1d。",
["Draw the mini map in a squared shape instead of a round one."] = "小地图以方形显示,而不是圆形。",
["Enemy Castbars"] = "敌对目标施法条",
["Equip Compare"] = "装备比较",
["Estimates health numbers, and shows numbers on player, pet and target unit frames."] = "显示玩家、宠物和目标单位的血量。",
["General"] = "常规",
["Hide Errors"] = "隐藏LUA错误",
["Hide Gryphons"] = "隐藏动作条狮鹫",
["Hides and ignores all Lua errors produced by broken addons."] = "隐藏并忽略所有插件脚本产生的LUA错误。",
["Hides the gryphons left and right of the action bar."] = "隐藏动作条左右两边狮鹫。",
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = "隐藏小地图缩放按钮,改用使用鼠标滚轮进行缩放。",
["Increases the healthbar of the player and target unitframe."] = "增加玩家和目标单位的血量条的宽度。",
["Item Rarity Borders"] = "物品品质边框着色",
["Localtime"] = "本地时间",
["Makes all nameplates honor the UI-Scale setting."] = "名字板跟随UI缩放设置。",
["MiniMap Clock"] = "小地图时钟",
["MiniMap Square"] = "方形小地图",
["MiniMap Tweaks"] = "小地图增强",
["Movable Unit Frames"] = "头像框架可移动",
["N/A"] = "N/A",
["Nameplate Castbar"] = "姓名板施法条",
["Nameplate Class Colors"] = "姓名板职业着色",
["Nameplate Scale"] = "姓名板缩放",
["Nameplates"] = nil,
["Player"] = "玩家",
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = "当<Shift>和<Ctrl>键同时按下时,玩家与目标单位的头像可移动。",
["Real Health Numbers"] = "显示预估生命值",
["Reduced Actionbar Size"] = "减少动作条大小",
["Reduces the actionbar size by removing several items such as the bag panel and microbar"] = "减少底部动作条大小,移除动作条上的按钮。",
["Replace unitframe portraits with class icons."] = "单位头像替换为职业图标。",
["Sell Grey Items"] = "售卖灰色物品",
["Sell Junk"] = "售卖垃圾物品",
["Servertime"] = "服务器时间",
["Show class colored circles on world and battlefield map."] = "在世界地图和战场地图上显示团队成员职业颜色的圆圈。",
["Show class colors in Who, Guild, Friends and Chat."] = "显示职业颜色在Who、公会、好友和聊天等列表中。",
["Show debuff durations on the target unit frame."] = "目标单位显示debuff的持续时间。",
["Show item rarity as the border color on bags, bank, character and inspect frames."] = "在背包、银行、角色、查看窗口中显示物品品质边框颜色。",
["Shows an enemy castbar on target unit frame."] = "显示敌对目标单位的施法条。",
["Shows currently equipped items on tooltips while the shift key is pressed."] = "按下<Shift>键时与当前与装备物品在进行比较。",
["Shows the vendor sell values on all item tooltips."] = "物品的提示框中显示物品的售价。",
["Social & Chat"] = "社交&聊天",
["Social Colors"] = "显示职业颜色",
["Tooltip Details"] = "提示框详情",
["Tooltip & Items"] = "提示框&物品",
["Turns the entire interface into darker colors."] = "将整个用户界面元素改为暗黑向。",
["Turns the world map into a movable window. The map can be scaled with <Ctrl> + Mousewheel."] = "让世界地图窗口可移动,可以按住<Ctrl>并滚动鼠标滚轮进行缩放。",
["Turtle WoW Compatibility"] = "乌龟服兼容修改",
["Unit Frame Big Health"] = "单位框架大生命文本",
["Unit Frame Class Colors"] = "单位框架职业着色",
["Unit Frame Class Portraits"] = "单位框架职业图标头像",
["Unit Frame Health Colors"] = "单位框架血条颜色",
["Unit Frames"] = "单位框架",
["Vendor Values"] = "售卖价格",
["WorldMap Class Colors"] = "世界地图职业着色",
["WorldMap Coordinates"] = "世界地图显示坐标",
["WorldMap Window"] = "世界地图窗口",
["World & MiniMap"] = "地图&小地图",
["Your vendor trash has been sold and you earned"] = "你的垃圾物品已出售,你获得了:",
}
-86
View File
@@ -1,86 +0,0 @@
ShaguTweaks_translation = ShaguTweaks_translation or {}
ShaguTweaks_translation["zhTW"] = {
["Adds a castbar to the nameplate based on combat log estimations."] = nil,
["Adds a “Sell Junk” button to every merchant window, that sells all grey items."] = nil,
["Adds a small 24h clock to the mini map."] = nil,
["Adds class colors to the player, target and party unit frames."] = nil,
["Adds compatibility to Turtle WoW's custom changes."] = nil,
["Adds coordinates to the bottom of the World Map."] = nil,
["Advanced Options"] = nil,
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil,
["Auto Dismount"] = nil,
["Automatically dismounts whenever a spell is casted."] = nil,
["Automatically switch to the required warrior or druid stance on spell cast."] = nil,
["Auto Stance"] = nil,
["Blue Shaman Class Colors"] = nil,
["Change health text color based on its value."] = nil,
["Changes the class color code of shamans to blue, as known from TBC+."] = nil,
["Changes the nameplate health bar color to the class color."] = nil,
["Chat Hyperlinks"] = nil,
["Chat Tweaks"] = nil,
["Clock"] = nil,
["Cooldown Numbers"] = nil,
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil,
["Cursor"] = nil,
["Darkened UI"] = nil,
["Debuff Timer"] = nil,
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil,
["Display the remaining duration as text on every cooldown."] = nil,
["Draw the mini map in a squared shape instead of a round one."] = nil,
["Enemy Castbars"] = nil,
["Equip Compare"] = nil,
["Estimates health numbers, and shows numbers on player, pet and target unit frames."] = nil,
["General"] = nil,
["Hide Errors"] = nil,
["Hide Gryphons"] = nil,
["Hides and ignores all Lua errors produced by broken addons."] = nil,
["Hides the gryphons left and right of the action bar."] = nil,
["Hides unnecessary mini map buttons and allows to zoom using the mouse wheel."] = nil,
["Increases the healthbar of the player and target unitframe."] = nil,
["Item Rarity Borders"] = nil,
["Localtime"] = nil,
["Makes all nameplates honor the UI-Scale setting."] = nil,
["MiniMap Clock"] = nil,
["MiniMap Square"] = nil,
["MiniMap Tweaks"] = nil,
["Movable Unit Frames"] = nil,
["N/A"] = nil,
["Nameplate Castbar"] = nil,
["Nameplate Class Colors"] = nil,
["Nameplate Scale"] = nil,
["Nameplates"] = nil,
["Player"] = nil,
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil,
["Real Health Numbers"] = nil,
["Reduced Actionbar Size"] = nil,
["Reduces the actionbar size by removing several items such as the bag panel and microbar"] = nil,
["Replace unitframe portraits with class icons."] = nil,
["Sell Grey Items"] = nil,
["Sell Junk"] = nil,
["Servertime"] = nil,
["Show class colored circles on world and battlefield map."] = nil,
["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil,
["Social & Chat"] = nil,
["Social Colors"] = nil,
["Tooltip Details"] = nil,
["Tooltip & Items"] = nil,
["Turns the entire interface into darker colors."] = nil,
["Turns the world map into a movable window. The map can be scaled with <Ctrl> + Mousewheel."] = nil,
["Turtle WoW Compatibility"] = nil,
["Unit Frame Big Health"] = nil,
["Unit Frame Class Colors"] = nil,
["Unit Frame Class Portraits"] = nil,
["Unit Frame Health Colors"] = nil,
["Unit Frames"] = nil,
["Vendor Values"] = nil,
["WorldMap Class Colors"] = nil,
["WorldMap Coordinates"] = nil,
["WorldMap Window"] = nil,
["World & MiniMap"] = nil,
["Your vendor trash has been sold and you earned"] = nil,
}
+59
View File
@@ -43,17 +43,71 @@ ShaguTweaks:RegisterEvent("VARIABLES_LOADED")
ShaguTweaks:SetScript("OnEvent", function() ShaguTweaks:SetScript("OnEvent", function()
-- flag all external modules as unofficial -- flag all external modules as unofficial
if event == "ADDON_LOADED" then if event == "ADDON_LOADED" then
-- Most Extras ClassicAPI modules are built in now. With the old addon
-- still enabled, its copies replace the built-in ones (same titles).
if arg1 == "ShaguTweaks-extras" then
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00ShaguTweaks:|r the Extras modules are now built in. Disable the |cffffcc00ShaguTweaks Extras ClassicAPI|r addon so they only run once.")
end
ShaguTweaks.provider = nil ShaguTweaks.provider = nil
official = false official = false
return return
end end
-- load current expansion -- load current expansion
if not ShaguTweaks.API.IsSupportedRuntime() then
DEFAULT_CHAT_FRAME:AddMessage("|cffff6060ShaguTweaks:|r ClassicAPI " .. ShaguTweaks.API.minimum_version_text
.. "+ is required. Update both its DLL and !!!ClassicAPI addon, then restart the client. Tweaks have not been enabled.")
return
end
local expansion = ShaguTweaks:GetExpansion() local expansion = ShaguTweaks:GetExpansion()
-- initialize empty config -- initialize empty config
if not ShaguTweaks_config then ShaguTweaks_config = {} end if not ShaguTweaks_config then ShaguTweaks_config = {} end
local prediction = ShaguTweaks.T["Healing Predictions"]
for _, oldName in ipairs({"Enable Healing Prediction", "Enable Healing Prediction on Unit Frames"}) do
local oldPrediction = ShaguTweaks.T[oldName]
if ShaguTweaks_config[prediction] == nil then ShaguTweaks_config[prediction] = ShaguTweaks_config[oldPrediction] end
ShaguTweaks_config[oldPrediction] = nil
end
-- Retire the removed duplicate toggle; ClassicAPI owns these preferences.
ShaguTweaks_config[ShaguTweaks.T["Auto Dismount"]] = nil
-- Area looting is delegated to a dedicated addon from 3.8.6 onward.
ShaguTweaks_config[ShaguTweaks.T["AoE Loot"]] = nil
-- Gold numbers belong to Real Health Numbers; retire the duplicate toggle.
ShaguTweaks_config["Unit Frame Health Colors"] = nil
-- The portrait-only hiding option has been removed.
ShaguTweaks_config[ShaguTweaks.T["Hide Player Portrait"]] = nil
-- Split the former shared setting without overriding either new choice.
local oldBigHealth = ShaguTweaks_config[ShaguTweaks.T["Unit Frame Big Health"]]
if oldBigHealth ~= nil then
local playerKey = ShaguTweaks.T["Unit Frame Big Health for Player"]
local targetKey = ShaguTweaks.T["Unit Frame Big Health for Target"]
if ShaguTweaks_config[playerKey] == nil then ShaguTweaks_config[playerKey] = oldBigHealth end
if ShaguTweaks_config[targetKey] == nil then ShaguTweaks_config[targetKey] = oldBigHealth end
ShaguTweaks_config[ShaguTweaks.T["Unit Frame Big Health"]] = nil
end
-- Replace detached native bars with copies; hiding the originals is opt-in.
local oldStandalone = ShaguTweaks.T["Standalone Player Frame"]
local resourceDisplay = ShaguTweaks.T["Personal Resource Display"]
if ShaguTweaks_config[resourceDisplay] == nil then
ShaguTweaks_config[resourceDisplay] = ShaguTweaks_config[oldStandalone]
end
if ShaguTweaks_config.StandalonePlayerFrame then
local positions = ShaguTweaks_config.PersonalResourceDisplay or {}
for _, key in ipairs({"Health", "Power"}) do
if not positions[key] then positions[key] = ShaguTweaks_config.StandalonePlayerFrame[key] end
end
ShaguTweaks_config.PersonalResourceDisplay = positions
end
ShaguTweaks_config[oldStandalone] = nil
ShaguTweaks_config.StandalonePlayerFrame = nil
-- initialize overwrites -- initialize overwrites
ShaguTweaks_config.overwrites = ShaguTweaks_config.overwrites or {} ShaguTweaks_config.overwrites = ShaguTweaks_config.overwrites or {}
@@ -85,6 +139,11 @@ ShaguTweaks:SetScript("OnEvent", function()
end end
end end
-- Cast-bar movement is always available, independent of saved option flags.
if expansion == "vanilla" and ShaguTweaks.EnableCastBarMover then
ShaguTweaks.EnableCastBarMover()
end
-- Libraries that can stay dormant until a module actually consumes them -- Libraries that can stay dormant until a module actually consumes them
-- get one final activation pass after every enabled module has registered -- get one final activation pass after every enabled module has registered
-- its callbacks. -- its callbacks.
+40
View File
@@ -0,0 +1,40 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Dragonflight Gryphons"],
description = T["Replaces actionbar gryphons with the dragonflight version."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Action Bar"],
config = {
["dragonfly.gryphon"] = "retail",
},
enabled = nil,
})
module.enable = function(self)
local gryphon = CreateFrame("Frame", nil, MainMenuBarArtFrame)
gryphon:SetFrameStrata("HIGH")
gryphon:RegisterEvent("PLAYER_ENTERING_WORLD")
gryphon:SetScript("OnEvent", function()
-- replace original gryphons by dragonflight versions
if module.config["dragonfly.gryphon"] == "retail" and UnitFactionGroup("player") == "Horde" then
-- retail horde
MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-wyvern")
MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-wyvern")
elseif module.config["dragonfly.gryphon"] == "retail" and UnitFactionGroup("player") == "Alliance" then
-- retail alliance
MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon")
MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon")
else
-- beta artwork
MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon-beta")
MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon-beta")
end
-- move gryphons above action buttons
MainMenuBarLeftEndCap:SetParent(this)
MainMenuBarRightEndCap:SetParent(this)
end)
end
+107
View File
@@ -0,0 +1,107 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({
title = T["Floating Actionbar"],
description = T["Removes all background textures and lets the actionbar float."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Action Bar"],
enabled = nil,
})
local texture_removals = {
MainMenuXPBarTexture0, MainMenuXPBarTexture1, MainMenuXPBarTexture2, MainMenuXPBarTexture3,
ReputationXPBarTexture0, ReputationXPBarTexture1, ReputationXPBarTexture2, ReputationXPBarTexture3,
ReputationWatchBarTexture0, ReputationWatchBarTexture1, ReputationWatchBarTexture2, ReputationWatchBarTexture3,
MainMenuBarTexture0, MainMenuBarTexture1, MainMenuBarTexture2, MainMenuBarTexture3,
BonusActionBarTexture1, BonusActionBarTexture0, BonusActionBarTexture2
}
local actionbars = {
"Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight",
"MultiBarLeft", "MultiBarRight", "Shapeshift"
}
module.enable = function(self)
MainMenuBar:ClearAllPoints()
MainMenuBar:SetPoint("BOTTOM", 0, 8)
-- align actionbutton textures and add border
for _, prefix in ipairs(actionbars) do
for i = 1, NUM_ACTIONBAR_BUTTONS do
local button = _G[prefix .. "Button" .. i]
local texture = _G[prefix.."Button"..i.."NormalTexture"]
if button and texture then
texture:SetWidth(60)
texture:SetHeight(60)
texture:SetPoint("CENTER", 0, 0)
ShaguTweaks.AddBorder(button, 3, { r=.7, g=.7, b=.7, a=1 })
end
end
end
ShaguTweaks.AddBorder(MainMenuBarPerformanceBarFrameButton, { -12, -0.5, -8, 4.5 }, { r=.7, g=.7, b=.7, a=1 })
-- replace reputation bar texture
ReputationWatchStatusBar:SetStatusBarTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
ReputationWatchStatusBarBackground:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
ReputationWatchStatusBarBackground:SetVertexColor(0, 0, 0, .5)
-- replace experience bar texture
MainMenuExpBar:SetStatusBarTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
local _, _, _, _, _, background = MainMenuExpBar:GetRegions()
if background then
background:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
background:SetVertexColor(0, 0, 0, .5)
end
-- center the experience bar when Floating Actionbar is used on its own.
-- Reduced Actionbar applies its own matching offset, so avoid shifting it twice.
local function UpdateExperiencePosition()
local reducedEnabled = ShaguTweaks_config
and ShaguTweaks_config[T["Reduced Actionbar Size"]] == 1
if reducedEnabled then return end
MainMenuExpBar:ClearAllPoints()
if ReputationWatchBar:IsShown() then
MainMenuExpBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 0)
else
MainMenuExpBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 3)
end
end
-- keep the custom reputation position without replacing Blizzard's updater
local function UpdateReputationPosition()
UpdateExperiencePosition()
ReputationWatchBar:ClearAllPoints()
if MainMenuExpBar:IsShown() then
ReputationWatchBar:SetPoint("BOTTOM", MainMenuBar, "TOP", 0, -7)
else
ReputationWatchBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 2)
end
end
hooksecurefunc("ReputationWatchBar_Update", UpdateReputationPosition)
-- Blizzard can restore the default XP bar anchor after login/reloads and
-- actionbar visibility changes, so re-apply the fixed floating offset.
hooksecurefunc("UIParent_ManageFramePositions", UpdateExperiencePosition)
UpdateReputationPosition()
-- hide max level top frame
MainMenuBarMaxLevelBar:SetAlpha(0)
-- remove textures
for _, texture in ipairs(texture_removals) do
if texture then
texture:SetTexture()
texture:Hide()
end
end
end
+62
View File
@@ -0,0 +1,62 @@
-- Extend the native extra-bar preference to main/bonus action buttons.
-- Own exactly one grid reference; drag-and-drop owns its separate reference.
local _G = ShaguTweaks.GetGlobalEnv()
local function BonusVisible()
return _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible()
end
local function HideCoveredButton(button)
if ShaguTweaks.API.IsSupportedRuntime() and BonusVisible() then button:Hide() end
end
local function RefreshBaseButtons()
if not ShaguTweaks.API.IsSupportedRuntime() then return end
local previous = this
for i = 1, 12 do
local button = _G["ActionButton" .. i]
if button then
if BonusVisible() then button:Hide()
else
-- Let the native updater restore filled/empty slot visibility using
-- the existing grid references. Never change the contents of a slot.
this = button
ActionButton_Update()
end
end
end
this = previous
end
local function UpdateGrid()
if not ShaguTweaks.API.IsSupportedRuntime() then return end
local show = _G.ALWAYS_SHOW_MULTIBARS == 1 or _G.ALWAYS_SHOW_MULTIBARS == "1"
for _, prefix in ipairs({"ActionButton", "BonusActionButton"}) do
for i = 1, 12 do
local button = _G[prefix .. i]
if button then
if show and not button.stAlwaysShowGrid then
button.stAlwaysShowGrid = true
ActionButton_ShowGrid(button)
elseif not show and button.stAlwaysShowGrid then
button.stAlwaysShowGrid = nil
ActionButton_HideGrid(button)
end
end
end
end
RefreshBaseButtons()
end
ShaguTweaks.hooksecurefunc("MultiActionBar_UpdateGridVisibility", UpdateGrid)
-- Floating artwork no longer masks the caster bar below the form bar.
-- Native button refreshes and dragging can call Show again: keep covered
-- buttons hidden until the actual bonus frame closes (including animations).
for i = 1, 12 do
local button = _G["ActionButton" .. i]
if button then ShaguTweaks.hooksecurefunc(button, "Show", HideCoveredButton) end
end
if _G.BonusActionBarFrame then
ShaguTweaks.HookScript(BonusActionBarFrame, "OnShow", RefreshBaseButtons)
ShaguTweaks.HookScript(BonusActionBarFrame, "OnHide", RefreshBaseButtons)
end
local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:SetScript("OnEvent", function()
if ShaguTweaks.API.IsSupportedRuntime() then UpdateGrid() end
end)
+34
View File
@@ -0,0 +1,34 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Hide Macro Text"],
description = T["Hides macro names on action buttons."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
local function HideMacroText(button)
if not button or not button.GetName then return end
local name = button:GetName()
local text = name and _G[name .. "Name"]
if text then text:SetAlpha(0) end
end
for i = 1, 24 do
HideMacroText(_G["BonusActionButton" .. i])
end
for i = 1, 12 do
HideMacroText(_G["ActionButton" .. i])
HideMacroText(_G["MultiBarRightButton" .. i])
HideMacroText(_G["MultiBarLeftButton" .. i])
HideMacroText(_G["MultiBarBottomLeftButton" .. i])
HideMacroText(_G["MultiBarBottomRightButton" .. i])
end
end
+15
View File
@@ -0,0 +1,15 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Mouseover Right"],
description = T["Hide the right actionbar and show it on mouseover."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
ShaguTweaks.CreateMouseoverActionBar(MultiBarRight, "SHOW_MULTI_ACTIONBAR_3")
end
+15
View File
@@ -0,0 +1,15 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Mouseover Right 2"],
description = T["Hide the second right actionbar and show it on mouseover."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
ShaguTweaks.CreateMouseoverActionBar(MultiBarLeft, "SHOW_MULTI_ACTIONBAR_4")
end
+138
View File
@@ -0,0 +1,138 @@
-- Shared mouseover actionbar helper.
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local _G = ShaguTweaks.GetGlobalEnv()
if not ShaguTweaks.CreateMouseoverActionBar then
ShaguTweaks.CreateMouseoverActionBar = function(bar, visibilityFlag)
if not bar or not visibilityFlag then return end
if bar.ShaguTweaksMouseoverController then return end
local controller = CreateFrame("Frame", nil, UIParent)
local hotspot = CreateFrame("Frame", nil, UIParent)
controller.bar = bar
controller.hotspot = hotspot
controller.visibilityFlag = visibilityFlag
controller.enabled = false
controller.elapsed = 0
controller.idle = 0
bar.ShaguTweaksMouseoverController = controller
-- One invisible reveal area replaces the original per-button overlay
-- frames. It only accepts mouse input while the actionbar is hidden.
hotspot:SetAllPoints(bar)
hotspot:SetFrameStrata("DIALOG")
hotspot:EnableMouse(false)
hotspot:Hide()
local function ReadEnabled()
return _G[visibilityFlag] and true or false
end
local function StopWatching()
controller:SetScript("OnUpdate", nil)
controller.elapsed = 0
controller.idle = 0
end
local function StartWatching()
if not controller.enabled or not bar:IsShown() then return end
hotspot:EnableMouse(false)
controller.elapsed = 0
controller.idle = 0
controller:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + (arg1 or 0)
if this.elapsed < .10 then return end
local step = this.elapsed
this.elapsed = 0
if not this.enabled then
StopWatching()
hotspot:EnableMouse(false)
hotspot:Hide()
return
end
if not bar:IsShown() then
StopWatching()
hotspot:Show()
hotspot:EnableMouse(true)
return
end
if MouseIsOver(bar) then
this.idle = 0
return
end
this.idle = this.idle + step
if this.idle >= 2 then
StopWatching()
bar:Hide()
end
end)
end
local function Sync()
controller.enabled = ReadEnabled()
if not controller.enabled then
StopWatching()
hotspot:EnableMouse(false)
hotspot:Hide()
return
end
hotspot:Show()
if bar:IsShown() then
StartWatching()
else
hotspot:EnableMouse(true)
end
end
hotspot:SetScript("OnEnter", function()
if not controller.enabled then return end
hotspot:EnableMouse(false)
bar:Show()
-- The bar's OnShow script starts the watcher. Avoid a second redundant
-- StartWatching() call here.
end)
-- Keep existing scripts intact while observing native/UI-addon visibility
-- changes. These wrappers are installed once per bar.
local oldOnShow = bar:GetScript("OnShow")
bar:SetScript("OnShow", function()
if oldOnShow then oldOnShow() end
if controller.enabled then StartWatching() end
end)
local oldOnHide = bar:GetScript("OnHide")
bar:SetScript("OnHide", function()
if oldOnHide then oldOnHide() end
StopWatching()
if controller.enabled then
hotspot:Show()
hotspot:EnableMouse(true)
else
hotspot:EnableMouse(false)
hotspot:Hide()
end
end)
controller:RegisterEvent("PLAYER_ENTERING_WORLD")
controller:RegisterEvent("CVAR_UPDATE")
controller:SetScript("OnEvent", Sync)
Sync()
end
end
+402
View File
@@ -0,0 +1,402 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local S = { states = {} }
local Unlocked = ShaguTweaks.API.AreMoveModifiersDown
function S.IsMoved(name)
return S.positions and S.positions[name] ~= nil
end
local function Apply(state)
local position = S.positions[state.name]
if not position then return end
state.clear(state.frame)
state.point(state.frame, "TOPLEFT", UIParent, "BOTTOMLEFT", position[1], position[2])
end
local function Layout()
if _G.UIParent_ManageFramePositions then _G.UIParent_ManageFramePositions() end
if _G.ReputationWatchBar_Update then _G.ReputationWatchBar_Update() end
end
-- Detect anchor chains as well as parent chains. Pin attached, movable bars
-- before moving their owner so moving one does not drag the others with it.
local function DependsOn(frame, owner, depth)
if not frame or frame == UIParent or depth > 12 then return false end
if frame == owner then return true end
if frame.GetParent and DependsOn(frame:GetParent(), owner, depth + 1) then return true end
if frame.GetPoint then
local count = frame.GetNumPoints and frame:GetNumPoints() or 1
for i = 1, count do
local point, relative = frame:GetPoint(i)
if type(relative) == "string" then relative = _G[relative] end
if point and relative and DependsOn(relative, owner, depth + 1) then return true end
end
end
return false
end
local function PinDependents(owner)
for _, state in ipairs(S.states) do
if state ~= owner and not S.positions[state.name] and DependsOn(state.frame, owner.frame, 0) then
local x, y = state.frame:GetLeft(), state.frame:GetTop()
if x and y then S.positions[state.name] = { x, y }; Apply(state) end
end
end
end
-- Capture native neighbour offsets before applying any saved positions.
-- Keeping the entire offset preserves intentional insets as well as gaps
-- (the pet/stance buttons do not start at the main frame's left edge).
local function CaptureSnapOffsets()
for _, state in ipairs(S.states) do
state.snapOffsets = {}
local frame = state.frame
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
local x, y = frame:GetLeft(), frame:GetTop()
if x and y then
x, y = x * scale, y * scale
for _, other in ipairs(S.states) do
if other ~= state then
local target = other.frame
local targetScale = target:GetEffectiveScale() / UIParent:GetEffectiveScale()
local tx, ty = target:GetLeft(), target:GetTop()
if tx and ty then
local dx, dy = x - tx * targetScale, y - ty * targetScale
local width = math.max(frame:GetWidth() * scale, target:GetWidth() * targetScale)
local height = math.max(frame:GetHeight() * scale, target:GetHeight() * targetScale)
if math.abs(dx) <= width + 20 and math.abs(dy) <= height + 20 then
table.insert(state.snapOffsets, { target = other, x = dx, y = dy })
end
end
end
end
end
end
end
local function SnapTargetVisible(state)
local frame = state.frame
if not frame:IsVisible() then return false end
local parent = frame
while parent do
if parent.GetAlpha and parent:GetAlpha() == 0 then return false end
parent = parent:GetParent()
end
if state.name == "MainMenuExpBar" then
-- The container can remain shown at the level cap without an XP bar.
if _G.UnitLevel and UnitLevel("player") >= (_G.MAX_PLAYER_LEVEL or 60) then return false end
elseif state.name == "ReputationWatchBar" then
if not _G.GetWatchedFactionInfo or not GetWatchedFactionInfo() then return false end
local status = _G.ReputationWatchStatusBar
if not status or not status:IsVisible() or (status.GetAlpha and status:GetAlpha() == 0) then return false end
end
return true
end
local function ButtonBounds(state)
local first, last = _G[state.first or ""], _G[state.last or ""]
if state.name == "MainMenuBar" and _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then
first, last = _G.BonusActionButton1, _G.BonusActionButton12
end
if not first or not last then return end
local l, t, r, b = first:GetLeft(), first:GetTop(), last:GetRight(), last:GetBottom()
if not l or not t or not r or not b then return end
local a = first:GetEffectiveScale() / UIParent:GetEffectiveScale()
local z = last:GetEffectiveScale() / UIParent:GetEffectiveScale()
return l * a, t * a, r * z, b * z
end
local function MainActionPair(a, b)
return not a.progress and not b.progress
and (a.name == "MainMenuBar" or b.name == "MainMenuBar")
end
function S.Snap(state, x, y)
local scale = state.frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
local px, py = x * scale, y * scale
local best, resultX, resultY = 289, x, y
for _, offset in ipairs(state.snapOffsets) do
local target = offset.target.frame
if not MainActionPair(state, offset.target) and SnapTargetVisible(offset.target) then
local targetScale = target:GetEffectiveScale() / UIParent:GetEffectiveScale()
local tx, ty = target:GetLeft(), target:GetTop()
if tx and ty then
tx, ty = tx * targetScale + offset.x, ty * targetScale + offset.y
local dx, dy = tx - px, ty - py
local distance = dx * dx + dy * dy
if math.abs(dx) <= 12 and math.abs(dy) <= 12 and distance < best then
best, resultX, resultY = distance, tx / scale, ty / scale
end
end
end
end
-- MainMenuBar's native rectangle contains unused artwork. Its old frame
-- offsets must never determine the visible gap between action-button rows.
local l, t, r, b = ButtonBounds(state)
if l then
local ox = l - state.frame:GetLeft() * scale
local oy = t - state.frame:GetTop() * scale
local height = t - b
local gap = 6 * scale
if _G.ActionButton1 and _G.ActionButton2 then
local right, left = ActionButton1:GetRight(), ActionButton2:GetLeft()
if right and left then
local measured = left * ActionButton2:GetEffectiveScale() / UIParent:GetEffectiveScale()
- right * ActionButton1:GetEffectiveScale() / UIParent:GetEffectiveScale()
if measured >= 0 and measured <= 20 then gap = measured end
end
end
for _, other in ipairs(S.states) do
if other ~= state and MainActionPair(state, other) and SnapTargetVisible(other) then
local tl, tt, tr, tb = ButtonBounds(other)
if tl then
for side = 1, 2 do
local tx = tl - ox
local ty = (side == 1 and tb - gap or tt + gap + height) - oy
local dx, dy = tx - px, ty - py
local distance = dx * dx + dy * dy
if math.abs(dx) <= 12 and math.abs(dy) <= 12 and distance < best then
best, resultX, resultY = distance, tx / scale, ty / scale
end
end
end
end
end
end
return resultX, resultY
end
local function Finish(save)
local state = S.dragging
if not state then return end
S.dragging = nil
S.outline:StopMovingOrSizing()
local x, y = S.outline:GetLeft(), S.outline:GetTop()
S.outline:Hide()
if save and x and y then
x, y = S.Snap(state, x - state.dragOffsetX, y - state.dragOffsetY)
PinDependents(state)
S.positions[state.name] = { x, y }
Apply(state)
-- Positions are stored in addon settings; native bars need not be movable.
Layout()
end
end
local function Reset(state)
Finish(false)
S.positions[state.name] = nil
state.clear(state.frame)
for _, point in ipairs(state.defaults) do state.point(state.frame, unpack(point)) end
Layout()
end
local function Begin()
if not Unlocked() then return end
Finish(true)
local state = this.state
local x, y = state.frame:GetLeft(), state.frame:GetTop()
if not x or not y then return end
local left, top = x, y
local width, height = state.frame:GetWidth(), math.max(state.frame:GetHeight(), 14)
-- Main and pet frames include artwork beyond the visible button row.
-- Read button edges directly; anchored marker dimensions are not reliable
-- on every supported client.
if state.name == "MainMenuBar" or state.name == "PetActionBarFrame" then
local first, last = _G.ActionButton1, _G.ActionButton12
if state.name == "PetActionBarFrame" then
first, last = _G.PetActionButton1, _G.PetActionButton10
elseif _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then
first, last = _G.BonusActionButton1, _G.BonusActionButton12
end
if first and last then
local scale = first:GetEffectiveScale() / state.frame:GetEffectiveScale()
local lastScale = last:GetEffectiveScale() / state.frame:GetEffectiveScale()
local l, t, r, b = first:GetLeft(), first:GetTop(), last:GetRight(), last:GetBottom()
if l and t and r and b then
left, top = l * scale - 2, t * scale + 2
width, height = r * lastScale + 2 - left, top - b * lastScale + 2
end
end
end
state.dragOffsetX, state.dragOffsetY = left - x, top - y
S.dragging = state
local outline = S.outline
outline:SetScale(state.frame:GetEffectiveScale() / UIParent:GetEffectiveScale())
outline:SetWidth(width)
outline:SetHeight(height)
outline:ClearAllPoints()
outline:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
outline:Show()
outline:StartMoving()
end
local function MarkerClick()
if Unlocked() and arg1 == "RightButton" then Reset(this.state) end
end
local function UpdateMarkers()
local unlocked = Unlocked()
if not unlocked then Finish(true) end
for _, state in ipairs(S.states) do
local controller = state.frame.ShaguTweaksMouseoverController
local visible = state.progress or state.frame:IsVisible() or (controller and controller.enabled)
if unlocked and visible then
-- Follow actual button bounds; the main frame also includes gryphons
-- and XP artwork outside its clickable action-button row.
local first = state.first and _G[state.first]
local last = state.last and _G[state.last]
if state.name == "MainMenuBar" and _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then
first, last = _G.BonusActionButton1, _G.BonusActionButton12
elseif state.name == "ShapeshiftBarFrame" and type(_G.GetNumShapeshiftForms) == "function" then
first, last = _G.ShapeshiftButton1, _G["ShapeshiftButton" .. GetNumShapeshiftForms()]
end
state.marker:ClearAllPoints()
if first and last then
state.marker:SetPoint("TOPLEFT", first, "TOPLEFT", -2, 2)
state.marker:SetPoint("BOTTOMRIGHT", last, "BOTTOMRIGHT", 2, -2)
elseif state.progress then
local anchor = state.name == "MainMenuExpBar" and "TOPLEFT" or "BOTTOMLEFT"
state.marker:SetPoint(anchor, state.frame, anchor, 0, 0)
state.marker:SetWidth(state.frame:GetWidth())
state.marker:SetHeight(14)
else
state.marker:SetAllPoints(state.frame)
end
state.marker:Show()
else
if S.dragging == state then Finish(true) end
state.marker:Hide()
end
end
end
local function MarkerStyle(marker)
marker:SetFrameStrata("DIALOG")
marker:SetFrameLevel(100)
marker:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 8,
edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } })
marker:SetBackdropColor(0, 0, 0, .7)
local color = ShaguTweaks.UIBorder
marker:SetBackdropBorderColor(color and color.r or 1, color and color.g or .82, color and color.b or 0, 1)
end
local function Add(definition)
local frame = _G[definition[1]]
if not frame then return end
local state = { name = definition[1], frame = frame, defaults = {}, first = definition[3],
last = definition[4], progress = definition[5], point = frame.SetPoint, clear = frame.ClearAllPoints }
for i = 1, (frame.GetNumPoints and frame:GetNumPoints() or 1) do
local p, relative, rp, x, y = frame:GetPoint(i)
if p then table.insert(state.defaults, { p, relative or UIParent, rp or p, x or 0, y or 0 }) end
end
-- Guard BOTH operations: ignoring SetPoint alone would let a preceding
-- ClearAllPoints detach a manually positioned bar during native relayout.
frame.ClearAllPoints = function(self)
if S.positions[state.name] then return end
state.clear(self)
end
frame.SetPoint = function(self, a, b, c, d, e)
if S.positions[state.name] then return end
state.point(self, a, b, c, d, e)
end
local marker = CreateFrame("Button", "ShaguTweaksMover" .. state.name, UIParent)
MarkerStyle(marker)
marker:SetScale(frame:GetEffectiveScale() / UIParent:GetEffectiveScale())
marker:EnableMouse(true)
marker:RegisterForDrag("LeftButton")
marker:RegisterForClicks("RightButtonUp")
marker.state = state
marker:SetScript("OnDragStart", Begin)
marker:SetScript("OnDragStop", function() Finish(true) end)
marker:SetScript("OnClick", MarkerClick)
marker:SetScript("OnHide", function() if S.dragging == this.state then Finish(true) end end)
marker.text = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
marker.text:SetPoint("CENTER", marker, "CENTER", 0, 0)
marker.text:SetText(T[definition[2]])
marker:Hide()
state.marker = marker
table.insert(S.states, state)
ShaguTweaks.HookScript(frame, "OnShow", UpdateMarkers)
ShaguTweaks.HookScript(frame, "OnHide", UpdateMarkers)
end
local function Setup()
if S.installed then return end
S.installed = true
ShaguTweaks_config.ActionBarPositions = ShaguTweaks_config.ActionBarPositions or {}
S.positions = ShaguTweaks_config.ActionBarPositions
ShaguTweaks.ActionBarMovers = S
S.outline = CreateFrame("Frame", "ShaguTweaksActionBarDragOutline", UIParent)
MarkerStyle(S.outline)
S.outline:SetMovable(true)
S.outline:SetClampedToScreen(true)
S.outline:SetScript("OnHide", function() if S.dragging then Finish(true) end end)
S.outline:Hide()
if S.actions then
local definitions = {
{ "MainMenuBar", "Main Action Bar", "ActionButton1", "ActionButton12" },
{ "MultiBarBottomLeft", "Bottom Left Bar", "MultiBarBottomLeftButton1", "MultiBarBottomLeftButton12" },
{ "MultiBarBottomRight", "Bottom Right Bar", "MultiBarBottomRightButton1", "MultiBarBottomRightButton12" },
{ "MultiBarRight", "Right Bar", "MultiBarRightButton1", "MultiBarRightButton12" },
{ "MultiBarLeft", "Right Bar 2", "MultiBarLeftButton1", "MultiBarLeftButton12" },
{ "PetActionBarFrame", "Pet Action Bar", "PetActionButton1", "PetActionButton10" },
{ "ShapeshiftBarFrame", "Stance Bar" },
}
for _, definition in ipairs(definitions) do Add(definition) end
end
if S.progress then
Add({ "MainMenuExpBar", "Experience Bar", nil, nil, true })
Add({ "ReputationWatchBar", "Reputation Bar", nil, nil, true })
end
CaptureSnapOffsets()
for _, state in ipairs(S.states) do Apply(state) end
S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
for _, state in ipairs(S.states) do Apply(state) end
end
UpdateMarkers()
end)
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
if API.modifierstate then
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
else
S.events.elapsed = 0
S.events:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + (arg1 or 0)
if this.elapsed < .1 then return end
this.elapsed = 0
UpdateMarkers()
end)
end
UpdateMarkers()
end
local function Schedule()
if S.scheduled then return end
S.scheduled = true
API.AfterWorldReady(Setup)
end
local function ReducedEnabled()
local enabled = ShaguTweaks_config[T["Reduced Actionbar Size"]]
if enabled == nil then
local reduced = ShaguTweaks.mods[T["Reduced Actionbar Size"]]
enabled = reduced and reduced.enabled and 1 or 0
end
return enabled == 1
end
ShaguTweaks.EnableActionBarMovers = function()
if not ReducedEnabled() then return end
S.actions = true
S.progress = true
Schedule()
end
+104
View File
@@ -0,0 +1,104 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Proc & Reactive Spell Highlights"],
description = T["Highlights action buttons for active procs and reactive spells using ClassicAPI. Supports spells and macros; central proc artwork is left to other addons."],
expansions = { ["vanilla"] = true }, category = T["Action Bar"], enabled = nil,
})
module.enable = function(self)
if self.states then return end
local query = _G.C_SpellActivationOverlay and _G.C_SpellActivationOverlay.IsSpellOverlayed or _G.IsSpellOverlayed
local valid = _G.C_EventUtils and _G.C_EventUtils.IsEventValid
if type(query) ~= "function" or not API.GetActionInfo or not valid
or not valid("SPELL_ACTIVATION_OVERLAY_GLOW_SHOW")
or not valid("SPELL_ACTIVATION_OVERLAY_GLOW_HIDE") then return end
local states, byButton = {}, {}
self.states = states
local function Paint(state, active)
if active then
if not state.glow:IsShown() then
state.glow:Show()
if state.pulse then state.pulse:Play() end
end
else
if state.pulse then state.pulse:Stop() end
state.glow:Hide()
end
end
local function Sync(state)
local spell
if state.button:IsVisible() then
local slot = ActionButton_GetPagedID(state.button)
if slot then
local kind, id = API.GetActionInfo(slot)
if kind == "spell" then spell = id
elseif kind == "macro" and _G.GetMacroSpell then
local name, rank, spellID = _G.GetMacroSpell(id)
spell = spellID
end
end
end
state.spell = spell
Paint(state, spell and query(spell))
end
for _, prefix in ipairs({"ActionButton", "BonusActionButton", "MultiBarBottomLeftButton",
"MultiBarBottomRightButton", "MultiBarRightButton", "MultiBarLeftButton"}) do
for i = 1, 12 do
local button = _G[prefix .. i]
if button then
local glow = CreateFrame("Frame", nil, button)
glow:SetAllPoints(button)
glow:SetFrameLevel(button:GetFrameLevel() + 5)
glow:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 12 })
glow:SetBackdropBorderColor(.7, .9, 1, 1)
-- Additive light stands out against both dark and gold button rims.
-- Use the client's existing action-button glow artwork.
local halo = glow:CreateTexture(nil, "OVERLAY")
halo:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
halo:SetBlendMode("ADD")
halo:SetVertexColor(.55, .85, 1, 1)
halo:SetPoint("TOPLEFT", button, "TOPLEFT", -8, 8)
halo:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 8, -8)
glow:EnableMouse(false)
glow:Hide()
local state = { button = button, glow = glow, halo = halo }
if type(glow.CreateAnimationGroup) == "function" then
local pulse = glow:CreateAnimationGroup()
local fade = pulse:CreateAnimation("Alpha")
if type(fade.SetFromAlpha) == "function" and type(fade.SetToAlpha) == "function" then
pulse:SetLooping("BOUNCE")
fade:SetFromAlpha(1); fade:SetToAlpha(.7)
fade:SetDuration(.4); fade:SetSmoothing("IN_OUT")
state.pulse = pulse
end
end
table.insert(states, state); byButton[button] = state
local function Changed() Sync(state) end
ShaguTweaks.HookScript(button, "OnShow", Changed)
ShaguTweaks.HookScript(button, "OnHide", Changed)
Sync(state)
end
end
end
ShaguTweaks.hooksecurefunc("ActionButton_Update", function()
local state = byButton[this]
if state then Sync(state) end
end)
local events = CreateFrame("Frame")
self.events = events
for _, name in ipairs({"SPELL_ACTIVATION_OVERLAY_GLOW_SHOW", "SPELL_ACTIVATION_OVERLAY_GLOW_HIDE",
"PLAYER_ENTERING_WORLD", "ACTIONBAR_SLOT_CHANGED", "ACTIONBAR_PAGE_CHANGED",
"UPDATE_BONUS_ACTIONBAR", "UPDATE_MACROS", "ACTIONBAR_UPDATE_STATE",
"PLAYER_TARGET_CHANGED", "MODIFIER_STATE_CHANGED", "SPELLS_CHANGED"}) do
if valid(name) then events:RegisterEvent(name) end
end
events:SetScript("OnEvent", function()
local proc = event == "SPELL_ACTIVATION_OVERLAY_GLOW_SHOW" or event == "SPELL_ACTIVATION_OVERLAY_GLOW_HIDE"
for _, state in ipairs(states) do
if proc then
if state.spell == arg1 then Paint(state, state.button:IsVisible() and query(arg1)) end
else Sync(state) end
end
end)
end
+144 -23
View File
@@ -11,39 +11,153 @@ local module = ShaguTweaks:register({
enabled = nil, enabled = nil,
}) })
-- Modern APIs return booleans; legacy range APIs return 1/0/nil.
-- In Lua, numeric zero is truthy, so normalize both contracts explicitly.
local function NormalizeRange(value)
if value == true or value == 1 then return 1 end
if value == false or value == 0 then return 0 end
end
local function GetActionRange(action) local function GetActionRange(action)
-- ClassicAPI can resolve a real spell/item action and run the same if not UnitExists("target") then return nil end
-- range-only engine test directly against the target unit. This avoids
-- relying on the old action-slot heuristic for normal spell buttons.
if API and API.GetActionInfo then if API and API.GetActionInfo then
local actionType, id = API.GetActionInfo(action) local actionType, id = API.GetActionInfo(action)
if actionType == "spell" and id and API.spellrange
if actionType == "spell" and id and API.IsSpellInRange then and API.IsSpellInRange then
local inRange = API.IsSpellInRange(id, "target") -- ClassicAPI's check is geometric: it does not reject the wrong
if inRange ~= nil then -- faction. A friendly buff should not turn red against an enemy.
return inRange and 1 or 0 local spells = _G.C_Spell
end if spells then
elseif actionType == "item" and id and API.IsItemInRange then if type(spells.IsSpellHelpful) == "function"
local inRange = API.IsItemInRange(id, "target") and spells.IsSpellHelpful(id) and type(UnitCanAssist) == "function"
if inRange ~= nil then and not UnitCanAssist("player", "target") then return nil end
return inRange and 1 or 0 if type(spells.IsSpellHarmful) == "function"
and spells.IsSpellHarmful(id) and type(UnitCanAttack) == "function"
and not UnitCanAttack("player", "target") then return nil end
end end
-- nil is authoritative: self/rangeless spells have no range to
-- fail. Never turn it into an old action-slot false result.
return NormalizeRange(API.IsSpellInRange(id, "target"))
elseif actionType == "item" and id and API.itemrange
and API.IsItemInRange then
return NormalizeRange(API.IsItemInRange(id, "target"))
end end
end end
-- Macros, unresolved bag-item actions and older ClassicAPI versions keep -- Macros, unresolved actions and clients without the native capability
-- Vanilla's native action-slot range check as the compatibility fallback. -- retain the action-slot compatibility fallback.
return IsActionInRange(action) return NormalizeRange(IsActionInRange(action))
end
local RANGE_INTERVAL = TOOLTIP_UPDATE_TIME or 0.2
-- Newer ClassicAPI owns the range sampling; Lua reacts only to slot changes.
local function EnableNativeRange()
local actionbar = _G.C_ActionBar
if not actionbar or type(actionbar.EnableActionRangeCheck) ~= "function"
or not API.eventutils or not _G.C_EventUtils.IsEventValid("ACTION_RANGE_CHECK_UPDATE") then return false end
local states, byButton, ranges = {}, {}, {}
local function Paint(state)
local button = state.button
local slot = state.slot
if not slot then return end
local icon = state.icon
if not icon then return end
local result = ranges[slot]
-- Keep invalid-faction spells neutral, as the old module did.
local kind, id = API.GetActionInfo(slot)
local spells = _G.C_Spell
if result == 0 and kind == "spell" and spells then
if (spells.IsSpellHelpful and spells.IsSpellHelpful(id) and not UnitCanAssist("player", "target"))
or (spells.IsSpellHarmful and spells.IsSpellHarmful(id) and not UnitCanAttack("player", "target")) then result = nil end
end
if result == 0 then
icon:SetVertexColor(button.r or .85, button.g or .18, button.b or .18, button.a or 1)
else
local usable, noMana = IsUsableAction(slot)
if usable and usable ~= 0 then icon:SetVertexColor(1, 1, 1, 1)
elseif noMana and noMana ~= 0 then icon:SetVertexColor(.5, .5, 1, 1)
else icon:SetVertexColor(.4, .4, .4, 1) end
end
end
local function Sync(state)
local slot = state.button:IsVisible() and ActionButton_GetPagedID(state.button) or nil
if slot and (slot < 1 or slot > 120) then slot = nil end
if slot ~= state.slot then
local old = state.slot
state.slot = slot
if old then actionbar.EnableActionRangeCheck(old, false) end
if slot then
ranges[slot] = nil
actionbar.EnableActionRangeCheck(slot, true)
end
end
Paint(state)
end
local function ButtonChanged()
local state = byButton[this]
if state then Sync(state) end
end
local function AllChanged()
for _, state in ipairs(states) do Sync(state) end
end
local listener = CreateFrame("Frame")
listener:RegisterEvent("ACTION_RANGE_CHECK_UPDATE")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
listener:RegisterEvent("PLAYER_TARGET_CHANGED")
listener:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
listener:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
listener:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
listener:SetScript("OnEvent", function()
if event == "ACTION_RANGE_CHECK_UPDATE" then
ranges[arg1] = arg3 and NormalizeRange(arg2) or nil
for _, state in ipairs(states) do if state.slot == arg1 then Paint(state) end end
else
-- Unchanged native results do not fire again: retain their cached state.
AllChanged()
end
end)
for _, prefix in ipairs({"ActionButton", "BonusActionButton", "MultiBarBottomLeftButton",
"MultiBarBottomRightButton", "MultiBarLeftButton", "MultiBarRightButton"}) do
for i = 1, 12 do
local button = _G[prefix .. i]
if button then
local state = {button = button, icon = _G[prefix .. i .. "Icon"]}
byButton[button] = state
table.insert(states, state)
ShaguTweaks.HookScript(button, "OnShow", ButtonChanged)
ShaguTweaks.HookScript(button, "OnHide", ButtonChanged)
end
end
end
hooksecurefunc("ActionButton_Update", ButtonChanged)
hooksecurefunc("ActionButton_UpdateUsable", ButtonChanged)
AllChanged()
return true
end end
module.enable = function(self) module.enable = function(self)
if EnableNativeRange() then return end
hooksecurefunc("ActionButton_OnUpdate", function(elapsed) hooksecurefunc("ActionButton_OnUpdate", function(elapsed)
-- Blizzard sets rangeTimer only for actions that have a range. Use our
-- own countdown instead of sharing it: decrementing rangeTimer here and
-- testing "<= 0.2" ran this check on every frame for every ranged
-- button, and kept Blizzard's own range check from ever firing.
if this.rangeTimer then if this.rangeTimer then
this.rangeTimer = this.rangeTimer - elapsed local timer = (this.shaguRangeTimer or 0) - (elapsed or 0)
if this.rangeTimer <= 0.2 then if timer > 0 then
this.shaguRangeTimer = timer
else
this.shaguRangeTimer = RANGE_INTERVAL
local action = ActionButton_GetPagedID(this) local action = ActionButton_GetPagedID(this)
local inRange = GetActionRange(action) local inRange = GetActionRange(action)
local icon = _G[this:GetName() .. "Icon"] -- Look the icon up once instead of building its name each time.
local icon = this.shaguRangeIcon
if not icon then
icon = _G[this:GetName() .. "Icon"]
this.shaguRangeIcon = icon
end
if icon then if icon then
if inRange == 0 then if inRange == 0 then
@@ -51,12 +165,19 @@ module.enable = function(self)
this.r, this.g, this.b, this.a = 0.85, 0.18, 0.18, 1 this.r, this.g, this.b, this.a = 0.85, 0.18, 0.18, 1
end end
icon:SetVertexColor(this.r, this.g, this.b, this.a) icon:SetVertexColor(this.r, this.g, this.b, this.a)
elseif IsUsableAction(action) then else
icon:SetVertexColor(1, 1, 1, 1) -- Restore the normal usability tint even if the old
-- red action is now unusable or short of mana.
local usable, noMana = IsUsableAction(action)
if usable and usable ~= 0 then
icon:SetVertexColor(1, 1, 1, 1)
elseif noMana and noMana ~= 0 then
icon:SetVertexColor(0.5, 0.5, 1, 1)
else
icon:SetVertexColor(0.4, 0.4, 0.4, 1)
end
end end
end end
this.rangeTimer = TOOLTIP_UPDATE_TIME
end end
end end
end, true) end, true)
+167
View File
@@ -0,0 +1,167 @@
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)
if self.counter then return end
local reagent_slots = { }
local reagent_counts = { }
local macro_slots = { }
local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight" }
local reagentcounter = CreateFrame("Frame", "ShaguTweaksReagentCount", UIParent)
self.counter = reagentcounter
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("SPELLS_CHANGED")
reagentcounter:RegisterEvent("BAG_UPDATE")
local macroEvents = { "MODIFIER_STATE_CHANGED", "PLAYER_TARGET_CHANGED",
"UPDATE_MOUSEOVER_UNIT", "PLAYER_REGEN_DISABLED", "PLAYER_REGEN_ENABLED",
"UPDATE_SHAPESHIFT_FORM", "UPDATE_SHAPESHIFT_FORMS", "SPELL_UPDATE_COOLDOWN",
"UNIT_AURA", "UNIT_DISPLAYPOWER" }
local macroEventSet = {}
local watchingMacros = false
local function WatchMacros()
local needed = next(macro_slots) ~= nil
if needed == watchingMacros then return end
watchingMacros = needed
for _, name in ipairs(macroEvents) do
if needed and API.eventutils and _G.C_EventUtils.IsEventValid(name) then
macroEventSet[name] = true
if name == "UNIT_AURA" or name == "UNIT_DISPLAYPOWER" then
API.RegisterUnitEvent(reagentcounter, name, "player")
else reagentcounter:RegisterEvent(name) end
elseif not needed then
reagentcounter:UnregisterEvent(name)
macroEventSet[name] = nil
end
end
end
-- 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()
if (event == "UNIT_AURA" or event == "UNIT_DISPLAYPOWER") and arg1 ~= "player" then return end
this.pending = true
if macroEventSet[event] then
this.macros = true
elseif 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
for slot = 1, 120 do
reagentcounter.ScanSlot(slot)
end
WatchMacros()
elseif this.macros then
-- A native macro can resolve to another spell without being edited.
-- Re-evaluate only known macro actions, not all 120 action slots.
for slot in pairs(macro_slots) do reagentcounter.ScanSlot(slot) end
WatchMacros()
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] = nil end
for _, itemID in pairs(reagent_slots) do
if reagent_counts[itemID] == nil then
reagent_counts[itemID] = API.GetItemCount(itemID, false, false) or 0
end
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.macros = nil
this:Hide()
end)
reagentcounter.ScanSlot = function(slot)
reagent_slots[slot] = nil
macro_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
macro_slots[slot] = actionID
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
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
+17
View File
@@ -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
-121
View File
@@ -1,121 +0,0 @@
-- Adapted from AoELoot by Sandrea / ChatGPT for ShaguTweaks-ClassicAPI.
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["AoE Loot"],
description = T["Automatically loots all nearby corpses using ClassicAPI without confirmation prompts. Leaves Master Loot untouched."],
expansions = { ["vanilla"] = true },
category = T["Loot"],
enabled = nil,
})
module.enable = function(self)
-- AoE Loot relies entirely on ClassicAPI's native corpse walker,
-- container classification and timer. Do not keep partial legacy behavior
-- when those capabilities are unavailable.
if not API or not API.aoeloot or not API.containeropenable or not API.timer then return end
local frame = CreateFrame("Frame", "ShaguTweaksAoELoot")
local pending = false
local containerLootDeadline = 0
local takeoverGeneration = 0
local TAKEOVER_DELAY = 0.3
local function CancelTakeover()
takeoverGeneration = takeoverGeneration + 1
pending = false
end
local function TrackContainerUse(bag, slot)
local isOpenable, canOpen = API.IsContainerItemOpenable(bag, slot)
if isOpenable and canOpen then
-- LOOT_OPENED is also fired by clams, lockboxes and similar bag items.
-- Mark that session before UseContainerItem runs so it remains under the
-- normal client handling and the consumed container can leave the bag.
containerLootDeadline = GetTime() + 3
end
end
-- Use the ShaguTweaks hook explicitly because its prepend mode records the
-- bag item before UseContainerItem can synchronously fire LOOT_OPENED.
ShaguTweaks.hooksecurefunc("UseContainerItem", TrackContainerUse, true)
local function IsMasterLootActive()
if not GetLootMethod then return false end
return GetLootMethod() == "master"
end
local function CanStartAoELoot()
if IsMasterLootActive() then return false end
if API.IsLootScanInProgress() then return false end
return true
end
local function HasNearbyLootableCorpse()
local units = API.GetNearbyLootableUnits()
return type(units) == "table" and table.getn(units) > 0
end
local function StartAoELoot(generation)
if generation ~= takeoverGeneration or not pending then return end
pending = false
if CanStartAoELoot() then
API.LootAllCorpses()
end
end
local function TakeOverLootSession()
-- AoE Loot owns corpse sessions while enabled. Close the normal client
-- window immediately so Vanilla autoloot, SuperAPI SetAutoloot modes and
-- quickloot-patched clients cannot keep control of the session.
takeoverGeneration = takeoverGeneration + 1
local generation = takeoverGeneration
pending = true
CloseLoot()
-- Native autoloot may already have emitted loot packets before LOOT_OPENED
-- reaches Lua. ClassicAPI's own loot test uses the same 0.3s settling
-- window between loot operations; after it expires the native corpse walker
-- becomes the sole owner of all remaining nearby corpse loot.
C_Timer.After(TAKEOVER_DELAY, function()
StartAoELoot(generation)
end)
end
frame:RegisterEvent("LOOT_OPENED")
frame:SetScript("OnEvent", function()
if event ~= "LOOT_OPENED" then return end
-- Inventory containers use the same loot event as corpses. Let the normal
-- client finish them instead of treating them as an AoE-loot trigger.
if containerLootDeadline > 0 then
local isContainerLoot = GetTime() <= containerLootDeadline
containerLootDeadline = 0
if isContainerLoot then
CancelTakeover()
return
end
end
-- The master looter must keep the normal window to inspect and assign loot.
if IsMasterLootActive() then
CancelTakeover()
return
end
-- Chests, fishing nodes and other non-corpse sources also emit LOOT_OPENED.
-- Only take over when ClassicAPI can see at least one nearby lootable unit.
if not HasNearbyLootableCorpse() then return end
-- A queued takeover already owns this interaction. ClassicAPI suppresses
-- its own LOOT_OPENED/LOOT_CLOSED events while walking corpses, so no
-- additional session needs to be stacked here.
if pending then return end
TakeOverLootSession()
end)
end
-121
View File
@@ -1,121 +0,0 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Auto Dismount"],
description = T["Automatically dismounts whenever a spell is casted."],
expansions = { ["vanilla"] = true },
category = T["Action Bar"],
enabled = true,
})
module.enable = function(self)
local dismount = CreateFrame("Frame")
ShaguTweaks.dismount = dismount
-- Keep the legacy detection data as a compatibility fallback for custom
-- mounts/forms that ClassicAPI does not identify as one of the known forms.
dismount.strings = {
-- deDE
"^Erhöht Tempo um (.+)%%",
-- enUS
"^Increases speed by (.+)%%",
-- esES
"^Aumenta la velocidad en un (.+)%%",
-- frFR
"^Augmente la vitesse de (.+)%%",
-- ruRU
"^Скорость увеличена на (.+)%%",
-- koKR
"^이동 속도 (.+)%%만큼 증가",
-- zhCN
"^速度提高(.+)%%",
-- turtle-wow
"speed based on", "Slow and steady...", "Riding",
"Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.",
"根据您的骑行技能提高速度。", "根据骑术技能提高速度。", "又慢又稳......",
}
-- shapeshift buff icons (legacy/custom fallback)
dismount.shapeshifts = {
"ability_racial_bearform", "ability_druid_catform", "ability_druid_travelform",
"spell_nature_forceofnature", "ability_druid_aquaticform", "spell_nature_spiritwolf"
}
-- ClassicAPI SpellShapeshiftForm.dbc IDs matching the forms the original
-- module intentionally cancelled. Do not include Moonkin/Shadowform/Stealth.
local removableForms = {
[1] = true, -- Cat
[2] = true, -- Tree (vanilla DBC)
[3] = true, -- Travel
[4] = true, -- Aquatic
[5] = true, -- Bear
[8] = true, -- Dire Bear
[9] = true, -- Tree of Life (Turtle WoW)
[11] = true, -- Swift Travel (Turtle WoW)
[16] = true, -- Ghost Wolf
}
-- errors that indicate mount/shapeshift
dismount.errors = { SPELL_FAILED_NOT_MOUNTED, ERR_ATTACK_MOUNTED, ERR_TAXIPLAYERALREADYMOUNTED,
SPELL_FAILED_NOT_SHAPESHIFT, SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED, SPELL_NOT_SHAPESHIFTED,
SPELL_NOT_SHAPESHIFTED_NOSPACE, ERR_CANT_INTERACT_SHAPESHIFTED, ERR_NOT_WHILE_SHAPESHIFTED,
ERR_NO_ITEMS_WHILE_SHAPESHIFTED, ERR_TAXIPLAYERSHAPESHIFTED,ERR_MOUNT_SHAPESHIFTED }
dismount.scanner = ShaguTweaks.libtipscan:GetScanner("dismount")
local function LegacyCancel()
for i=0, 31 do
-- detect mounts based on tooltip text
dismount.scanner:SetPlayerBuff(i)
for _, str in pairs(dismount.strings) do
if dismount.scanner:Find(str) then
CancelPlayerBuff(i)
return true
end
end
-- detect shapeshift based on texture
local buff = GetPlayerBuffTexture(i)
if buff then
for _, bufftype in pairs(dismount.shapeshifts) do
if string.find(string.lower(buff), bufftype) then
CancelPlayerBuff(i)
return true
end
end
end
end
end
dismount:RegisterEvent("UI_ERROR_MESSAGE")
dismount:SetScript("OnEvent", function()
-- stand up
if arg1 == SPELL_FAILED_NOT_STANDING then
SitOrStand()
return
end
for _, errorstring in pairs(dismount.errors) do
if arg1 == errorstring then
-- ClassicAPI reads the real mounted/form state directly, avoiding a
-- 32-buff tooltip scan in the normal case.
if API.IsMounted() then
API.Dismount()
return
end
local formID = API.GetShapeshiftFormID()
if removableForms[formID] then
API.CancelShapeshiftForm()
return
end
-- Preserve compatibility with custom private-server auras/forms.
LegacyCancel()
return
end
end
end)
end
+1 -1
View File
@@ -3,7 +3,7 @@ local cmatch = ShaguTweaks.cmatch
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Auto Stance"], title = T["Auto Stance"],
description = T["Automatically switch to the required warrior or druid stance on spell cast."], description = T["Automatically switch to the warrior stance or druid form required by a spell. To leave a form for spells such as Moonfire, enable ClassicAPI Auto Cancel Form."],
expansions = { ["vanilla"] = true }, expansions = { ["vanilla"] = true },
category = T["Action Bar"], category = T["Action Bar"],
enabled = true, enabled = true,
+669
View File
@@ -0,0 +1,669 @@
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
-- Let this invocation's predecessor buy one without opening StackSplit.
return false, 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 installed, dispatching
local function DispatchClick(original, button, ignoreModifiers)
local handled, single = Buy:HandleMerchantClick(this, button, ignoreModifiers)
if handled then return end
return original(single and "LeftButton" or button, single and 1 or ignoreModifiers)
end
local function InstallHook()
local current = _G.MerchantItemButton_OnClick
if type(current) ~= "function" then return end
if current == installed then return end
-- A later addon may wrap our previous hook. Each installation must retain
-- its own predecessor; a mutable shared fallback would form a cycle.
local original = current
installed = function(button, ignoreModifiers)
if dispatching then return original(button, ignoreModifiers) end
dispatching = true
local ok, r1, r2, r3, r4 = pcall(DispatchClick, original, button, ignoreModifiers)
dispatching = nil
if not ok then error(r1) end
return r1, r2, r3, r4
end
_G.MerchantItemButton_OnClick = installed
end
InstallHook()
local frame = CreateFrame("Frame", nil, UIParent)
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("MERCHANT_SHOW")
frame:SetScript("OnEvent", InstallHook)
end
+141
View File
@@ -0,0 +1,141 @@
-- Movable Cast Bar
--
-- Hold Ctrl+Shift+Alt to show a marker over the player's cast bar and drag
-- it anywhere; right-click the marker to put it back. The position is saved
-- per character. The Swing Timer, Wand Timer and the Improved Castbar icon
-- are anchored to the cast bar, so they follow it.
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local POSITION_KEY = "CastBarPosition"
-- One table for state: Lua 5.0 allows only 32 upvalues per function.
local S = { preview = false }
local ModifiersDown = ShaguTweaks.API.AreMoveModifiersDown
local function SavedPosition()
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
if pos and pos[1] and pos[2] then return pos end
end
-- Put the bar at the saved spot. Goes through the original SetPoint because
-- the wrapper below ignores everybody else while a saved spot exists.
local function Apply()
local pos = SavedPosition()
if not pos then return end
CastingBarFrame:ClearAllPoints()
S.orig(CastingBarFrame, "BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
end
-- Back to the stock spot; Reduced Actionbar re-anchors it on the next layout pass.
local function Reset()
if ShaguTweaks_config then ShaguTweaks_config[POSITION_KEY] = nil end
CastingBarFrame:ClearAllPoints()
S.orig(CastingBarFrame, "BOTTOM", UIParent, "BOTTOM", 0, 60)
if _G.UIParent_ManageFramePositions then _G.UIParent_ManageFramePositions() end
end
local function SetPreview(on)
if S.preview == on then return end
S.preview = on
if on then
local tint = ShaguTweaks.UITint
S.mover.border:SetVertexColor(tint and tint.r or 1, tint and tint.g or 1,
tint and tint.b or 1, tint and tint.a or 1)
S.mover:Show()
else
CastingBarFrame:StopMovingOrSizing()
S.mover:Hide()
end
end
-- Same look as the Swing Timer preview bars: the classic cast bar with its
-- border and spark, full, in the cast bar's gold.
local function CreateMover()
local m = CreateFrame("StatusBar", "ShaguTweaksCastBarMover", UIParent)
m:SetFrameStrata("DIALOG")
m:SetAllPoints(CastingBarFrame)
m:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
m:SetStatusBarColor(1, .7, 0)
m:SetMinMaxValues(0, 1)
m:SetValue(1)
m:EnableMouse(true)
m:Hide()
m.bg = m:CreateTexture(nil, "BACKGROUND")
m.bg:SetAllPoints(m)
m.bg:SetTexture(0, 0, 0, .5)
-- Keep the rim above the StatusBar's ARTWORK fill during preview/dragging.
m.border = m:CreateTexture(nil, "OVERLAY")
m.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border")
m.border:SetWidth(256)
m.border:SetHeight(64)
m.border:SetPoint("TOP", m, "TOP", 0, 28)
m.spark = m:CreateTexture(nil, "OVERLAY")
m.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
m.spark:SetBlendMode("ADD")
m.spark:SetWidth(32)
m.spark:SetHeight(32)
m.spark:SetPoint("CENTER", m, "RIGHT", 0, 2)
m.text = m:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
m.text:SetPoint("CENTER", m, "CENTER", 0, 3)
m.text:SetText(T["Cast Bar"])
m:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then
-- the swing bars default to sitting on top of the cast bar: pin them
-- first so only the cast bar moves
if ShaguTweaks.FreezeSwingBars then ShaguTweaks.FreezeSwingBars() end
CastingBarFrame:StartMoving()
end
end)
m:SetScript("OnMouseUp", function()
CastingBarFrame:StopMovingOrSizing()
ShaguTweaks_config = ShaguTweaks_config or {}
if arg1 == "RightButton" then
Reset()
else
ShaguTweaks_config[POSITION_KEY] = { CastingBarFrame:GetLeft(), CastingBarFrame:GetBottom() }
Apply()
end
end)
S.mover = m
end
ShaguTweaks.EnableCastBarMover = function()
if not CastingBarFrame then return end
if not S.mover then
S.orig = CastingBarFrame.SetPoint
CreateMover()
CastingBarFrame:SetMovable(true)
CastingBarFrame:SetClampedToScreen(true)
S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
SetPreview(ModifiersDown())
else
Apply()
end
end)
end
-- Reduced Actionbar and others re-anchor the bar on every layout pass.
-- While a saved spot exists their SetPoint calls are ignored.
CastingBarFrame.SetPoint = function(frame, a, b, c, d, e)
if SavedPosition() then return end
return S.orig(frame, a, b, c, d, e)
end
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
Apply()
end
+68
View File
@@ -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
+45
View File
@@ -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
+92
View File
@@ -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
+17 -22
View File
@@ -16,11 +16,11 @@ local COOLDOWN = 70
-- how often (in seconds) stale entries may be pruned when chat traffic arrives -- how often (in seconds) stale entries may be pruned when chat traffic arrives
local CLEAN_INTERVAL = 30 local CLEAN_INTERVAL = 30
-- double-buffered cache of [frame] = {message = last_seen_time}, keyed per -- Cache of [frame] = {message = last_seen_time}, keyed per
-- chat frame so that a message shown in one tab (e.g. "General") doesn't -- chat frame so that a message shown in one tab (e.g. "General") doesn't
-- get treated as a duplicate when it also needs to show in another tab -- get treated as a duplicate when it also needs to show in another tab
-- (e.g. a custom "World" tab) -- (e.g. a custom "World" tab)
local cache = { {}, {}, INDEX = 1 } local cache = {}
local last_cleanup = 0 local last_cleanup = 0
local canFilterEmotes = API and API.guildmembership and API.friendmembership local canFilterEmotes = API and API.guildmembership and API.friendmembership
@@ -37,21 +37,12 @@ local function PruneCache(now)
if now - last_cleanup < CLEAN_INTERVAL then return end if now - last_cleanup < CLEAN_INTERVAL then return end
last_cleanup = now last_cleanup = now
local index = cache.INDEX for frame, frameCache in pairs(cache) do
local newindex = (index == 1) and 2 or 1
cache[newindex] = {}
for frame, frameCache in pairs(cache[index]) do
for msg, seen_at in pairs(frameCache) do for msg, seen_at in pairs(frameCache) do
if (seen_at + COOLDOWN) > now then if (seen_at + COOLDOWN) <= now then frameCache[msg] = nil end
cache[newindex][frame] = cache[newindex][frame] or {}
cache[newindex][frame][msg] = seen_at
end
end end
if not next(frameCache) then cache[frame] = nil end
end end
cache[index] = {}
cache.INDEX = newindex
end end
-- returns true (and records the message) if this exact text was already seen -- returns true (and records the message) if this exact text was already seen
@@ -60,11 +51,10 @@ local function IsDuplicate(frame, msg)
local now = GetTime() local now = GetTime()
PruneCache(now) PruneCache(now)
local index = cache.INDEX local frameCache = cache[frame]
local frameCache = cache[index][frame]
if not frameCache then if not frameCache then
frameCache = {} frameCache = {}
cache[index][frame] = frameCache cache[frame] = frameCache
end end
local seen_at = frameCache[msg] local seen_at = frameCache[msg]
@@ -91,24 +81,29 @@ module.enable = function(self)
local Original_ChatFrame_OnEvent = ChatFrame_OnEvent local Original_ChatFrame_OnEvent = ChatFrame_OnEvent
ChatFrame_OnEvent = function(event) ChatFrame_OnEvent = function(event)
local message, sender = arg1, arg2
local chatEvent, chatMessage, chatSender = API.GetCurrentChatEvent()
if chatEvent then
event, message, sender = chatEvent, chatMessage, chatSender
end
-- suppress "#showtooltip" macro errors leaking into chat -- suppress "#showtooltip" macro errors leaking into chat
if strfind(arg1 or "", "^#showtooltip") then if strfind(message or "", "^#showtooltip") then
return return
end end
-- suppress BigWigs "Casted X on Y" cast announcements -- suppress BigWigs "Casted X on Y" cast announcements
if event == "CHAT_MSG_SAY" and strfind(arg1 or "", "^Casted %u[%a%s]+ on %u[%a%s]+") then if event == "CHAT_MSG_SAY" and strfind(message or "", "^Casted %u[%a%s]+ on %u[%a%s]+") then
return return
end end
-- hide repeated messages in say/yell/channel chat, and emotes from -- hide repeated messages in say/yell/channel chat, and emotes from
-- strangers (guildmates/friends are never filtered on emote) -- strangers (guildmates/friends are never filtered on emote)
if arg1 and arg2 and arg2 ~= GetPlayerName("player") then if message and sender and sender ~= GetPlayerName("player") then
local isChatType = event == "CHAT_MSG_SAY" or event == "CHAT_MSG_CHANNEL" or event == "CHAT_MSG_YELL" local isChatType = event == "CHAT_MSG_SAY" or event == "CHAT_MSG_CHANNEL" or event == "CHAT_MSG_YELL"
local isStrangerEmote = canFilterEmotes and event == "CHAT_MSG_EMOTE" local isStrangerEmote = canFilterEmotes and event == "CHAT_MSG_EMOTE"
and not (IsGuildMate(arg2) or IsFriendOf(arg2)) and not (IsGuildMate(sender) or IsFriendOf(sender))
if (isChatType or isStrangerEmote) and IsDuplicate(this, arg1) then if (isChatType or isStrangerEmote) and IsDuplicate(this, message) then
return return
end end
end end
+19
View File
@@ -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
+57
View File
@@ -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
+127 -6
View File
@@ -18,6 +18,90 @@ local module = ShaguTweaks:register({
-- by one complete clock cycle. -- by one complete clock cycle.
local TIMER_WRAP_SECONDS = (2 ^ 32) / 1000 local TIMER_WRAP_SECONDS = (2 ^ 32) / 1000
local formatter
local nativeCooldowns = setmetatable({}, { __mode = "k" })
local cvarWatcher
local SetCooldown
local function FontSize(parent)
local size = parent:GetHeight() or 0
size = size > 0 and size * .64 or 12
return size > 14 and 14 or size
end
local function NativeNumbersEnabled()
return type(_G.GetCVar) == "function" and _G.GetCVar("countdownForCooldowns") == "1"
end
local function CanUseNative(cooldown)
return type(cooldown.GetCountdownFontString) == "function"
and type(cooldown.SetCountdownFormatter) == "function"
and type(cooldown.SetMinimumCountdownDuration) == "function"
and type(cooldown.SetHideCountdownNumbers) == "function"
and _G.C_StringUtil and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function"
and _G.Enum and _G.Enum.NumericRuleFormatRounding
end
local function NativeTarget(cooldown, parent)
if CanUseNative(cooldown) then return cooldown end
if cooldown.stCountdownNativeFrame ~= nil then return cooldown.stCountdownNativeFrame or nil end
if not ShaguTweaks.API.nativecooldowns or not _G.C_StringUtil
or type(_G.C_StringUtil.CreateNumericRuleFormatter) ~= "function" then return end
-- Stock Vanilla indicators can still be Models. Keep their sweep untouched
-- and use an engine-driven child solely for numbers, as the old overlay did.
local ok, child = pcall(CreateFrame, "Cooldown", nil, cooldown)
if not ok or not child then
cooldown.stCountdownNativeFrame = false
return
end
if not CanUseNative(child) then
child:Hide()
cooldown.stCountdownNativeFrame = false
return
end
child.stCountdownHelper = true
child:SetAllPoints(cooldown)
child:SetFrameLevel(parent:GetFrameLevel() + 1)
child:SetDrawSwipe(false)
child:SetDrawEdge(false)
child:SetDrawBling(false)
cooldown.stCountdownNativeFrame = child
return child
end
local function GetFormatter()
if formatter then return formatter end
local up = _G.Enum.NumericRuleFormatRounding.Up
formatter = _G.C_StringUtil.CreateNumericRuleFormatter()
-- Match TimeConvert, including its colors before rounding at 5/10 seconds.
formatter:SetBreakpoints({
{ threshold = 0, step = 1, rounding = up, format = "|cffff5555%d" },
{ threshold = 5, step = 1, rounding = up, format = "|cffffff55%d" },
{ threshold = 10, step = 1, rounding = up, format = "|cffffffff%d" },
{ threshold = 60, format = "|cffffffff%dm", components = { { div = 60, step = 1, rounding = up } } },
{ threshold = 3600, format = "|cffffffff%dh", components = { { div = 3600, step = 1, rounding = up } } },
{ threshold = 86400, format = "|cffffffff%dd", components = { { div = 86400, step = 1, rounding = up } } },
})
return formatter
end
local function RememberNative(cooldown, start, duration, enable)
nativeCooldowns[cooldown] = true
cooldown.stCountdownStart, cooldown.stCountdownDuration, cooldown.stCountdownEnable = start, duration, enable
if cvarWatcher then return end
cvarWatcher = CreateFrame("Frame")
cvarWatcher.enabled = NativeNumbersEnabled()
cvarWatcher:RegisterEvent("CVAR_UPDATE")
cvarWatcher:SetScript("OnEvent", function()
local enabled = NativeNumbersEnabled()
if enabled == cvarWatcher.enabled then return end
cvarWatcher.enabled = enabled
for frame in pairs(nativeCooldowns) do
SetCooldown(frame, frame.stCountdownStart, frame.stCountdownDuration, frame.stCountdownEnable)
end
end)
end
local function CooldownOnUpdate() local function CooldownOnUpdate()
-- hide frames without parent -- hide frames without parent
local parent = this:GetParent() local parent = this:GetParent()
@@ -73,9 +157,7 @@ local function CreateCoolDown(cooldown)
cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString(nil, "OVERLAY") cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString(nil, "OVERLAY")
-- detect dynamic font size -- detect dynamic font size
local size = parent:GetHeight() or 0 local size = FontSize(parent)
size = size > 0 and size * .64 or 12
size = size > 14 and 14 or size
-- set fonts -- set fonts
cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE") cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE")
@@ -83,16 +165,55 @@ local function CreateCoolDown(cooldown)
cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate) cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate)
end end
local function SetCooldown(this, start, duration, enable) SetCooldown = function(this, start, duration, enable)
if this.stCountdownHelper then return end
-- Let OmniCC and ShaguPlates own their cooldown text. Also hide an overlay -- Let OmniCC and ShaguPlates own their cooldown text. Also hide an overlay
-- that may have been created before either compatibility flag was applied. -- that may have been created before either compatibility flag was applied.
if this.noCooldownCount or this.pfCooldownType then if this.noCooldownCount or this.pfCooldownType or this.readable then
if this.cooldowntext then if this.cooldowntext then
this.cooldowntext:Hide() this.cooldowntext:Hide()
end end
if this.stCountdownNativeFrame then this.stCountdownNativeFrame:Hide() end
if (this.stNativeCountdown or this.noCooldownCount)
and type(this.SetHideCountdownNumbers) == "function" then
this:SetHideCountdownNumbers(true)
end
return return
end end
local parent = this:GetParent()
if parent and (CanUseNative(this) or (ShaguTweaks.API.nativecooldowns
and _G.C_StringUtil and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function")) then
RememberNative(this, start, duration, enable)
if NativeNumbersEnabled() then
local target = NativeTarget(this, parent)
local text = target and target:GetCountdownFontString()
if text then
if this.cooldowntext then this.cooldowntext:Hide() end
if target == this then this.stNativeCountdown = true end
target:SetCountdownFormatter(GetFormatter())
-- Native threshold is strictly greater-than; retain duration >= 2s.
target:SetMinimumCountdownDuration(1999)
local active = start and start > 0 and duration
and duration >= 2 and (not enable or enable > 0)
target:SetHideCountdownNumbers(not active)
if target ~= this then
if active then target:SetCooldown(start, duration) else target:Hide() end
end
text:SetFont(STANDARD_TEXT_FONT, FontSize(parent), "OUTLINE")
text:ClearAllPoints()
text:SetPoint("CENTER", target, "CENTER", 0, 0)
text:SetTextColor(1, 1, 1, 1)
return
end
end
end
-- Never switch on the global CVar: it also controls unrelated addons.
-- Older builds and CVar-off clients keep the Lua renderer.
if this.stCountdownNativeFrame then this.stCountdownNativeFrame:Hide() end
if type(this.SetHideCountdownNumbers) == "function" then this:SetHideCountdownNumbers(true) end
-- don't draw global cooldowns -- don't draw global cooldowns
if not duration or duration < 2 then if not duration or duration < 2 then
-- hide if already existing -- hide if already existing
@@ -108,7 +229,7 @@ local function SetCooldown(this, start, duration, enable)
end end
if this.cooldowntext then if this.cooldowntext then
if start > 0 and duration > 0 and (not enable or enable > 0) then if start and start > 0 and duration > 0 and (not enable or enable > 0) then
this.cooldowntext.elapsed = 0 this.cooldowntext.elapsed = 0
this.cooldowntext:Show() this.cooldowntext:Show()
else else
+99
View File
@@ -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
+816 -36
View File
@@ -15,6 +15,26 @@ local module = ShaguTweaks:register({
} }
}) })
-- Golden UI runs the same recolor pass with a gold palette (like the WoW
-- Forever client): textures are tinted gold, borders and tooltip edges get a
-- softer gold, and backgrounds keep their own colors.
local golden = ShaguTweaks:register({
title = T["Golden UI"],
description = T["Turns the borders of the interface, tooltips and dialogs gold, like the WoW Forever client. Darkened UI takes priority when both are enabled."],
expansions = { ["vanilla"] = true },
category = T["Interface"],
enabled = nil,
config = {
["goldmode.color"] = { r = 1, g = .78, b = .4, a = 1 },
["goldmode.border"] = { r = .8, g = .62, b = .32, a = 1 },
["goldmode.bar"] = { r = .8, g = .63, b = .62, a = 1 },
}
})
-- Active palette, set by whichever of the two modules is enabled:
-- tint goes on textures, edge on backdrop borders and added button borders.
local tint, edge
local blacklist = { local blacklist = {
["Solid Texture"] = true, ["Solid Texture"] = true,
["WHITE8X8"] = true, ["WHITE8X8"] = true,
@@ -53,11 +73,7 @@ local regionskips = {
} }
local backgrounds = { local backgrounds = {
["^SpellBookFrame$"] = { 325, 355, 17, -74 },
["^ItemTextFrame$"] = { 300, 355, 24, -74 }, ["^ItemTextFrame$"] = { 300, 355, 24, -74 },
["^QuestLogDetailScrollFrame$"] = { QuestLogDetailScrollChildFrame:GetWidth(), QuestLogDetailScrollChildFrame:GetHeight(), 0, 0 },
["^QuestFrame(.+)Panel$"] = { 300, 330, 24, -82 },
["^GossipFrameGreetingPanel$"] = { 300, 330, 24, -82 },
} }
local borders = { local borders = {
@@ -81,6 +97,9 @@ local borders = {
["ChatFrame(.+)Button"] = -2, ["ChatFrame(.+)Button"] = -2,
["PetFrameHappiness"] = 2, ["PetFrameHappiness"] = 2,
["MicroButton"] = { -21, 0, 0, 0 }, ["MicroButton"] = { -21, 0, 0, 0 },
-- VCB's buff and weapon-buff buttons, like the Blizzard buff buttons
["^VCB_BF_BUFF_BUTTON%d+$"] = 3,
["^VCB_BF_WEAPON_BUTTON%d+$"] = 3,
} }
local addonframes = { local addonframes = {
@@ -97,6 +116,7 @@ local addonframes = {
["Blizzard_RaidUI"] = { "ReadyCheckFrame" }, ["Blizzard_RaidUI"] = { "ReadyCheckFrame" },
["Blizzard_TradeSkillUI"] = { "TradeSkillFrame" }, ["Blizzard_TradeSkillUI"] = { "TradeSkillFrame" },
["Blizzard_TrainerUI"] = { "ClassTrainerFrame" }, ["Blizzard_TrainerUI"] = { "ClassTrainerFrame" },
["Artisan"] = { "ArtisanFrame" },
} }
-- Turtle WoW exposes these extra inspect pages. Keep them out of the generic -- Turtle WoW exposes these extra inspect pages. Keep them out of the generic
@@ -117,21 +137,69 @@ if GetExpansion() == "tbc" then
borders["MicroButton"] = { -21, 0, 0, 0 } borders["MicroButton"] = { -21, 0, 0, 0 }
end end
local function IsBlacklisted(texture) -- Frame art that ships inside an addon (the "AddOns" entry above skips
local name = texture:GetName() -- addon textures, which are mostly icons) but should follow the palette.
local texture = texture:GetTexture() local whitelist = {
if not texture then return true end "outfitter\\textures\\outfitterframe",
"outfitter\\textures\\scrollbartrench",
-- Artisan swaps in its own lower half of the profession window
"artisan\\textures\\bottomleft",
"artisan\\textures\\bottomright",
-- RollFor's classic loot window
"rollfor\\assets\\og-loot-frame",
}
-- Addon windows that build their rows while opening: recolored every time
-- they are shown (and once more on the next frame, after the rows exist).
local showframes = {
["RollForLootFrameHeader"] = true,
}
local PATH_CACHE_LIMIT = 256
local pathCache, pathKeys, pathCursor = {}, {}, 0
local function ClassifyPath(path)
local result = pathCache[path]
local capture = ShaguTweaks.RecolorCapture
if capture then
if result then capture.cacheHits = capture.cacheHits + 1
else capture.cacheMisses = capture.cacheMisses + 1 end
end
if result then return result end
local lower = string.lower(path)
result = 3 -- Candidate: name exclusions must still be evaluated per region.
if string.find(lower, "interface\\questframe\\ui-questlog-empty-", 1, true)
or string.find(lower, "interface\\stationery\\", 1, true)
or string.find(lower, "interface\\mailframe\\mailitemborder", 1, true) then
result = 2 -- Paper/insets always remain native.
else
for i = 1, table.getn(whitelist) do
if string.find(lower, whitelist[i], 1, true) then result = 1; break end
end
if result == 3 then
for entry in pairs(blacklist) do
if string.find(path, entry, 1) then result = 2; break end
end
end
end
pathCursor = math.mod(pathCursor, PATH_CACHE_LIMIT) + 1
local previous = pathKeys[pathCursor]
if previous then pathCache[previous] = nil end
pathKeys[pathCursor], pathCache[path] = path, result
return result
end
local function IsBlacklisted(texture)
local path = texture:GetTexture()
if type(path) ~= "string" then return true end
local name = texture:GetName()
if name and string.find(name, "StationeryBackground", 1, true) then return true end
local result = ClassifyPath(path)
if result == 1 then return nil end -- Explicit artwork whitelist wins over names.
if result == 2 then return true end
if name then if name then
for entry in pairs(blacklist) do for entry in pairs(blacklist) do
if string.find(name, entry, 1) then return true end if string.find(name, entry, 1) then return true end
end end
end end
for entry in pairs(blacklist) do
if string.find(texture, entry, 1) then return true end
end
return nil return nil
end end
@@ -140,50 +208,257 @@ local function AddSpecialBackground(frame, w, h, x, y)
frame.Material:SetTexture("Interface\\Stationery\\StationeryTest1") frame.Material:SetTexture("Interface\\Stationery\\StationeryTest1")
frame.Material:SetWidth(w) frame.Material:SetWidth(w)
frame.Material:SetHeight(h) frame.Material:SetHeight(h)
frame.Material:SetPoint("TOPLEFT", frame, x, y) frame.Material:SetPoint("TOPLEFT", frame, "TOPLEFT", x, y)
frame.Material:SetVertexColor(.8, .8, .8) frame.Material:SetVertexColor(.8, .8, .8)
end end
local function DarkenFrame(frame, r, g, b, a) local function IsSlotArtwork(path)
-- dont't do anything if disabled if type(path) ~= "string" then return end
if not ShaguTweaks.DarkMode then return end path = string.lower(path)
return string.find(path, "interface\\buttons\\ui-slot-background", 1, true)
or string.find(path, "bagshui\\images\\itemslot\\emptyslot", 1, true)
end
-- Leave the original artwork white. Four narrow copies color only its rim.
-- Anchor to the owning button (not a Texture region), so pooled Bagshui
-- buttons carry every strip with them when resized, moved or hidden.
local function TintSlotBorder(texture, button)
local path = texture:GetTexture()
local strips = texture.stSlotEdges
if not IsSlotArtwork(path) then
if strips then
for i = 1, 4 do strips[i]:Hide() end
end
return
end
if not strips then
strips = {}
texture.stSlotEdges = strips
for i = 1, 4 do
strips[i] = button:CreateTexture(nil, "BORDER")
strips[i].stSlotEdge = true
end
strips[1]:SetPoint("TOPLEFT", button, "TOPLEFT", -1, 1)
strips[1]:SetPoint("TOPRIGHT", button, "TOPRIGHT", 1, 1)
strips[1]:SetHeight(3)
strips[2]:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", -1, -1)
strips[2]:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, -1)
strips[2]:SetHeight(3)
strips[3]:SetPoint("TOPLEFT", button, "TOPLEFT", -1, -2)
strips[3]:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", -1, 2)
strips[3]:SetWidth(3)
strips[4]:SetPoint("TOPRIGHT", button, "TOPRIGHT", 1, -2)
strips[4]:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, 2)
strips[4]:SetWidth(3)
end
local left, top, bottomLeft, bottom, right = texture:GetTexCoord()
local w, h = button:GetWidth() + 2, button:GetHeight() + 2
if w <= 6 or h <= 6 then w, h = 39, 39 end
local dx, dy = (right - left) * 3 / w, (bottom - top) * 3 / h
texture:SetVertexColor(1, 1, 1, 1)
strips[1]:SetTexCoord(left, right, top, top + dy)
strips[2]:SetTexCoord(left, right, bottom - dy, bottom)
strips[3]:SetTexCoord(left, left + dx, top + dy, bottom - dy)
strips[4]:SetTexCoord(right - dx, right, top + dy, bottom - dy)
for i = 1, 4 do
strips[i]:SetTexture(path)
strips[i]:SetVertexColor(edge.r, edge.g, edge.b, 1)
strips[i]:SetAlpha(texture:GetAlpha())
strips[i]:Show()
end
end
-- TrinketMenu owns its styling; its future fork must not compete with this
-- palette, including the initial recursive pass over UIParent.
local function IsExcludedFrame(frame)
local name = frame and frame.GetName and frame:GetName()
return name and string.find(name, "TrinketMenu", 1, true) == 1
end
-- The Spellbook's metal frame and torn paper share four 256px-high tiles.
local spellbookPaper = {
topleft = {22, 74, 256, 256, 256},
topright = {0, 74, 85, 256, 128},
botleft = {22, 0, 256, 175, 256},
botright = {0, 0, 85, 175, 128},
}
local function RestoreSpellbookParchment(frame, source)
if source.stSpellbookPaper then return end
local path = source:GetTexture()
if type(path) ~= "string" then return end
local _, _, side = string.find(string.lower(path), "^interface\\spellbook\\ui%-spellbookpanel%-(%a+)$")
local crop = spellbookPaper[side]
if not crop then return end
local width, height = source:GetWidth(), source:GetHeight()
if not width or width <= 0 or not height or height <= 0 then return end
-- Keep the art below the paper and the page number above it.
source:SetDrawLayer("BORDER")
if _G.SpellBookPageText then _G.SpellBookPageText:SetDrawLayer("OVERLAY") end
local paper = frame:CreateTexture(nil, "ARTWORK")
paper.stOriginalQuestPaper = true
paper:SetTexture(path)
paper:SetVertexColor(1, 1, 1, 1)
local x1, y1, x2, y2, tileWidth = unpack(crop)
local sx, sy = width / tileWidth, height / 256
paper:SetTexCoord(x1 / tileWidth, x2 / tileWidth, y1 / 256, y2 / 256)
paper:SetWidth((x2 - x1) * sx)
paper:SetHeight((y2 - y1) * sy)
paper:SetPoint("TOPLEFT", source, "TOPLEFT", x1 * sx, -y1 * sy)
source.stSpellbookPaper = paper
end
local function RestoreQuestParchment(frame)
local name = frame.GetName and frame:GetName()
if not name or not (string.find(name, "^QuestFrame.*Panel$")
or name == "GossipFrameGreetingPanel") then return end
for _, source in pairs({frame:GetRegions()}) do
local path = source.GetTexture and source:GetTexture()
if type(path) == "string" and not source.stQuestPaper and not source.stOriginalQuestPaper then
local lower = string.lower(path)
local left = string.find(lower, "%-topleft$") or string.find(lower, "%-botleft$")
local right = string.find(lower, "%-topright$") or string.find(lower, "%-botright$")
local top = string.find(lower, "%-topleft$") or string.find(lower, "%-topright$")
if (left or right) and (string.find(lower, "quest", 1, true) or string.find(lower, "gossip", 1, true)) then
-- Native 256px tiles contain both the frame and torn parchment.
-- Copy only the paper area; leave the palette on the outer artwork.
local x1, x2 = left and 22 or 0, left and 256 or 67
-- Native paper starts below the header and includes the torn lower
-- edge through row 159. Stopping at 148 leaves a tinted bottom band.
local y1, y2 = top and 82 or 0, top and 256 or 160
-- Use a higher layer than both the native BACKGROUND tiles and
-- BORDER material textures. Equal-layer texture batching can draw
-- tinted native tiles over only some of our restored paper pieces.
local paper = frame:CreateTexture(nil, "ARTWORK")
paper.stOriginalQuestPaper = true
paper:SetTexture(path)
local tileWidth = left and 256 or 128
paper:SetTexCoord(x1/tileWidth, x2/tileWidth, y1/256, y2/256)
local sx, sy = source:GetWidth()/tileWidth, source:GetHeight()/256
paper:SetWidth((x2-x1)*sx)
paper:SetHeight((y2-y1)*sy)
paper:SetPoint("TOPLEFT", source, "TOPLEFT", x1*sx, -y1*sy)
paper:SetVertexColor(1, 1, 1, 1)
source.stQuestPaper = paper
end
end
end
end
local function RestoreQuestLogParchment(frame)
if not frame.GetName or frame:GetName() ~= "QuestLogFrame" then return end
for _, source in pairs({frame:GetRegions()}) do
local path = source.GetTexture and source:GetTexture()
if type(path) == "string" and not source.stQuestLogCrops and not source.stOriginalQuestPaper then
local lower = string.lower(path)
local crops, tileWidth
-- Turtle's two native tiles, verified against its shipped artwork.
-- Keep the list surface and both parchment halves; exclude metal rims.
if string.find(lower, "interface\\questframe\\ui-questlog-left", 1, true)
and string.find(lower, "ui%-questlog%-left$") then
crops, tileWidth = {{19,74,320,436},{348,74,512,456}}, 512
elseif string.find(lower, "interface\\questframe\\ui-questlog-right", 1, true)
and string.find(lower, "ui%-questlog%-right$") then
crops, tileWidth = {{0,74,136,456}}, 256
end
if crops then
local w,h = source:GetWidth(), source:GetHeight()
if w and h and w > 0 and h > 0 then
source.stQuestLogCrops = {}
for _, rect in ipairs(crops) do
local paper = frame:CreateTexture(nil, "OVERLAY")
paper.stOriginalQuestPaper = true
paper:SetTexture(path)
paper:SetTexCoord(rect[1]/tileWidth,rect[3]/tileWidth,rect[2]/512,rect[4]/512)
paper:SetWidth((rect[3]-rect[1])*w/tileWidth)
paper:SetHeight((rect[4]-rect[2])*h/512)
paper:SetPoint("TOPLEFT",source,"TOPLEFT",rect[1]*w/tileWidth,-rect[2]*h/512)
paper:SetVertexColor(1,1,1,1)
table.insert(source.stQuestLogCrops,paper)
end
end
end
end
end
end
local function DarkenFrame(frame, r, g, b, a, scan)
-- dont't do anything if neither Darkened UI nor Golden UI is enabled
if not tint then return end
if IsExcludedFrame(frame) then return end
local capture = ShaguTweaks.RecolorCapture
if capture and frame then
if not scan then
scan = capture.rootIndex[frame]
if not scan then
scan = { name = frame.GetName and frame:GetName() or tostring(frame), calls = 0, frames = 0, regions = 0, tables = 0 }
capture.rootIndex[frame] = scan
table.insert(capture.roots, scan)
end
scan.calls = scan.calls + 1
end
scan.frames = scan.frames + 1
capture.frameVisits = capture.frameVisits + 1
if capture.seen[frame] then capture.repeatVisits = capture.repeatVisits + 1
else capture.seen[frame] = true; capture.uniqueFrames = capture.uniqueFrames + 1 end
end
-- set defaults -- set defaults
if not r and not g and not b then if not r and not g and not b then
r, g, b, a = module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, module.config["darkmode.color"].a r, g, b, a = tint.r, tint.g, tint.b, tint.a
end end
-- iterate through all subframes -- iterate through all subframes
if frame and frame.GetChildren then if frame and frame.GetChildren then
if capture then scan.tables = scan.tables + 1 end
for _, frame in pairs({frame:GetChildren()}) do for _, frame in pairs({frame:GetChildren()}) do
DarkenFrame(frame, r, g, b, a) DarkenFrame(frame, r, g, b, a, scan)
end end
end end
-- set vertex on all regions -- set vertex on all regions
if frame and frame.GetRegions then if frame and frame.GetRegions then
if capture then scan.tables = scan.tables + 1 end
-- read name -- read name
local name = frame.GetName and frame:GetName() local name = frame.GetName and frame:GetName()
-- set a dark backdrop border color everywhere -- set the palette's backdrop border color everywhere
frame:SetBackdropBorderColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, module.config["darkmode.color"].a) if not frame.stPreserveRarityColor then
if capture then capture.borderWrites = capture.borderWrites + 1 end
frame:SetBackdropBorderColor(edge.r, edge.g, edge.b, edge.a)
end
-- add special backgrounds to quests and such -- add special backgrounds to quests and such
for pattern, inset in pairs(backgrounds) do for pattern, inset in pairs(backgrounds) do
if name and string.find(name, pattern) then AddSpecialBackground(frame, inset[1], inset[2], inset[3], inset[4]) end if name and string.find(name, pattern) then
AddSpecialBackground(frame, inset[1], inset[2], inset[3], inset[4])
end
end end
-- add black borders around specified buttons -- add black borders around specified buttons
for pattern, inset in pairs(borders) do for pattern, inset in pairs(borders) do
if name and string.find(name, pattern) then AddBorder(frame, inset, module.config["darkmode.color"]) end if name and string.find(name, pattern) then AddBorder(frame, inset, edge) end
end end
-- scan through all regions (textures) -- scan through all regions (textures)
for id, region in pairs({frame:GetRegions()}) do for id, region in pairs({frame:GetRegions()}) do
if capture then scan.regions = scan.regions + 1 end
if region.SetVertexColor and region:GetObjectType() == "Texture" then if region.SetVertexColor and region:GetObjectType() == "Texture" then
if region:GetTexture() and string.find(region:GetTexture(), "UI%-Panel%-Button%-Up") then if region:GetTexture() and string.find(region:GetTexture(), "UI%-Panel%-Button%-Up") then
-- monochrome buttons -- monochrome buttons
-- region:SetDesaturated(true) -- region:SetDesaturated(true)
elseif region.stOriginalQuestPaper then
-- Untinted copies of native quest/gossip parchment.
elseif region.stSlotEdge then
-- Border strips are maintained by their source texture.
elseif IsSlotArtwork(region:GetTexture()) then
TintSlotBorder(region, frame)
elseif name == "InboxFrame" then
-- The inbox artwork includes its parchment. Tinting it turns the
-- paper orange; covering it with stationery leaves a straight seam.
-- Keep these regions stock; child buttons and the mail header still
-- receive the palette through their own frames.
elseif name and id and regionskips[name] and regionskips[name][id] then elseif name and id and regionskips[name] and regionskips[name][id] then
-- skip special regions -- skip special regions
elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then
@@ -191,18 +466,251 @@ local function DarkenFrame(frame, r, g, b, a)
elseif IsBlacklisted(region) then elseif IsBlacklisted(region) then
-- skip blacklisted texture names -- skip blacklisted texture names
else else
if capture then capture.textureWrites = capture.textureWrites + 1 end
region:SetVertexColor(r,g,b,a) region:SetVertexColor(r,g,b,a)
if name == "SpellBookFrame" then RestoreSpellbookParchment(frame, region) end
end end
end end
end end
end end
if frame and frame.GetRegions then RestoreQuestParchment(frame) end
if frame and frame.GetRegions then RestoreQuestLogParchment(frame) end
end end
-- register darken frame to global -- register darken frame to global
ShaguTweaks.DarkenFrame = DarkenFrame ShaguTweaks.DarkenFrame = DarkenFrame
module.enable = function(self) -- Other addons that draw their own tooltip-style borders. Their frames (and
ShaguTweaks.DarkMode = true -- everything inside them) keep following the palette even when the addon
-- sets its border color again later, or builds a window only when opened.
local ADDON_PREFIXES = {
"ShaguDPS", "Bagshui", "Outfitter", "SoulstoneTracker", "SSWarlock",
"SSTClassic", "SSTracker", "OctoBus", "RollFor", "VCB_",
"aux_frame", "AuxCraft", "AuxDragOutline",
-- Filled mail slots get rarity borders after their item texture changes.
"MailAttachment", "SendMailPackageButton",
}
local function IsAddonFrameName(name)
if not name then return end
for i = 1, table.getn(ADDON_PREFIXES) do
if string.find(name, ADDON_PREFIXES[i], 1, true) == 1 then return true end
end
end
-- Only light grey/white/silver borders follow the palette. Colored borders
-- (item quality, hover highlights, class or theme colors) and dark ones stay
-- as the addon set them.
local function IsLightNeutral(r, g, b)
r, g, b = tonumber(r) or 0, tonumber(g) or 0, tonumber(b) or 0
local hi, lo = math.max(r, g, b), math.min(r, g, b)
return hi - lo < .1 and hi >= .35
end
local function LockBorder(frame)
if not frame or frame.stBorderLock or not frame.SetBackdropBorderColor then return end
frame.stBorderLock = true
local setColor, setBackdrop = frame.SetBackdropBorderColor, frame.SetBackdrop
frame.SetBackdropBorderColor = function(self, r, g, b, a)
if not self.stPreserveRarityColor and IsLightNeutral(r, g, b) then
setColor(self, edge.r, edge.g, edge.b, a or 1)
else
setColor(self, r, g, b, a)
end
end
-- a new backdrop starts with a white border
if setBackdrop then
frame.SetBackdrop = function(self, backdrop)
local r, g, b, a
if self.stPreserveRarityColor and self.GetBackdropBorderColor then
r, g, b, a = self:GetBackdropBorderColor()
end
setBackdrop(self, backdrop)
if backdrop and backdrop.edgeFile then
if r then
setColor(self, r, g, b, a)
else
setColor(self, edge.r, edge.g, edge.b, 1)
end
end
end
end
-- run the current color through the filter once
if frame.GetBackdropBorderColor then
local ok, r, g, b, a = pcall(frame.GetBackdropBorderColor, frame)
if ok and r then frame:SetBackdropBorderColor(r, g, b, a) end
end
end
-- Border textures (VCB's buff and weapon borders): same rule, on the
-- texture's vertex color.
local function LockTextureBorder(texture)
if texture.stBorderLock or not texture.SetVertexColor then return end
texture.stBorderLock = true
local setColor = texture.SetVertexColor
texture.SetVertexColor = function(self, r, g, b, a)
if IsLightNeutral(r, g, b) then
setColor(self, edge.r, edge.g, edge.b, a or 1)
else
setColor(self, r, g, b, a)
end
end
if texture.GetVertexColor then
local ok, r, g, b, a = pcall(texture.GetVertexColor, texture)
if ok and r then texture:SetVertexColor(r, g, b, a) end
end
end
local function LockTree(frame)
if not frame then return end
if IsExcludedFrame(frame) then return end
local capture = ShaguTweaks.RecolorCapture
if capture then capture.lockVisits = capture.lockVisits + 1 end
LockBorder(frame)
if frame.GetRegions then
if capture then capture.lockTables = capture.lockTables + 1 end
local regions = { frame:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
local name = region.GetName and region:GetName()
if name and string.find(name, "Border$") and region.GetObjectType and region:GetObjectType() == "Texture" then
LockTextureBorder(region)
end
end
end
if frame.GetChildren then
if capture then capture.lockTables = capture.lockTables + 1 end
local children = { frame:GetChildren() }
for i = 1, table.getn(children) do LockTree(children[i]) end
end
end
-- find the addon frames that exist already, anywhere below UIParent
local function FindAddonFrames(frame)
local capture = ShaguTweaks.RecolorCapture
if capture then
capture.discoveryVisits = capture.discoveryVisits + 1
capture.discoveryTables = capture.discoveryTables + 1
end
local children = { frame:GetChildren() }
for i = 1, table.getn(children) do
local child = children[i]
if IsExcludedFrame(child) then
-- Leave the whole TrinketMenu subtree to its own styling code.
elseif IsAddonFrameName(child.GetName and child:GetName()) then
LockTree(child)
else
FindAddonFrames(child)
end
end
end
-- These buttons need explicit discovery when created by XML or nested below
-- a minimap child frame. Outfitter and TrinketMenu bake the rim into their art.
local addonMinimapButtons = {
OutfitterMinimapButton = true,
TrinketMenu_IconFrame = true,
AtlasCFMMinimapButton = true,
}
local function TintMinimapButton(frame)
if not tint or not frame or not frame.GetRegions then return end
local name = frame.GetName and frame:GetName()
if name == "OutfitterMinimapButton" or name == "TrinketMenu_IconFrame" then
local normal = frame:GetNormalTexture()
local pushed = frame:GetPushedTexture()
if normal then normal:SetVertexColor(tint.r, tint.g, tint.b, 1) end
if pushed then pushed:SetVertexColor(tint.r, tint.g, tint.b, 1) end
return
end
if IsExcludedFrame(frame) then return end
local regions = { frame:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
local texture = region.GetTexture and region:GetTexture()
if texture and string.find(string.lower(texture), "minimap%-trackingborder") then
region:SetVertexColor(tint.r, tint.g, tint.b, 1)
end
end
end
local function TintLoadedMinimapButton(name)
HookAddonOrVariable(name, function() TintMinimapButton(_G[name]) end)
end
local function RecolorOnShow(frame)
if not frame or frame.stRecolorOnShow then return end
frame.stRecolorOnShow = true
if golden.rollforLoot and frame:GetName() == "RollForLootFrameHeader" then golden.rollforLoot(frame) end
local pending
local function Trailing()
pending = nil
DarkenFrame(frame)
end
ShaguTweaks.HookScript(frame, "OnShow", function()
DarkenFrame(frame)
if not pending and _G.C_Timer and _G.C_Timer.After then
pending = true
_G.C_Timer.After(0, Trailing)
end
end)
if frame:IsShown() then DarkenFrame(frame) end
end
local function LockAddonFrames()
FindAddonFrames(UIParent)
for name in pairs(showframes) do RecolorOnShow(_G[name]) end
if Minimap then
local buttons = { Minimap:GetChildren() }
for i = 1, table.getn(buttons) do TintMinimapButton(buttons[i]) end
end
for name in pairs(addonMinimapButtons) do
local button = _G[name]
if not button then
TintLoadedMinimapButton(name)
elseif button:GetParent() ~= Minimap then
TintMinimapButton(button)
end
end
-- frames those addons create later: named after the addon, or created
-- inside one of their frames (templates bring their children along)
local create = CreateFrame
CreateFrame = function(a1, a2, a3, a4, a5, a6)
local frame = create(a1, a2, a3, a4, a5, a6)
if frame and (IsAddonFrameName(a2) or (type(a3) == "table" and a3.stBorderLock)) then
LockTree(frame)
end
if frame and type(a2) == "string" and showframes[a2] then RecolorOnShow(frame) end
-- buttons that get an extra border (VCB buffs made after login)
if frame and type(a2) == "string" then
for pattern, inset in pairs(borders) do
if string.find(a2, pattern) then AddBorder(frame, inset, edge) end
end
end
-- minimap buttons get their border texture right after they are made
if frame and ((a3 and a3 == Minimap) or addonMinimapButtons[a2]) then
if _G.C_Timer and _G.C_Timer.After then
_G.C_Timer.After(0, function() TintMinimapButton(frame) end)
end
end
return frame
end
end
local function Apply(tintColor, edgeColor, darkBackgrounds)
if tint then return end
tint, edge = tintColor, edgeColor
ShaguTweaks.UITint = tint
ShaguTweaks.UIBorder = edge
local name, original, r, g, b local name, original, r, g, b
local hookBuffButton_Update = BuffButton_Update local hookBuffButton_Update = BuffButton_Update
@@ -220,23 +728,45 @@ module.enable = function(self)
elseif not original and _G[name] then elseif not original and _G[name] then
-- tbc buff buttons don't have borders, so we -- tbc buff buttons don't have borders, so we
-- need to manually add a dark one. -- need to manually add a dark one.
AddBorder(_G[name], 2, module.config["darkmode.color"]) AddBorder(_G[name], 2, edge)
end end
end end
TOOLTIP_DEFAULT_COLOR.r = module.config["darkmode.color"].r TOOLTIP_DEFAULT_COLOR.r = edge.r
TOOLTIP_DEFAULT_COLOR.g = module.config["darkmode.color"].g TOOLTIP_DEFAULT_COLOR.g = edge.g
TOOLTIP_DEFAULT_COLOR.b = module.config["darkmode.color"].b TOOLTIP_DEFAULT_COLOR.b = edge.b
TOOLTIP_DEFAULT_BACKGROUND_COLOR.r = module.config["darkmode.color"].r -- Darkened UI also greys the tooltip background; Golden UI keeps it.
TOOLTIP_DEFAULT_BACKGROUND_COLOR.g = module.config["darkmode.color"].g if darkBackgrounds then
TOOLTIP_DEFAULT_BACKGROUND_COLOR.b = module.config["darkmode.color"].b TOOLTIP_DEFAULT_BACKGROUND_COLOR.r = tint.r
TOOLTIP_DEFAULT_BACKGROUND_COLOR.g = tint.g
TOOLTIP_DEFAULT_BACKGROUND_COLOR.b = tint.b
end
DarkenFrame(UIParent) DarkenFrame(UIParent)
DarkenFrame(WorldMapFrame) DarkenFrame(WorldMapFrame)
DarkenFrame(DropDownList1) DarkenFrame(DropDownList1)
DarkenFrame(DropDownList2) DarkenFrame(DropDownList2)
DarkenFrame(DropDownList3) DarkenFrame(DropDownList3)
LockAddonFrames()
-- Bagshui restores its empty-slot artwork to white whenever items are
-- assigned. Reapply only to that background, leaving icons, quality
-- borders and user-configured solid background colors alone.
HookAddonOrVariable("Bagshui", function()
local ui = _G.Bagshui and _G.Bagshui.prototypes and _G.Bagshui.prototypes.Ui
if not ui or not ui.UpdateItemButtonColorsAndBadges then return end
ShaguTweaks.hooksecurefunc(ui, "UpdateItemButtonColorsAndBadges", function(self, button)
local data = button and button.bagshuiData
local background = data and data.buttonComponents and data.buttonComponents.background
if background then TintSlotBorder(background, button) end
end)
end)
-- the loot window gets its art set again when it opens
if LootFrame then
ShaguTweaks.HookScript(LootFrame, "OnShow", function() DarkenFrame(LootFrame) end)
end
-- align all actionbutton textures -- align all actionbutton textures
local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "Shapeshift" } local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "Shapeshift" }
@@ -259,7 +789,7 @@ module.enable = function(self)
if button[func] then if button[func] then
local tex = button[func](button) local tex = button[func](button)
if tex then if tex then
tex:SetVertexColor(module.config["darkmode.color"].r+.2, module.config["darkmode.color"].g+.2, module.config["darkmode.color"].b+.2, 1) tex:SetVertexColor(math.min(tint.r+.2, 1), math.min(tint.g+.2, 1), math.min(tint.b+.2, 1), 1)
end end
end end
end end
@@ -268,12 +798,12 @@ module.enable = function(self)
HookAddonOrVariable("Blizzard_AuctionUI", function() HookAddonOrVariable("Blizzard_AuctionUI", function()
for i = 1, 15 do for i = 1, 15 do
local tex = _G["AuctionFilterButton"..i]:GetNormalTexture() local tex = _G["AuctionFilterButton"..i]:GetNormalTexture()
tex:SetVertexColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, 1) tex:SetVertexColor(tint.r, tint.g, tint.b, 1)
end end
for i = 1, 8 do for i = 1, 8 do
_G["BrowseButton"..i.."Left"]:SetVertexColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, 1) _G["BrowseButton"..i.."Left"]:SetVertexColor(tint.r, tint.g, tint.b, 1)
_G["BrowseButton"..i.."Right"]:SetVertexColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, 1) _G["BrowseButton"..i.."Right"]:SetVertexColor(tint.r, tint.g, tint.b, 1)
end end
end) end)
@@ -413,3 +943,253 @@ module.enable = function(self)
table.insert(ShaguTweaks.libnameplate.OnShow, ReapplyShaguDarkMode) table.insert(ShaguTweaks.libnameplate.OnShow, ReapplyShaguDarkMode)
end end
end end
-- Title bars: the dark strips the window titles sit on. Golden UI
-- multiplies them toward WoW Forever's red-brown with a MOD layer. Measured
-- from the window art: rows below the frame's top edge, and how far the strip
-- runs from the left (after the portrait) and from the right (before the
-- close button).
local STRIP_A = { 72, -16, -60, -34 } -- character, spellbook, mail, quest log art
local STRIP_B = { 72, -15, -62, -33 } -- merchant, friends art
local function Shade(parent, layer, color)
local shade = parent:CreateTexture(nil, layer)
shade:SetTexture(1, 1, 1, 1)
shade:SetVertexColor(tonumber(color.r) or .8, tonumber(color.g) or .63, tonumber(color.b) or .62, 1)
shade:SetBlendMode("MOD")
return shade
end
local function PlaceStrip(shade, frame, strip)
shade:ClearAllPoints()
shade:SetPoint("TOPLEFT", frame, "TOPLEFT", strip[1], strip[2])
shade:SetPoint("BOTTOMRIGHT", frame, "TOPRIGHT", strip[3], strip[4])
end
-- The shade has to sit above the window art, which would darken a title
-- that is drawn in the same frame. Those titles are drawn again on top and
-- the original is kept, invisible, for its position and text.
local function RedrawTitle(holder, title)
if not title then return end
local copy = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
local font = title.GetFontObject and title:GetFontObject()
if font then copy:SetFontObject(font) end
copy:SetAllPoints(title)
if title.GetJustifyH then copy:SetJustifyH(title:GetJustifyH()) end
copy:SetText(title:GetText())
title:SetAlpha(0)
local setText = title.SetText
title.SetText = function(self, text)
setText(self, text)
copy:SetText(self:GetText())
end
local setFormattedText = title.SetFormattedText
if type(setFormattedText) == "function" then
title.SetFormattedText = function(self, ...)
setFormattedText(self, unpack(arg))
copy:SetText(self:GetText())
end
end
return copy
end
local function ShadeTitleBar(frame, strip, title, color, level)
if not frame or frame.stTitleBar then return end
local bar = CreateFrame("Frame", nil, frame)
bar:SetAllPoints(frame)
if level then bar:SetFrameLevel(level) end
frame.stTitleBar = bar
bar.shade = Shade(bar, "BACKGROUND", color)
PlaceStrip(bar.shade, frame, strip)
bar.title = RedrawTitle(bar, title)
return bar
end
-- frames whose titles live in child frames (or above the art layer): the
-- shade is a plain texture in the window itself, nothing to redraw
local function ShadeStripInFrame(frame, strip, layer, color)
if not frame or frame.stTitleShade then return end
frame.stTitleShade = Shade(frame, layer, color)
PlaceStrip(frame.stTitleShade, frame, strip)
end
-- MailFrame's four art tiles contain both the metal frame and the paper.
-- Restore cropped copies of the original paper above the tinted tiles, below
-- the inbox's child frames. Parent them to InboxFrame so they hide on the
-- Send Mail and TurtleMail Log tabs. Matching the original pixels preserves the torn
-- edges and tile seams without replacing the parchment with stationery.
local function RestoreMailParchment()
if not InboxFrame then return end
if not InboxFrame.stParchmentRetryHook then
InboxFrame.stParchmentRetryHook = true
ShaguTweaks.HookScript(InboxFrame, "OnShow", RestoreMailParchment)
end
if InboxFrame.stOriginalParchment then return end
InboxFrame.stParchmentTiles = InboxFrame.stParchmentTiles or {}
local copies = InboxFrame.stParchmentTiles
local complete = true
local tiles = {
{ "MailFrameTopLeft", 22, 74, 256, 256 },
{ "MailFrameTopRight", 0, 74, 80, 256 },
{ "MailFrameBotLeft", 22, 0, 256, 170 },
{ "MailFrameBotRight", 0, 0, 80, 170 },
}
for i = 1, table.getn(tiles) do
local tile = tiles[i]
local source = _G[tile[1]]
if not copies[i] and source and source:GetTexture() then
local w, h = source:GetWidth(), source:GetHeight()
if w and h and w > 0 and h > 0 then
local paper = InboxFrame:CreateTexture(nil, "BACKGROUND")
paper:SetTexture(source:GetTexture())
paper:SetVertexColor(1, 1, 1, 1)
paper:SetTexCoord(tile[2] / w, tile[4] / w, tile[3] / h, tile[5] / h)
paper:SetWidth(tile[4] - tile[2])
paper:SetHeight(tile[5] - tile[3])
paper:SetPoint("TOPLEFT", source, "TOPLEFT", tile[2], -tile[3])
copies[i] = paper
end
end
if not copies[i] then complete = false end
end
InboxFrame.stOriginalParchment = complete or nil
end
module.enable = function(self)
ShaguTweaks.DarkMode = true
Apply(module.config["darkmode.color"], module.config["darkmode.color"], true)
RestoreMailParchment()
end
local function ShadeNpcTitleBar(frame, nameFrame, panels, color)
if not frame or not nameFrame or frame.stTitleBar then return end
local level = frame:GetFrameLevel() + 1
for _, panel in pairs(panels) do
if panel then level = math.max(level, panel:GetFrameLevel() + 1) end
end
-- NPC names already live in a separate frame. Keep that native title above
-- both the panel artwork and shade, without creating another text copy.
if nameFrame:GetFrameLevel() <= level then nameFrame:SetFrameLevel(level + 1) end
ShadeTitleBar(frame, {72,-22,-60,-39}, nil, color, level)
end
local function ShadeTitleBars(color)
-- world map: its strip is 3 to 21 px below the top of its art (384 above
-- the frame center), from the left corner to the window buttons
if WorldMapFrame and WorldMapFrameTitle and not WorldMapFrame.stTitleBar then
local bar = ShadeTitleBar(WorldMapFrame, { 0, 0, 0, 0 }, WorldMapFrameTitle, color)
bar.shade:ClearAllPoints()
bar.shade:SetPoint("TOPLEFT", WorldMapFrame, "CENTER", -510, 381)
bar.shade:SetPoint("BOTTOMRIGHT", WorldMapFrame, "CENTER", 455, 363)
-- the stone art is lighter around the seam of its first two pieces
-- (measured in 8 px columns): darken those columns a bit more so the
-- bar comes out even
local even = {
{ -272, .81 }, { -264, .76 },
{ -256, .66 }, { -248, .52 }, { -240, .49 }, { -232, .47 }, { -224, .53 }, { -216, .54 },
{ -208, .52 }, { -200, .58 }, { -192, .69 }, { -184, .76 }, { -176, .68 }, { -168, .68 },
{ -160, .69 }, { -152, .71 }, { -144, .78 }, { -136, .86 },
}
for i = 1, table.getn(even) do
local x, f = even[i][1], even[i][2]
local fix = Shade(bar, "BACKGROUND", { r = f, g = f, b = f })
fix:SetPoint("TOPLEFT", WorldMapFrame, "CENTER", x, 381)
fix:SetPoint("BOTTOMRIGHT", WorldMapFrame, "CENTER", x + 8, 363)
end
end
ShadeTitleBar(MerchantFrame, STRIP_B, MerchantNameText, color)
ShadeTitleBar(SpellBookFrame, STRIP_A, SpellBookTitleText, color)
ShadeTitleBar(FriendsFrame, STRIP_B, FriendsFrameTitleText, color)
ShadeNpcTitleBar(QuestFrame, _G.QuestNpcNameFrame,
{ _G.QuestFrameGreetingPanel, _G.QuestFrameDetailPanel, _G.QuestFrameProgressPanel, _G.QuestFrameRewardPanel }, color)
ShadeNpcTitleBar(GossipFrame, _G.GossipNpcNameFrame, { _G.GossipFrameGreetingPanel }, color)
-- the quest log is wider: its strip ends before the close button
ShadeTitleBar(QuestLogFrame, { 72, -16, -121, -34 }, QuestLogTitleText, color)
-- mail: the art is on the BORDER layer, the titles are on the page frames
ShadeStripInFrame(MailFrame, STRIP_A, "ARTWORK", color)
-- character: the art is on the tab pages (PaperDollFrame and the others),
-- the name on CharacterNameFrame. The shade goes between the two.
if CharacterFrame and PaperDollFrame and CharacterNameFrame then
local level = PaperDollFrame:GetFrameLevel() + 1
if CharacterNameFrame:GetFrameLevel() <= level then CharacterNameFrame:SetFrameLevel(level + 1) end
ShadeTitleBar(CharacterFrame, STRIP_A, nil, color, level)
end
end
-- RollFor's classic loot window: the art is on the BACKGROUND layer and the
-- "Items" title on ARTWORK, so the shade fits between them. The strip runs
-- 12 to 30 px down, from the portrait to 25 px before the right edge (the
-- window width changes with the item names).
local function ShadeRollForLoot(frame, color)
ShadeStripInFrame(frame, { 68, -12, -25, -30 }, "BORDER", color)
end
local function GoldenUnitNameBar(frame)
local title = frame and frame.name
if not title or frame.stGoldenNameBar then return end
local parent = title:GetParent()
local point, relative, relativePoint, x, y = title:GetPoint(1)
if not parent or not point then return end
local bar = CreateFrame("Frame", nil, parent)
bar:SetFrameLevel(parent:GetFrameLevel() + 1)
bar:SetWidth(119)
bar:SetHeight(14)
bar:SetPoint(point, relative, relativePoint, x, y)
bar.texture = bar:CreateTexture(nil, "BACKGROUND")
-- Opaque stone from the same native title strip used by the character
-- window, excluding its metal edges and portrait. No world bleed-through.
bar.texture:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-General-TopLeft")
bar.texture:SetTexCoord(80/256, 255/256, 18/256, 32/256)
bar.texture:SetAllPoints(bar)
local color = golden.config["goldmode.bar"]
bar.texture:SetVertexColor((tonumber(tint.r) or 1) * (tonumber(color.r) or .8),
(tonumber(tint.g) or .78) * (tonumber(color.g) or .63),
(tonumber(tint.b) or .4) * (tonumber(color.b) or .62), 1)
local originalAlpha = title:GetAlpha()
bar.title = RedrawTitle(bar, title)
frame.stGoldenNameBar = bar
local function UpdateNameBar()
if UnitExists("target") and not UnitIsPlayer("target") then
title:SetAlpha(0)
bar:Show()
else
bar:Hide()
title:SetAlpha(originalAlpha)
end
end
bar:RegisterEvent("PLAYER_TARGET_CHANGED")
bar:RegisterEvent("PLAYER_ENTERING_WORLD")
bar:SetScript("OnEvent", UpdateNameBar)
ShaguTweaks.HookScript(frame, "OnShow", UpdateNameBar)
UpdateNameBar()
end
golden.enable = function(self)
-- Both recolor the same textures; Darkened UI wins when both are ticked.
if ShaguTweaks_config and ShaguTweaks_config[T["Darkened UI"]] == 1 then return end
ShaguTweaks.GoldMode = true
Apply(golden.config["goldmode.color"], golden.config["goldmode.border"], false)
ShadeTitleBars(golden.config["goldmode.bar"])
-- The opaque title artwork is only for NPC targets. Player names retain
-- their native/class-colored background, including our own player frame.
-- Big Health moves the name into the enlarged health fill. An opaque
-- header there would cover its upper portion, regardless of module order.
if not ShaguTweaks_config or ShaguTweaks_config[T["Unit Frame Big Health for Target"]] ~= 1 then
GoldenUnitNameBar(TargetFrame)
end
RestoreMailParchment()
golden.rollforLoot = function(frame) ShadeRollForLoot(frame, golden.config["goldmode.bar"]) end
if _G.RollForLootFrameHeader then golden.rollforLoot(_G.RollForLootFrameHeader) end
end
-- Function references for the optional companion; no profiling starts here.
ShaguTweaks.RecolorProfileFunctions = {
DarkenFrame = DarkenFrame, LockTree = LockTree,
FindAddonFrames = FindAddonFrames, ClassifyPath = ClassifyPath,
}
+177
View File
@@ -10,11 +10,186 @@ local module = ShaguTweaks:register({
enabled = true, enabled = true,
}) })
local function MatchBackground(compare, source)
if not ShaguTweaks_config or ShaguTweaks_config[T["Darkened UI"]] ~= 1
or not source.GetBackdropColor or not compare.SetBackdropColor then return end
local r,g,b,a = source:GetBackdropColor()
if not r then return end
local cr,cg,cb,ca = compare:GetBackdropColor()
if cr ~= r or cg ~= g or cb ~= b or ca ~= a then compare:SetBackdropColor(r,g,b,a) end
end
local function EnableNativeCompare(self)
if not API.tooltipsetitem or not API.modifierstate or not API.timer
or not ShoppingTooltip1 or not ShoppingTooltip2
or type(ShoppingTooltip1.SetHyperlinkCompareItem) ~= "function"
or type(ShoppingTooltip2.SetHyperlinkCompareItem) ~= "function" then return false end
local active, activeLink, activeOwner, activeRevision
local firstShown, secondShown
local revision, building = 0, false
local tracked, order = {}, {}
local function Hide(tooltip)
if tooltip and tooltip ~= active then return end
active = nil
activeLink, activeOwner, activeRevision = nil, nil, nil
firstShown, secondShown = nil, nil
ShoppingTooltip1:Hide(); ShoppingTooltip2:Hide()
end
local function Restore(tooltip, link, owner)
if not API.IsShiftKeyDown() or active ~= tooltip or activeLink ~= link
or activeOwner ~= owner or activeRevision ~= revision then return false end
-- Hide clears native ownership. Show alone can briefly display the cached
-- text but leave an unowned tooltip that the client then discards/fades.
-- Rebind to the actual hovered button (not the source tooltip) and refill
-- in the same frame only when the native owner/content was lost.
building = true
for i = 1, 2 do
local compare = i == 1 and ShoppingTooltip1 or ShoppingTooltip2
if (i == 1 and firstShown) or (i == 2 and secondShown) then
if not compare:IsShown() or compare:GetOwner() ~= (owner or UIParent) then
local point, relative, relativePoint, x, y = compare:GetPoint()
compare:SetOwner(owner or UIParent, "ANCHOR_NONE")
if point then compare:SetPoint(point, relative, relativePoint, x, y) end
if compare:SetHyperlinkCompareItem(link, i, true, tooltip) then compare:Show() end
end
MatchBackground(compare, tooltip)
end
end
building = false
return true
end
local function Show(tooltip)
if building then return end
if not tooltip:IsShown() then Hide(tooltip); return end
if not API.IsShiftKeyDown() then Hide(); return end
local _, link = tooltip:GetItem()
if not link then Hide(tooltip); return end
local owner = tooltip:GetOwner()
-- Native/tooltips addons can rebuild the hovered item repeatedly. Its
-- comparison does not change unless the item, owner or equipped data did.
if Restore(tooltip, link, owner) then return end
building = true
Hide()
local x = GetCursorPosition() / UIParent:GetEffectiveScale()
local left = x >= GetScreenWidth() / 2
local point, parent = tooltip:GetPoint()
if parent == UIParent and point == "TOPRIGHT" then left = true end
local anchor, relative = left and "TOPRIGHT" or "TOPLEFT", left and "TOPLEFT" or "TOPRIGHT"
local previous = tooltip
for i = 1, 2 do
local compare = i == 1 and ShoppingTooltip1 or ShoppingTooltip2
compare:SetOwner(owner or UIParent, "ANCHOR_NONE")
compare:ClearAllPoints()
compare:SetPoint(anchor, previous, relative, 0, 0)
-- Preserve the full item link (enchants/random suffixes). Native code
-- selects filled ring/weapon slots and supplies the equipped header.
if compare:SetHyperlinkCompareItem(link, i, true, tooltip) then
compare:Show()
MatchBackground(compare, tooltip)
previous = compare
active = tooltip
else compare:Hide() end
end
if active then
activeLink, activeOwner, activeRevision = link, owner, revision
firstShown, secondShown = ShoppingTooltip1:IsShown(), ShoppingTooltip2:IsShown()
end
building = false
end
local function Schedule(tooltip)
local state = tracked[tooltip]
if building or not state or state.pending then return end
state.pending = true
API.Defer(state.refresh)
end
-- A comparison may be hidden by native/foreign tooltip code independently
-- of its source. Repair on that event; no recurring polling driver.
local function ComparisonHidden()
if active and not building then Schedule(active) end
end
ShoppingTooltip1:HookScript("OnHide", ComparisonHidden)
ShoppingTooltip2:HookScript("OnHide", ComparisonHidden)
local function SourceUpdated(tooltip)
if building then return end
if tooltip:IsShown() then
local _, link = tooltip:GetItem()
if link then Restore(tooltip, link, tooltip:GetOwner()) end
end
Schedule(tooltip)
end
local function Track(tooltip)
if not tooltip or tracked[tooltip] or type(tooltip.GetItem) ~= "function" then return end
local state = {}
tracked[tooltip] = state
table.insert(order, tooltip)
state.refresh = function()
state.pending = nil
Show(tooltip)
end
local updated = function() SourceUpdated(tooltip) end
local settled = function() Schedule(tooltip) end
tooltip:HookScript("OnTooltipSetItem", updated)
-- Clearing is part of a normal item rebuild. Inspect the settled content
-- next frame instead of hiding a still-valid comparison immediately.
tooltip:HookScript("OnTooltipCleared", settled)
tooltip:HookScript("OnHide", settled)
tooltip:HookScript("OnShow", updated)
end
ShoppingTooltip1:SetClampedToScreen(true)
ShoppingTooltip2:SetClampedToScreen(true)
Track(GameTooltip)
Track(ItemRefTooltip)
ShaguTweaks.HookAddonOrVariable("AtlasLoot", function()
Track(AtlasLootTooltip); Track(AtlasLootTooltip2)
end)
local function VisibleItemTooltip()
for _, tooltip in ipairs(order) do
if tooltip:IsShown() then
local _, link = tooltip:GetItem()
if link then return tooltip end
end
end
end
local modifier = CreateFrame("Frame")
self.nativeModifier = modifier
modifier:RegisterEvent("MODIFIER_STATE_CHANGED")
local utils = _G.C_EventUtils
if utils and utils.IsEventValid and utils.IsEventValid("PLAYER_EQUIPMENT_CHANGED") then
modifier:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
else modifier:RegisterEvent("UNIT_INVENTORY_CHANGED") end
if utils and utils.IsEventValid and utils.IsEventValid("GET_ITEM_INFO_RECEIVED") then
modifier:RegisterEvent("GET_ITEM_INFO_RECEIVED")
end
modifier:SetScript("OnEvent", function()
if event == "PLAYER_EQUIPMENT_CHANGED" or event == "UNIT_INVENTORY_CHANGED"
or event == "GET_ITEM_INFO_RECEIVED" then
if event == "UNIT_INVENTORY_CHANGED" and arg1 ~= "player" then return end
revision = revision + 1
if not API.IsShiftKeyDown() then return end
if active then Schedule(active)
else
local tooltip = VisibleItemTooltip()
if tooltip then Schedule(tooltip) end
end
return
end
if arg1 ~= "LSHIFT" and arg1 ~= "RSHIFT" then return end
if not API.IsShiftKeyDown() then Hide(); return end
if active and active:IsShown() then Show(active); return end
local tooltip = VisibleItemTooltip()
if tooltip then Show(tooltip) end
end)
return true
end
module.enable = function(self) module.enable = function(self)
if self.installed then return end
if EnableNativeCompare(self) then self.installed = true; return end
if not API.tooltipsetitem or not API.modifierstate or not API.timer if not API.tooltipsetitem or not API.modifierstate or not API.timer
or not API.iteminventorytype or not API.iteminventoryslotkey then or not API.iteminventorytype or not API.iteminventoryslotkey then
return return
end end
self.installed = true
local sides = { "Left", "Right" } local sides = { "Left", "Right" }
@@ -129,6 +304,7 @@ module.enable = function(self)
ShoppingTooltip1:SetInventoryItem("player", slotID) ShoppingTooltip1:SetInventoryItem("player", slotID)
ShoppingTooltip1:Show() ShoppingTooltip1:Show()
AddHeader(ShoppingTooltip1) AddHeader(ShoppingTooltip1)
MatchBackground(ShoppingTooltip1, tooltip)
if slotNames[2] then if slotNames[2] then
local secondSlotID = GetInventorySlotInfo(slotNames[2]) local secondSlotID = GetInventorySlotInfo(slotNames[2])
@@ -142,6 +318,7 @@ module.enable = function(self)
ShoppingTooltip2:SetInventoryItem("player", secondSlotID) ShoppingTooltip2:SetInventoryItem("player", secondSlotID)
ShoppingTooltip2:Show() ShoppingTooltip2:Show()
AddHeader(ShoppingTooltip2) AddHeader(ShoppingTooltip2)
MatchBackground(ShoppingTooltip2, tooltip)
end end
activeTooltip = tooltip activeTooltip = tooltip
+1 -1
View File
@@ -1,5 +1,5 @@
local _G = ShaguTweaks.GetGlobalEnv() local _G = ShaguTweaks.GetGlobalEnv()
local L, T = ShaguTweaks.L, ShaguTweaks.T local T = ShaguTweaks.T
local API = ShaguTweaks.API local API = ShaguTweaks.API
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
-53
View File
@@ -1,53 +0,0 @@
local T = ShaguTweaks.T
local GetColorGradient = ShaguTweaks.GetColorGradient
local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({
title = T["Unit Frame Health Colors"],
description = T["Change health text color based on its value."],
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
enabled = nil,
})
local function SetTextColorIfChanged(fontString, r, g, b, a)
local cr, cg, cb, ca = fontString:GetTextColor()
if cr ~= r or cg ~= g or cb ~= b or ca ~= a then
fontString:SetTextColor(r, g, b, a)
end
end
module.enable = function(self)
local function UpdateManaTextColor(uf)
if not uf then uf = this end
if not uf or not uf.manabar or not uf.manabar.TextString then return end
local name = uf.manabar:GetName()
if name and not strfind(name, "Health") then
local r, g, b = uf.manabar:GetStatusBarColor()
SetTextColorIfChanged(uf.manabar.TextString,
(r + 2) / 3, (g + 2) / 3, (b + 2) / 3, 1)
end
end
local function UpdateHealthTextColor(sb)
if not sb then sb = this end
if not sb or not sb.TextString or not sb.unit then return end
local name = sb:GetName()
if not name or not strfind(name, "Health") then return end
local _, max = sb:GetMinMaxValues()
local cur = sb:GetValue()
local percent = max > 0 and floor(cur / max * 100) or 0
local r, g, b = GetColorGradient(percent / 100)
SetTextColorIfChanged(sb.TextString,
(r + 1) / 2, (g + 1) / 2, (b + 1) / 2, .75)
end
-- Append our color adjustments instead of replacing Blizzard's global
-- functions. This keeps the module cooperative with other UI addons.
hooksecurefunc("UnitFrame_UpdateManaType", UpdateManaTextColor)
hooksecurefunc("TextStatusBar_UpdateTextString", UpdateHealthTextColor)
end

Some files were not shown because too many files have changed in this diff Show More