Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc566feaf | ||
|
|
e43d93a524 | ||
|
|
b9912256bf | ||
|
|
40762969a1 | ||
|
|
9101a0b32f | ||
|
|
a81ca54e0c | ||
|
|
224f6db154 | ||
|
|
4f0c80c58e | ||
|
|
eb9faafafd | ||
|
|
662de4fced | ||
|
|
f393a827c7 | ||
|
|
ffa1ceb83b | ||
|
|
8b62382660 | ||
|
|
97ff7a7d84 | ||
|
|
5db1aa9c41 | ||
|
|
d51d2556ef | ||
|
|
48ee0747c3 | ||
|
|
dfe8cea692 | ||
|
|
7da9684025 | ||
|
|
e557dbe058 | ||
|
|
19bc58620b | ||
|
|
0784e2e5bb | ||
|
|
767b458aa6 | ||
|
|
9ef84b50fe | ||
|
|
28da226764 | ||
|
|
b47b2ec06e | ||
|
|
8e47666644 | ||
|
|
338135be34 | ||
|
|
2c58ce2aa3 | ||
|
|
b2c30df0e1 | ||
|
|
3c5f36b67a | ||
|
|
6de8e93496 | ||
|
|
00c6903a81 | ||
|
|
c5b3df38a1 | ||
|
|
15712e82c7 | ||
|
|
f3fd1d5c56 | ||
|
|
0a0cd055c4 | ||
|
|
151abf3280 | ||
|
|
a482808743 | ||
|
|
f5f651936c | ||
|
|
4ef534fabb | ||
|
|
c7d0f9c5f4 | ||
|
|
1ae8398a2d | ||
|
|
e4c7741226 | ||
|
|
d705a9b221 | ||
|
|
48d45f8fe6 |
@@ -0,0 +1,8 @@
|
||||
* text=auto eol=lf
|
||||
*.tga -text
|
||||
*.blp -text
|
||||
*.jpg -text
|
||||
*.jpeg -text
|
||||
*.png -text
|
||||
*.gif -text
|
||||
*.zip -text
|
||||
@@ -0,0 +1,10 @@
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*.rej
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -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.
|
||||
@@ -33,6 +33,10 @@ hashes.
|
||||
| `img/UI-TargetingFrame.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/UI-TargetingFrame.tga` | `c3c56c28ff81f99325a8aa5a8d8c5cd4a9c56a95` |
|
||||
| `img/circleparty.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleparty.tga` | `9634f9ffb07ae4fb047223f83c299a8ba1906dff` |
|
||||
| `img/circleraid.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleraid.tga` | `c8a5c3e4c572ebd4af426d878bdc8db21d758691` |
|
||||
| `img/df-gryphon-beta.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-gryphon-beta.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `bb4262fe7bf811fa7fff958f7f29269b69038280` |
|
||||
| `img/df-gryphon.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-gryphon.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `6c7e965613222b0313dc1d56d9aa81414a828eef` |
|
||||
| `img/df-wyvern.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-wyvern.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `4414fedcec732ea3096c30598ba7ada25a6af585` |
|
||||
| `img/xpbar.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/xpbar.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `de80c16875cd7d9fa96baf99bd6ae2e1514595f7` |
|
||||
|
||||
## Screenshot manifest
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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**.
|
||||
@@ -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
|
||||
TokensWorth/ShaguTweaks-mods material documented in
|
||||
`THIRD_PARTY_NOTICES.md`.
|
||||
- `ShaguTweaks-extras-MIT.txt` — MIT license of ShaguTweaks-extras, for the
|
||||
modules merged from ShaguTweaks-Extras-ClassicAPI documented in
|
||||
`THIRD_PARTY_NOTICES.md`.
|
||||
- `pfUI-MIT.txt` — MIT license for the Swing Timer and Wand Timer ported from
|
||||
brues-code/pfUI, documented in `THIRD_PARTY_NOTICES.md`.
|
||||
- `ClassicAPI-GPL-3.0.txt` — verbatim license document from the external
|
||||
ClassicAPI repository. ClassicAPI is not bundled here.
|
||||
- `SuperWoW-LICENSE.txt` — verbatim upstream license from the external
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,17 +1,22 @@
|
||||
# 🧩 ShaguTweaks — ClassicAPI
|
||||
|
||||
A lightweight fork of [ShaguTweaks](https://github.com/shagu/ShaguTweaks) for **Turtle WoW-like servers**.
|
||||
[](https://github.com/brues-code/ClassicAPI)
|
||||
[](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
|
||||
|
||||
- **[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
|
||||
|
||||
@@ -23,130 +28,245 @@ ClassicAPI is the main API layer. SuperWoW is only used as an additional fallbac
|
||||
|
||||
Settings: **Esc → Advanced Options**.
|
||||
|
||||
## ✨ Main changes
|
||||
## 📷 Screenshots
|
||||
|
||||
|
||||
Personal Resource Display, independently positioned player name and movable action bars with Golden UI.
|
||||
|
||||

|
||||
|
||||
Golden UI world map with the OctoBus Transports panel.
|
||||
|
||||

|
||||
|
||||
Modifier-key previews for movable action bars, cast bars and personal resources.
|
||||
|
||||

|
||||
|
||||
Advanced Options with search and grouped settings.
|
||||
|
||||

|
||||
|
||||
## ✨ 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- **English-only loading:** only English data and translations are included, avoiding unused locale allocations.
|
||||
- Better Turtle WoW-like server compatibility for custom items, spells and UI changes, plus various stability fixes across legacy ShaguTweaks modules.
|
||||
|
||||
## 🔷 Mods using ClassicAPI directly
|
||||
## 🔷 ClassicAPI integration
|
||||
|
||||
- Actionbar Range Color
|
||||
- AoE Loot
|
||||
- Auto Dismount
|
||||
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.
|
||||
|
||||
Modules that use ClassicAPI directly:
|
||||
|
||||
- Range Color
|
||||
- Chat Tweaks
|
||||
- Equip Compare
|
||||
- Free Slots
|
||||
- Free Slot Count
|
||||
- Improved Castbar
|
||||
- Item Rarity Borders
|
||||
- Item Rarity Glows
|
||||
- Loot History
|
||||
- Movable Unit Frames
|
||||
- Real Health Numbers
|
||||
- Nameplate Class Colors
|
||||
- Nameplate Castbar
|
||||
- No Toggle
|
||||
- No Toggle Auto-Attack
|
||||
- Sell Junk
|
||||
- Skip Gossip Text
|
||||
- Enemy Target Castbar
|
||||
- Swing Timer
|
||||
- Wand Timer
|
||||
- Loss of Control Alert
|
||||
- Totem Timers
|
||||
- Enemy Castbars
|
||||
- Tooltip Details
|
||||
- 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
|
||||
|
||||
Names match the in-game options list. Modules marked *(off)* are disabled until you enable them.
|
||||
|
||||
### Action Bar
|
||||
|
||||
- Auto Dismount
|
||||
- Auto Stance
|
||||
- Actionbar Range Color
|
||||
- Auto Stance (enters a required stance/form; to cast Moonfire from Cat Form, enable ClassicAPI Auto Cancel Form instead)
|
||||
- Range Color *(off)*
|
||||
- Cooldown Numbers
|
||||
- Hide Gryphons
|
||||
- Improved Castbar
|
||||
- Hide Gryphons *(off)*
|
||||
- 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
|
||||
- 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
|
||||
|
||||
- Big Health
|
||||
- Class Colors
|
||||
- Class Portraits
|
||||
- Energy & Mana Tick
|
||||
- Health Colors
|
||||
- Unit Frame Big Health for Player *(off)*
|
||||
- Unit Frame Big Health for Target *(off)*
|
||||
- Unit Frame Class Colors *(off)*
|
||||
- Modern Class Colors *(off)*
|
||||
- Unit Frame Class Portraits *(off)*
|
||||
- Unit Frame Energy & Mana Tick *(off)*
|
||||
- Hide Pet Unit Frame *(off)*
|
||||
- Unit Frame Abbreviated Names *(off)*
|
||||
- Movable Unit Frames
|
||||
- Real Health Numbers
|
||||
- Enemy Target Castbar
|
||||
- Target Debuff Timer
|
||||
- Real Health Numbers *(off)*
|
||||
- White Unit Frame Numbers *(off)*
|
||||
- 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
|
||||
|
||||
- Equip Compare
|
||||
- Free Slots
|
||||
- Free Slot Count
|
||||
- Item Rarity Borders
|
||||
- Item Rarity Glows
|
||||
- Item Rarity Glows *(off)*
|
||||
- Sell Junk
|
||||
- Tooltip Details
|
||||
- Vendor Values
|
||||
- Metric Range
|
||||
- Cursor Tooltip *(off)*
|
||||
- Hide Combat Tooltip *(off)*
|
||||
|
||||
### Chat & Social
|
||||
|
||||
- Blue Shaman Class Colors
|
||||
- Blue Shaman Class Colors *(off)*
|
||||
- Chat Tweaks
|
||||
- Chat Levels
|
||||
- Chat Spam Filter
|
||||
- Chat Levels *(off)*
|
||||
- Chat Spam Filter *(off)*
|
||||
- Social Colors
|
||||
- Chat Timestamps *(off)*
|
||||
- Enable Text Shadow *(off)*
|
||||
- Center Text Input Box *(off)*
|
||||
- Block NPC Spam *(off)*
|
||||
- World Chat Hider *(off)*
|
||||
|
||||
### Minimap & World Map
|
||||
|
||||
- Clean Minimap
|
||||
- Minimap Clock
|
||||
- Minimap Square
|
||||
- Minimap Tweaks
|
||||
- Minimap Zoom
|
||||
- Minimap Clean *(off)*
|
||||
- MiniMap Clock
|
||||
- MiniMap Square *(off)*
|
||||
- MiniMap Tweaks
|
||||
- Enlarged Minimap *(off)*
|
||||
- Hide Tracking Icon
|
||||
- WorldMap Class Colors
|
||||
- WorldMap Coordinates
|
||||
- WorldMap Window
|
||||
- Reveal World Map
|
||||
- MiniMap Framerate & Latency *(off)*
|
||||
|
||||
### Loot
|
||||
|
||||
- AoE Loot
|
||||
- Improved Roll Frames
|
||||
- Improved Roll Frames *(off)*
|
||||
- Loot Cursor
|
||||
- Loot History (`/loothistory`)
|
||||
- Loot Quality *(off)*
|
||||
|
||||
### Macro
|
||||
|
||||
- Macro Icons
|
||||
- Macro Tweaks
|
||||
|
||||
### Merchant
|
||||
|
||||
- Buy Em All
|
||||
|
||||
### Interface
|
||||
|
||||
- Darkened UI
|
||||
- Hide Errors
|
||||
- Smaller Error Frame
|
||||
- XP Bar Text
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
- Super WoW Compatibility
|
||||
- Turtle WoW Compatibility
|
||||
- Skip Gossip Text
|
||||
|
||||
## 🔧 Compatibility
|
||||
|
||||
- ClassicAPI integration
|
||||
- Turtle WoW-like server integration
|
||||
- SuperWoW integration
|
||||
- Skip Gossip Text *(off)*
|
||||
|
||||
## 🧹 Removed / integrated modules
|
||||
|
||||
@@ -154,6 +274,7 @@ The following original ShaguTweaks modules are not included as standalone module
|
||||
|
||||
- **Chat Links** → integrated into **Chat Tweaks**
|
||||
- **Movable Unit Frames Extended** → integrated into **Movable Unit Frames**
|
||||
- **ShaguTweaks Extras ClassicAPI** → its modules are built in, except Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy
|
||||
|
||||
## 📜 Project identity & licensing
|
||||
|
||||
@@ -163,10 +284,14 @@ This is an independent community project. Compatibility with **World of Warcraft
|
||||
|
||||
For detailed provenance and licensing information, see:
|
||||
|
||||
- [CHANGELOG.md](CHANGELOG.md)
|
||||
- [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)
|
||||
- [PROJECT_IDENTITY.md](PROJECT_IDENTITY.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
|
||||
|
||||
@@ -176,12 +301,16 @@ 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.
|
||||
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.
|
||||
|
||||
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**.
|
||||
|
||||
@@ -2,30 +2,16 @@
|
||||
## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI]
|
||||
## Author: Shagu
|
||||
## Notes: A collection of tweaks and enhancements to the Default UI
|
||||
## Notes-ruRU: Коллекция настроек и улучшений стандартного пользовательского интерфейса.
|
||||
## Version: GIT
|
||||
## Version: 3.8.7
|
||||
## Dependencies: !!!ClassicAPI
|
||||
## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache
|
||||
## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache
|
||||
|
||||
# locales
|
||||
locales\locales_deDE.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
|
||||
locales\translations_deDE.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
|
||||
main.lua
|
||||
@@ -37,7 +23,6 @@ config.lua
|
||||
libs\libtipscan.lua
|
||||
libs\libspell.lua
|
||||
libs\libcast.lua
|
||||
libs\libdebuff.lua
|
||||
libs\libunitscan.lua
|
||||
libs\libnameplate.lua
|
||||
libs\libpredict.lua
|
||||
@@ -57,23 +42,18 @@ mods\minimap-clean.lua
|
||||
mods\minimap-zoom.lua
|
||||
mods\minimap-tracking.lua
|
||||
mods\actionbar-range-color.lua
|
||||
mods\actionbar-proc-highlights.lua
|
||||
mods\free-slots.lua
|
||||
mods\xpbar-text.lua
|
||||
mods\nameplate-scale.lua
|
||||
mods\sell-junk.lua
|
||||
mods\unitframes-classcolor.lua
|
||||
mods\vendor-values.lua
|
||||
mods\worldmap-coordinates.lua
|
||||
mods\worldmap-window.lua
|
||||
mods\nameplate-classcolor.lua
|
||||
mods\nameplate-castbar.lua
|
||||
mods\move-unitframes.lua
|
||||
mods\target-castbar.lua
|
||||
mods\target-debufftimer.lua
|
||||
mods\social-colors.lua
|
||||
mods\item-colors.lua
|
||||
mods\health-color.lua
|
||||
mods\auto-dismount.lua
|
||||
mods\auto-stance.lua
|
||||
mods\tooltip-details.lua
|
||||
mods\hide-errors.lua
|
||||
@@ -84,20 +64,67 @@ mods\hide-gryphons.lua
|
||||
mods\worldmap-colors.lua
|
||||
mods\unitframes-bighealth.lua
|
||||
mods\health-numbers.lua
|
||||
mods\unitframes-healing-prediction.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-hide-pet.lua
|
||||
mods\cooldown-numbers.lua
|
||||
mods\turtle-wow.lua
|
||||
mods\superwow.lua
|
||||
mods\reduced-actionbar.lua
|
||||
mods\actionbar-move.lua
|
||||
mods\actionbar-grid.lua
|
||||
mods\no-toggle.lua
|
||||
mods\aoe-loot.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-castbar.lua
|
||||
mods\castbar-move.lua
|
||||
mods\swing-timer.lua
|
||||
mods\smaller-errors-frame.lua
|
||||
mods\skip-gossip.lua
|
||||
|
||||
# modules merged from ShaguTweaks Extras ClassicAPI (Dusk)
|
||||
# action bar
|
||||
mods\actionbar-df-gryphon.lua
|
||||
mods\actionbar-right-center.lua
|
||||
mods\actionbar-reagents.lua
|
||||
mods\actionbar-float.lua
|
||||
mods\reduced-actionbar-bags.lua
|
||||
mods\reduced-actionbar-micromenu.lua
|
||||
mods\actionbar-mouseover-common.lua
|
||||
mods\actionbar-mouseover-bar-right.lua
|
||||
mods\actionbar-mouseover-bar-right2.lua
|
||||
mods\actionbar-hide-macro.lua
|
||||
# merchant
|
||||
mods\buy-em-all.lua
|
||||
# loot
|
||||
mods\loot-quality.lua
|
||||
# tooltip & items
|
||||
mods\metric-range.lua
|
||||
mods\cursor-tooltip.lua
|
||||
mods\hide-combat-tooltip.lua
|
||||
# unit frames
|
||||
mods\unitframes-abbrev-names.lua
|
||||
# minimap & world map
|
||||
mods\minimap-framerate-latency.lua
|
||||
mods\worldmap-reveal.lua
|
||||
# macro
|
||||
mods\macro-icons.lua
|
||||
mods\macro-tweaks.lua
|
||||
# chat & social
|
||||
mods\chat-filter-common.lua
|
||||
mods\chat-npc-spam.lua
|
||||
mods\world-chat-hider.lua
|
||||
mods\chat-input-center.lua
|
||||
mods\chat-text-shadow.lua
|
||||
mods\chat-timestamps.lua
|
||||
|
||||
# compatibility
|
||||
compat-dragonflightui.lua
|
||||
compat-guda.lua
|
||||
|
||||
@@ -33,9 +33,74 @@ from `TokensWorth/ShaguTweaks-mods`:
|
||||
- Copyright (c) 2022 GryllsAddons
|
||||
- License: MIT
|
||||
|
||||
The following modules, merged from ShaguTweaks-Extras-ClassicAPI, also
|
||||
contain code adapted from `TokensWorth/ShaguTweaks-mods` (as recorded by that
|
||||
project):
|
||||
|
||||
- `mods/actionbar-mouseover-common.lua`
|
||||
- `mods/actionbar-mouseover-bar-right.lua`
|
||||
- `mods/actionbar-mouseover-bar-right2.lua`
|
||||
- `mods/actionbar-hide-macro.lua`
|
||||
- `mods/unitframes-abbrev-names.lua`
|
||||
- `mods/cursor-tooltip.lua`
|
||||
- `mods/hide-combat-tooltip.lua`
|
||||
- `mods/minimap-framerate-latency.lua`
|
||||
- `mods/chat-npc-spam.lua`
|
||||
- `mods/world-chat-hider.lua`
|
||||
- `mods/loot-quality.lua`
|
||||
|
||||
The preserved license text is stored in
|
||||
`LICENSES/ShaguTweaks-mods-MIT.txt`.
|
||||
|
||||
## ShaguTweaks-Extras-ClassicAPI
|
||||
|
||||
These modules were merged from Dusk-92's ShaguTweaks-Extras-ClassicAPI
|
||||
(commit `8e14bb4`, 2026-09-18), itself a fork of paokkerkir's
|
||||
ShaguTweaks-extras:
|
||||
|
||||
- Action bar: `actionbar-df-gryphon.lua`, `actionbar-right-center.lua`,
|
||||
`actionbar-reagents.lua`, `actionbar-float.lua`,
|
||||
`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 is a required external runtime dependency used by
|
||||
@@ -52,12 +117,13 @@ ClassicAPI binaries or source are distributed as part of this addon.
|
||||
|
||||
## AoELoot
|
||||
|
||||
`mods/aoe-loot.lua` is adapted from the standalone AoELoot addon provided to
|
||||
the project by Sandrea:
|
||||
Before 3.8.6, `mods/aoe-loot.lua` was adapted from the standalone AoELoot
|
||||
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 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
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -7,6 +7,16 @@ ShaguTweaks.API = ShaguTweaks.API or {}
|
||||
local API = ShaguTweaks.API
|
||||
|
||||
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"
|
||||
or type(_G.C_Spell) == "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"
|
||||
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.iteminfo = API.items and type(_G.C_Item.GetItemInfo) == "function"
|
||||
API.iteminfoinstant = API.items
|
||||
@@ -80,6 +87,19 @@ API.eventutils = type(_G.C_EventUtils) == "table"
|
||||
API.timer = type(_G.C_Timer) == "table"
|
||||
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
|
||||
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")
|
||||
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"
|
||||
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.unittoken = type(_G.UnitTokenFromGUID) == "function"
|
||||
API.tooltipunit = _G.GameTooltip
|
||||
@@ -150,6 +160,10 @@ API.tooltipsetitem = API.tooltipitem
|
||||
API.unitrange = type(_G.UnitInRange) == "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"
|
||||
and type(_G.StopAttack) == "function"
|
||||
API.focus = type(_G.FocusUnit) == "function"
|
||||
@@ -225,6 +239,24 @@ API.GetChannelInfo = function(unit)
|
||||
isTradeSkill, notInterruptible, spellID, true
|
||||
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)
|
||||
if API.spellrange then
|
||||
return _G.C_Spell.IsSpellInRange(spell, unit)
|
||||
@@ -373,6 +405,38 @@ API.Defer = function(callback)
|
||||
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)
|
||||
if API.regionmouseover and region then
|
||||
return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset)
|
||||
@@ -388,30 +452,6 @@ API.GetContainerNumFreeSlots = function(bag)
|
||||
return 0, 0
|
||||
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
|
||||
-- MODIFIER_STATE_CHANGED. Prefer that state over vanilla's merged Win32 key
|
||||
-- state when the richer ClassicAPI functions are available.
|
||||
@@ -451,6 +491,12 @@ API.IsAltKeyDown = function()
|
||||
return false
|
||||
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
|
||||
-- relying exclusively on legacy item-link parsing.
|
||||
local function GetItemIDFromLink(link)
|
||||
@@ -756,23 +802,6 @@ API.SellAllJunkItems = function()
|
||||
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)
|
||||
if API.unitguid then
|
||||
return _G.UnitGUID(unit)
|
||||
@@ -824,3 +853,34 @@ API.UnitInRange = function(unit)
|
||||
|
||||
return false, false
|
||||
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
|
||||
|
||||
@@ -98,14 +98,18 @@ Block("MiniMap Clock")
|
||||
Block("MiniMap Tweaks")
|
||||
Block("MiniMap Square")
|
||||
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
|
||||
-- health/power text. Keep the real ShaguTweaks modules disabled there even
|
||||
-- though Real Health Numbers is once again a genuine core module.
|
||||
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("Unit Frame Class Colors")
|
||||
Block("Unit Frame Health Colors")
|
||||
Block("Unit Frame Class Portraits")
|
||||
Block("Enemy Castbars")
|
||||
|
||||
|
||||
@@ -4,6 +4,19 @@ local GetExpansion = ShaguTweaks.GetExpansion
|
||||
local mod = math.mod or mod
|
||||
|
||||
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_height = 680
|
||||
|
||||
@@ -12,6 +25,7 @@ settings:Hide()
|
||||
|
||||
table.insert(UISpecialFrames, "AdvancedSettingsGUI")
|
||||
settings:SetScript("OnHide", function()
|
||||
if settings.search then settings.search:ClearFocus() end
|
||||
ShowUIPanel(GameMenuFrame)
|
||||
UpdateMicroButtons()
|
||||
end)
|
||||
@@ -34,10 +48,14 @@ settings:SetBackdrop({
|
||||
|
||||
settings.scrollframe = CreateFrame('ScrollFrame', 'AdvancedSettingsGUIScrollframe', settings, 'UIPanelScrollFrameTemplate')
|
||||
settings.scrollframe:SetWidth(max_width - 50)
|
||||
settings.scrollframe:SetPoint('CENTER', settings, -16, 15)
|
||||
settings.scrollframe:SetPoint('CENTER', settings, -16, -21)
|
||||
settings.scrollframe:Hide()
|
||||
|
||||
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:SetPoint("TOP", settings, "TOP", 0, 12)
|
||||
@@ -109,25 +127,181 @@ settings.defaults:SetScript("OnClick", function()
|
||||
settings:defaults()
|
||||
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)
|
||||
-- never use more than 3/4 of the screen size
|
||||
max_height = math.min(UIParent:GetHeight()/UIParent:GetScale()*0.75, 680)
|
||||
|
||||
-- update window sizing according to screen
|
||||
settings:SetHeight(max_height)
|
||||
settings.scrollframe:SetHeight(max_height - 80)
|
||||
settings.scrollframe:SetHeight(max_height - 152)
|
||||
settings.container:SetHeight(max_height - 30)
|
||||
|
||||
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()
|
||||
|
||||
-- sort all configs into categories
|
||||
local gui = {}
|
||||
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"]
|
||||
gui[category] = gui[category] or {}
|
||||
gui[category][title] = module
|
||||
if MatchesSearch(module, title, category, settings.query or "") then
|
||||
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
|
||||
|
||||
@@ -149,6 +323,9 @@ settings.load = function(self)
|
||||
-- add category background
|
||||
settings.category = settings.category or {}
|
||||
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
|
||||
settings.category[category]:SetPoint("TOPLEFT", settings.container, "TOPLEFT", spacing, -spacing - topspace)
|
||||
@@ -196,6 +373,7 @@ settings.load = function(self)
|
||||
end
|
||||
|
||||
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:SetWidth(22)
|
||||
settings.category[category].button:SetHeight(22)
|
||||
@@ -205,6 +383,7 @@ settings.load = function(self)
|
||||
end)
|
||||
|
||||
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:SetWidth(200)
|
||||
settings.category[category].title:SetHeight(entrysize)
|
||||
@@ -225,7 +404,12 @@ settings.load = function(self)
|
||||
settings.category[category]:SetBackdropBorderColor(.2,.2,.2,1)
|
||||
else
|
||||
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
|
||||
|
||||
-- add category title
|
||||
@@ -234,7 +418,19 @@ settings.load = function(self)
|
||||
settings.category[category].text:SetAllPoints()
|
||||
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
|
||||
settings.entries[title] = CreateFrame("CheckButton", "AdvancedSettingsGUI" .. title, settings.category[category], "OptionsCheckButtonTemplate")
|
||||
settings.entries[title]:SetHeight(24)
|
||||
@@ -248,6 +444,8 @@ settings.load = function(self)
|
||||
settings.category[category].buttons[button] = true
|
||||
|
||||
button.title = title
|
||||
button:Show()
|
||||
button:ClearAllPoints()
|
||||
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)
|
||||
|
||||
@@ -268,11 +466,16 @@ settings.load = function(self)
|
||||
end)
|
||||
|
||||
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
|
||||
current_config[this.title] = 1
|
||||
else
|
||||
current_config[this.title] = 0
|
||||
end
|
||||
settings:UpdateOptionAvailability()
|
||||
end)
|
||||
text:SetText(title)
|
||||
entry = entry + 1
|
||||
@@ -291,6 +494,8 @@ settings.load = function(self)
|
||||
end
|
||||
|
||||
local slider = settings.entries[sliderKey]
|
||||
slider:Show()
|
||||
slider:ClearAllPoints()
|
||||
settings.category[category].buttons[slider] = true
|
||||
|
||||
local yOffset = -(height + spacing / 2 + 16 + (sidx - 1) * sliderHeight)
|
||||
@@ -333,22 +538,32 @@ settings.load = function(self)
|
||||
required_height = required_height + height + spacing
|
||||
end
|
||||
|
||||
settings:UpdateOptionAvailability()
|
||||
-- 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)
|
||||
|
||||
if required_height < max_height then
|
||||
if required_height + 132 <= max_height then
|
||||
-- reduce base frame if possible
|
||||
settings:SetHeight(required_height + 60)
|
||||
settings:SetHeight(required_height + 132)
|
||||
settings.container:SetParent(settings)
|
||||
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.scrollframe:Hide()
|
||||
elseif required_height > max_height then
|
||||
else
|
||||
-- set up scrollframe when needed
|
||||
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.scrollframe:SetScrollChild(settings.container)
|
||||
@@ -380,6 +595,8 @@ settings:SetScript("OnShow", function()
|
||||
for k, v in pairs(ShaguTweaks_config) do
|
||||
current_config[k] = v
|
||||
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
|
||||
current_config.overwrites = {}
|
||||
for k, v in pairs(ShaguTweaks_config.overwrites or {}) do
|
||||
|
||||
@@ -76,18 +76,57 @@ ShaguTweaks.HookScript = function(f, script, func)
|
||||
end)
|
||||
end
|
||||
|
||||
ShaguTweaks.HookAddonOrVariable = function(addon, func)
|
||||
local lurker = CreateFrame("Frame", nil)
|
||||
lurker.func = func
|
||||
lurker:RegisterEvent("ADDON_LOADED")
|
||||
lurker:RegisterEvent("VARIABLES_LOADED")
|
||||
lurker:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
lurker:SetScript("OnEvent",function()
|
||||
if IsAddOnLoaded(addon) or _G[addon] then
|
||||
this:func()
|
||||
this:UnregisterAllEvents()
|
||||
-- One event listener for deferred addon/global initialization. Deliberately
|
||||
-- retain the old three triggers rather than invoking already-ready hooks inline.
|
||||
do
|
||||
local listener, draining
|
||||
local pending, ready, checked = {}, {}, {}
|
||||
local function Drain()
|
||||
if draining then return end
|
||||
draining = true
|
||||
local previousThis, previousEvent = this, event
|
||||
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)
|
||||
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
|
||||
|
||||
ShaguTweaks.hooksecurefunc = function(tbl, name, func, prepend)
|
||||
@@ -452,4 +491,4 @@ ShaguTweaks.GetItemLinkByName = function(name)
|
||||
return CacheItemLink(name, hyperLink)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -1,14 +1,10 @@
|
||||
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 libtipscan = ShaguTweaks.libtipscan
|
||||
local hooksecurefunc = ShaguTweaks.hooksecurefunc
|
||||
local cmatch = ShaguTweaks.cmatch
|
||||
|
||||
-- return instantly if we're not on a vanilla client
|
||||
if GetExpansion() ~= "vanilla" then return end
|
||||
if ShaguTweaks.GetExpansion() ~= "vanilla" then return end
|
||||
|
||||
local valid_units = {}
|
||||
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["raidpet" .. i .. "target"] = true end
|
||||
|
||||
local lastcasttex, lastrank, _
|
||||
local scanner
|
||||
local function GetScanner()
|
||||
if not scanner then
|
||||
scanner = libtipscan:GetScanner("libcast")
|
||||
end
|
||||
return scanner
|
||||
end
|
||||
|
||||
valid_units.focus = true
|
||||
valid_units.focustarget = true
|
||||
local libcast = CreateFrame("Frame", "ShaguTweaksEnemyCast")
|
||||
ShaguTweaks.libcast = libcast
|
||||
local player = UnitName("player")
|
||||
local _, playerClass = UnitClass("player")
|
||||
local useClassicCasts = API and API.casts
|
||||
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)
|
||||
-- ClassicAPI is authoritative for real unit tokens. Fall through to the
|
||||
-- legacy database only for the player's custom Hunter cast emulation or for
|
||||
-- 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
|
||||
function libcast:Prune(now)
|
||||
for key, cast in pairs(self.db) do
|
||||
if key ~= player and (not cast.start or not cast.casttime or cast.start + cast.casttime/1000 <= now) then self.db[key] = nil 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 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
|
||||
local function ReadCached(key, channel)
|
||||
local cast = key and libcast.db[key]
|
||||
if not cast or not cast.cast or not cast.start or not cast.casttime then return end
|
||||
if cast.start + cast.casttime/1000 <= GetTime() then
|
||||
if key == player then cast.cast = nil else libcast.db[key] = nil end
|
||||
return
|
||||
end
|
||||
|
||||
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
|
||||
if (cast.channel and true or false) ~= channel then return end
|
||||
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
|
||||
|
||||
ShaguTweaks.UnitCastingInfo = _G.UnitCastingInfo or function(unit)
|
||||
if valid_units[unit] and useClassicCasts then
|
||||
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill =
|
||||
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")
|
||||
-- No ClassicAPI remote event mirror or combat-text parser. Official castbars
|
||||
-- query ClassicAPI directly; optional SuperWoW owns timed GUID records only.
|
||||
if needsCustomHunter then
|
||||
libcast:RegisterEvent("SPELLCAST_STOP")
|
||||
libcast:RegisterEvent("SPELLCAST_FAILED")
|
||||
libcast:RegisterEvent("SPELLCAST_INTERRUPTED")
|
||||
libcast:RegisterEvent("SPELLCAST_DELAYED")
|
||||
libcast:RegisterEvent("SPELLCAST_CHANNEL_START")
|
||||
libcast:RegisterEvent("SPELLCAST_CHANNEL_STOP")
|
||||
libcast:RegisterEvent("SPELLCAST_CHANNEL_UPDATE")
|
||||
elseif needsCustomHunter then
|
||||
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)
|
||||
|
||||
libcast:SetScript("OnEvent", function()
|
||||
local cast = libcast.db[player]
|
||||
if cast then cast.cast = nil end
|
||||
lastrank = nil
|
||||
end)
|
||||
--[[ Custom Casts
|
||||
Enable Castbars for spells that don't have a castbar by default
|
||||
(e.g Multi-Shot and Aimed Shot)
|
||||
@@ -508,15 +200,10 @@ local function CastCustom(spell)
|
||||
end
|
||||
end
|
||||
|
||||
if not useClassicCasts or needsCustomHunter then
|
||||
hooksecurefunc("UseContainerItem", function(id, index)
|
||||
lastcasttex = GetContainerItemInfo(id, index)
|
||||
end)
|
||||
|
||||
hooksecurefunc("CastSpell", function(id, bookType)
|
||||
local spellName, rank = GetSpellName(id, bookType)
|
||||
if not GetCustomCast(spellName) then return end
|
||||
lastrank = rank
|
||||
lastcasttex = GetSpellTexture(id, bookType)
|
||||
|
||||
if GetSpellCooldown(id, bookType) ~= 0 then
|
||||
CastCustom(spellName)
|
||||
@@ -555,12 +242,11 @@ hooksecurefunc("UseAction", function(slot, target, button)
|
||||
-- CastSpellByName. Do not guess it from the macro's first cast line.
|
||||
return
|
||||
elseif actionType == "item" then
|
||||
lastcasttex = GetActionTexture(slot)
|
||||
lastrank = nil
|
||||
return
|
||||
elseif actionType == "spell" and actionID then
|
||||
local spellName, rank, icon = API.GetSpellInfo(actionID)
|
||||
lastcasttex = icon or GetActionTexture(slot)
|
||||
local spellName, rank = API.GetSpellInfo(actionID)
|
||||
if not GetCustomCast(spellName) then return end
|
||||
lastrank = rank
|
||||
if not IsCurrentAction(slot) then return end
|
||||
CastCustom(spellName)
|
||||
@@ -575,10 +261,7 @@ hooksecurefunc("UseAction", function(slot, target, button)
|
||||
tip:SetAction(slot)
|
||||
local spellName, rank = tip:Line(1)
|
||||
|
||||
lastcasttex = GetActionTexture(slot)
|
||||
lastrank = rank
|
||||
CastCustom(spellName)
|
||||
end)
|
||||
end
|
||||
|
||||
ShaguTweaks.libcast = libcast
|
||||
@@ -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
|
||||
@@ -39,8 +39,8 @@ local function StopSampling()
|
||||
end
|
||||
|
||||
local function StartSampling()
|
||||
libhealth:RegisterEvent("UNIT_COMBAT")
|
||||
libhealth:RegisterEvent("UNIT_HEALTH")
|
||||
ShaguTweaks.API.RegisterUnitEvent(libhealth, "UNIT_COMBAT", "target")
|
||||
ShaguTweaks.API.RegisterUnitEvent(libhealth, "UNIT_HEALTH", "target")
|
||||
end
|
||||
|
||||
local function SelectTarget()
|
||||
@@ -76,27 +76,8 @@ local function GetCachedHealth(unitstr, cur, max)
|
||||
return cur, max
|
||||
end
|
||||
|
||||
-- ClassicAPI can expose the real health deficit even when stock UnitHealth()
|
||||
-- still reports a percentage. Use that as an immediate estimate when possible.
|
||||
-- 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
|
||||
|
||||
-- UnitHealthMissing follows the same units as UnitHealth/UnitHealthMax.
|
||||
-- A percentage deficit cannot establish absolute HP; use the combat cache.
|
||||
function libhealth:GetUnitHealth(unitstr)
|
||||
-- Preserve the old public library behavior for external consumers while
|
||||
-- keeping the estimator lazy: the first actual query activates it.
|
||||
@@ -112,12 +93,6 @@ function libhealth:GetUnitHealth(unitstr)
|
||||
return cur, max, true
|
||||
end
|
||||
|
||||
local missingCur, missingMax, missingKnown =
|
||||
GetMissingHealthEstimate(unitstr, cur, max)
|
||||
if missingKnown then
|
||||
return missingCur, missingMax, true
|
||||
end
|
||||
|
||||
return GetCachedHealth(unitstr, cur, max)
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ local function IsNamePlate(frame)
|
||||
end
|
||||
|
||||
local registry = {}
|
||||
local lastParentCount = -1
|
||||
local libnameplate = CreateFrame("Frame", nil, UIParent)
|
||||
ShaguTweaks.libnameplate = libnameplate
|
||||
libnameplate.OnInit = {}
|
||||
@@ -78,13 +77,6 @@ local function DiscoverNameplates()
|
||||
InitializePlate(children[index])
|
||||
end
|
||||
|
||||
lastParentCount = count
|
||||
end
|
||||
|
||||
local function ScanNameplatesLegacy()
|
||||
local count = WorldFrame:GetNumChildren()
|
||||
if count == lastParentCount then return end
|
||||
DiscoverNameplates()
|
||||
end
|
||||
|
||||
local eventDriver = CreateFrame("Frame")
|
||||
@@ -110,10 +102,9 @@ function libnameplate:HasConsumers()
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
if HasClassicNameplateEvents() then
|
||||
-- Never enumerate WorldFrame children synchronously from the ClassicAPI
|
||||
-- nameplate event path. Defer discovery to the next rendered frame.
|
||||
ScheduleDiscovery()
|
||||
@@ -124,9 +115,7 @@ function libnameplate:Enable()
|
||||
end)
|
||||
|
||||
self:SetScript("OnUpdate", nil)
|
||||
else
|
||||
self:SetScript("OnUpdate", ScanNameplatesLegacy)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function libnameplate:EnableIfNeeded()
|
||||
|
||||
@@ -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 L = ShaguTweaks.L
|
||||
local API = ShaguTweaks.API
|
||||
local GetExpansion = ShaguTweaks.GetExpansion
|
||||
local libtipscan = ShaguTweaks.libtipscan
|
||||
local libspell = ShaguTweaks.libspell
|
||||
local hooksecurefunc = ShaguTweaks.hooksecurefunc
|
||||
local strsplit = ShaguTweaks.strsplit
|
||||
local cmatch = ShaguTweaks.cmatch
|
||||
|
||||
local senttarget
|
||||
local heals, ress, events = {}, {}, {}
|
||||
|
||||
local PRAYER_OF_HEALING
|
||||
do -- Prayer of Healing
|
||||
local locales = {
|
||||
["deDE"] = "Gebet der Heilung",
|
||||
["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"]
|
||||
local P = { pending = {}, enabled = false }
|
||||
ShaguTweaks.libpredict = P
|
||||
local learned, driver, player, sentGUID, sentTarget, sentSpell, activeGUID
|
||||
local elapsed = 0
|
||||
local equipment, equipmentReady = {}, false
|
||||
local sharedHooks = {}
|
||||
local hotAmounts, hotUnits = {}, {}
|
||||
local trackedUnits = {"player", "target"}
|
||||
local function HotTotal(unit)
|
||||
local state = hotUnits[unit]
|
||||
if not state or state.guid ~= UnitGUID(unit) then return 0 end
|
||||
local total, now = 0, GetTime()
|
||||
for _, hot in ipairs(state.entries) do if hot.expires > now then total = total + hot.amount end end
|
||||
return total
|
||||
end
|
||||
|
||||
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 function HotPending()
|
||||
local now = GetTime()
|
||||
local pending
|
||||
|
||||
for timestamp in pairs(events) do
|
||||
if now >= timestamp then
|
||||
events[timestamp] = nil
|
||||
else
|
||||
pending = true
|
||||
for _, state in pairs(hotUnits) do
|
||||
for _, hot in ipairs(state.entries) do if hot.expires > now then return true end end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function EquipmentChanged()
|
||||
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
|
||||
|
||||
-- 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
|
||||
if not any then driver:SetScript("OnUpdate", nil) end
|
||||
end
|
||||
|
||||
function libpredict:ParseComm(sender, msg)
|
||||
local msgtype, target, heal, time
|
||||
|
||||
if msg == "Healstop" or msg == "GrpHealstop" then
|
||||
msgtype = "Stop"
|
||||
elseif msg == "Resurrection/stop/" then
|
||||
msgtype = "RessStop"
|
||||
elseif msg then
|
||||
local msgobj = {strsplit("/", msg)}
|
||||
|
||||
if msgobj and msgobj[1] and msgobj[2] then
|
||||
-- legacy healcomm object
|
||||
if msgobj[1] == "GrpHealdelay" or msgobj[1] == "Healdelay" then
|
||||
msgtype, time = "Delay", msgobj[2]
|
||||
function P:HealStop(sender)
|
||||
if not sender then return end
|
||||
for target, senders in pairs(self.pending) do
|
||||
if senders[sender] then
|
||||
senders[sender] = nil
|
||||
if not next(senders) then self.pending[target] = nil end
|
||||
Notify(target)
|
||||
end
|
||||
end
|
||||
if driver and not next(self.pending) and not HotPending() then driver:SetScript("OnUpdate", nil) end
|
||||
end
|
||||
function P:Heal(sender, target, amount, milliseconds)
|
||||
amount, milliseconds = tonumber(amount), tonumber(milliseconds)
|
||||
if not self.enabled or self.external or not sender or not target or target == "" or not amount or not milliseconds
|
||||
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
|
||||
|
||||
if msgobj[1] and msgobj[1] == "Resurrection" and msgobj[2] then
|
||||
msgtype, target = "Ress", msgobj[2]
|
||||
end
|
||||
|
||||
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
|
||||
if (not name or not guid) and type(auras.GetAuraDataByIndex) == "function" then
|
||||
local aura = auras.GetAuraDataByIndex(unit, index, "HELPFUL")
|
||||
if aura then
|
||||
name, duration, expires, spell = aura.name, aura.duration, aura.expirationTime, aura.spellId
|
||||
guid = aura.sourceGUID or (aura.sourceUnit and UnitGUID(aura.sourceUnit))
|
||||
end
|
||||
end
|
||||
elseif select and UnitCastingInfo then
|
||||
-- latest healcomm
|
||||
msgtype = tonumber(string.sub(msg, 1, 3))
|
||||
if not msgtype then return end
|
||||
|
||||
if msgtype == 0 then
|
||||
msgtype = "Heal"
|
||||
heal = tonumber(string.sub(msg, 4, 8))
|
||||
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
|
||||
if not name then break end
|
||||
local byCaster = guid and hotAmounts[guid]
|
||||
local amount = byCaster and byCaster[spell]
|
||||
if amount and expires and expires > now then
|
||||
count = count + 1
|
||||
local hot = state.entries[count] or {}
|
||||
state.entries[count] = hot
|
||||
hot.amount, hot.expires = amount, expires
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return msgtype, target, heal, time
|
||||
for index = table.getn(state.entries), count+1, -1 do state.entries[index] = nil end
|
||||
-- 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
|
||||
|
||||
function libpredict:ParseChatMessage(sender, msg, comm)
|
||||
local msgtype, target, heal, time
|
||||
|
||||
if comm == "HealComm" then
|
||||
msgtype, target, heal, time = libpredict:ParseComm(sender, msg)
|
||||
elseif comm == "CTRA" then
|
||||
local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)")
|
||||
if cmd == "RES" and ctratarget and ctratarget ~= "" and ctratarget ~= UNKNOWN then
|
||||
msgtype = "Ress"
|
||||
target = ctratarget
|
||||
elseif cmd == "RESNO" then
|
||||
msgtype = "RessStop"
|
||||
end
|
||||
end
|
||||
|
||||
if msgtype == "Stop" and sender then
|
||||
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)
|
||||
local function TryShared()
|
||||
local shared = _G.PTHealPredict
|
||||
if type(shared) ~= "table" or type(shared.GetIncomingHealing) ~= "function"
|
||||
or type(shared.HookUpdates) ~= "function" then return false end
|
||||
-- Puppeteer uses SuperWoW GUIDs; never hand it incompatible identifiers.
|
||||
local _, guid = UnitExists("player")
|
||||
if not guid or guid ~= UnitGUID("player") then return false end
|
||||
P.external, P.source = shared, "Puppeteer"
|
||||
learned, activeGUID, sentGUID, sentTarget, sentSpell = nil, nil, nil, nil, nil
|
||||
if driver then driver:UnregisterAllEvents(); driver:SetScript("OnUpdate", nil) end
|
||||
for name in pairs(P.pending) do P.pending[name] = nil end
|
||||
hotUnits, hotAmounts = {}, {}
|
||||
if not sharedHooks[shared] then
|
||||
shared.HookUpdates(function(changedGUID)
|
||||
if not P.enabled or P.external ~= shared then return end
|
||||
for _, unit in ipairs(trackedUnits) do
|
||||
if UnitGUID(unit) == changedGUID then Notify(UnitName(unit)) end
|
||||
end
|
||||
else
|
||||
libpredict:Heal(sender, target, heal, time)
|
||||
end)
|
||||
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
|
||||
elseif msgtype == "Ress" then
|
||||
libpredict:Ress(sender, target)
|
||||
end
|
||||
end
|
||||
|
||||
function libpredict:AddEvent(time, target)
|
||||
events[time] = events[time] or {}
|
||||
table.insert(events[time], target)
|
||||
|
||||
if not self.timeoutActive then
|
||||
self.timeoutActive = true
|
||||
self:SetScript("OnUpdate", UpdateTimeouts)
|
||||
end
|
||||
local function Reset()
|
||||
for target in pairs(P.pending) do P.pending[target] = nil; Notify(target) end
|
||||
for unit, state in pairs(hotUnits) do hotUnits[unit] = nil; Notify(state.name) end
|
||||
if driver then driver:SetScript("OnUpdate", nil) end
|
||||
activeGUID, sentGUID, sentTarget, sentSpell = nil, nil, nil, nil
|
||||
end
|
||||
|
||||
function libpredict:Heal(sender, target, amount, duration)
|
||||
if not sender or not target or not amount or not duration then
|
||||
return
|
||||
end
|
||||
|
||||
local timeout = duration/1000 + GetTime()
|
||||
heals[target] = heals[target] or {}
|
||||
heals[target][sender] = { amount, timeout }
|
||||
libpredict:AddEvent(timeout, target)
|
||||
end
|
||||
|
||||
function libpredict:HealStop(sender)
|
||||
for ttarget, t in pairs(heals) do
|
||||
for tsender in pairs(heals[ttarget]) do
|
||||
if sender == tsender then
|
||||
heals[ttarget][tsender] = nil
|
||||
local function OnEvent()
|
||||
if event == "ADDON_LOADED" then
|
||||
TryShared()
|
||||
elseif event == "UNIT_AURA" then
|
||||
RefreshHots(arg1)
|
||||
elseif event == "PLAYER_TARGET_CHANGED" then
|
||||
RefreshHots("target")
|
||||
elseif event == "CHAT_MSG_ADDON" then
|
||||
if arg1 == "HealComm" and (arg3 == "PARTY" or arg3 == "RAID" or arg3 == "BATTLEGROUND") then P:ParseComm(arg4, arg2) end
|
||||
elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then
|
||||
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
|
||||
-- Learn only ticks landing on one of the two displayed units. Keep
|
||||
-- direct and periodic amounts separate (Regrowth uses both).
|
||||
if arg8 == UnitGUID("player") or arg8 == UnitGUID("target") then
|
||||
local amounts = hotAmounts[arg4] or {}
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
function P:Enable()
|
||||
if self.enabled then return end
|
||||
|
||||
self.enabled = true
|
||||
self.sender.enabled = true
|
||||
|
||||
-- incoming HealComm / resurrection tracking
|
||||
self:RegisterEvent("UNIT_HEALTH")
|
||||
self:RegisterEvent("CHAT_MSG_ADDON")
|
||||
self:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
|
||||
-- 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()
|
||||
if TryShared() then return end
|
||||
self.source = "ClassicAPI + HealComm"
|
||||
player = UnitName("player")
|
||||
learned = {} -- Session-only: actual gear/talent/level changes invalidate estimates.
|
||||
if EquipmentChanged() then
|
||||
local guid = UnitGUID("player")
|
||||
if guid then hotAmounts[guid] = nil 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
|
||||
|
||||
ShaguTweaks.libpredict = libpredict
|
||||
@@ -5,7 +5,15 @@ local API = ShaguTweaks.API
|
||||
|
||||
local units = { players = {}, mobs = {} }
|
||||
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)
|
||||
|
||||
@@ -289,21 +297,46 @@ end
|
||||
|
||||
ShaguTweaks.GetPlayerCache = EnsurePlayerCache
|
||||
|
||||
local function EnrichPlayerFromClassicAPI(name)
|
||||
if not name or not API or not API.GetCachedPlayerInfoByName then return end
|
||||
-- Same player name, ignoring case. Compare first so the common exact match
|
||||
-- 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)
|
||||
class = class and class ~= "" and class or nil
|
||||
guid = guid and guid ~= "" and guid or nil
|
||||
if not class and not guid then return end
|
||||
-- New player records carry a timestamp from the start, so the login cleanup
|
||||
-- can age every entry, not only chat-only ones.
|
||||
local function GetOrCreatePlayer(name)
|
||||
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
|
||||
-- duplicate SavedVariable entries if another cache normalizes name casing.
|
||||
units.players[name] = units.players[name] or {}
|
||||
units.players[name].class = class or units.players[name].class
|
||||
units.players[name].guid = guid or units.players[name].guid
|
||||
-- Store a class. A GUID is kept only while the class is unknown: it is then
|
||||
-- the one way to learn it later through GetPlayerInfoByGUID. Known players
|
||||
-- need no GUID in SavedVariables, and nothing else reads it.
|
||||
local function SetPlayerClass(data, class, 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
|
||||
|
||||
local function EnrichPlayerFromCurrentChat(name)
|
||||
@@ -316,14 +349,12 @@ local function EnrichPlayerFromCurrentChat(name)
|
||||
local guid = API.GetCurrentChatGUID()
|
||||
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
|
||||
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 {}
|
||||
units.players[name].class = class
|
||||
units.players[name].guid = guid
|
||||
SetPlayerClass(GetOrCreatePlayer(name), class, guid)
|
||||
|
||||
return name
|
||||
end
|
||||
@@ -333,21 +364,41 @@ function ShaguTweaks.GetUnitData(name, active)
|
||||
|
||||
EnsurePlayerCache()
|
||||
|
||||
if not units["players"][name] then
|
||||
EnrichPlayerFromClassicAPI(name)
|
||||
if not units["players"][name] then
|
||||
EnrichPlayerFromCurrentChat(name)
|
||||
local data = units.players[name]
|
||||
|
||||
if (not data or not data.class) and not EnrichPlayerFromStoredGUID(name, data) then
|
||||
-- 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
|
||||
elseif not units["players"][name].class then
|
||||
EnrichPlayerFromClassicAPI(name)
|
||||
if not units["players"][name].class then
|
||||
|
||||
-- Persistent ClassicAPI data can answer class-only queries without making
|
||||
-- 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)
|
||||
data = units.players[name]
|
||||
end
|
||||
end
|
||||
|
||||
if units["players"][name] then
|
||||
local ret = units["players"][name]
|
||||
return ret.class, ret.level, ret.elite, true
|
||||
if data then
|
||||
return data.class, data.level, data.elite, true
|
||||
end
|
||||
|
||||
if units["mobs"][name] then
|
||||
@@ -359,13 +410,28 @@ function ShaguTweaks.GetUnitData(name, active)
|
||||
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
|
||||
units[db][name] = units[db][name] or {}
|
||||
units[db][name].class = class or units[db][name].class
|
||||
units[db][name].level = level or units[db][name].level
|
||||
units[db][name].elite = elite or units[db][name].elite
|
||||
units[db][name].guid = guid or units[db][name].guid
|
||||
|
||||
local data = units[db][name]
|
||||
if not data then
|
||||
data = {}
|
||||
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
|
||||
end
|
||||
|
||||
@@ -374,6 +440,17 @@ local function RememberPlayer(name)
|
||||
|
||||
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.
|
||||
-- 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.
|
||||
@@ -386,26 +463,70 @@ local function RememberPlayer(name)
|
||||
class = apiClass and apiClass ~= "" and apiClass or nil
|
||||
end
|
||||
|
||||
units.players[name] = units.players[name] or {}
|
||||
units.players[name].class = class or units.players[name].class
|
||||
units.players[name].guid = guid or units.players[name].guid
|
||||
local now = time()
|
||||
units.players[name].lastseen = now
|
||||
units.players[name].lastseen_ts = now
|
||||
-- Skip only NEW chat-only records after native persistent storage succeeds.
|
||||
-- Existing records may contain levels/history and must retain timestamps.
|
||||
if not data and API and API.RememberPlayerInfo
|
||||
and API.RememberPlayerInfo(name, class, guid) then return end
|
||||
|
||||
data = GetOrCreatePlayer(name)
|
||||
SetPlayerClass(data, class, guid)
|
||||
data.lastseen = now
|
||||
data.lastseen_ts = now
|
||||
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()
|
||||
|
||||
for name, data in pairs(units.players) do
|
||||
-- Normalize any empty legacy/API class token back to an unknown value.
|
||||
if data and data.class == "" then data.class = nil end
|
||||
-- Entries written before every sighting carried a timestamp have no age.
|
||||
-- Date them from the cache's grace start, as the old Social Colors cleanup
|
||||
-- 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
|
||||
-- entries expire, and only when they carry our numeric last-seen timestamp.
|
||||
if data and not data.class and data.lastseen_ts and
|
||||
now - data.lastseen_ts > PASSIVE_CACHE_TTL then
|
||||
units.players[name] = nil
|
||||
local count = 0
|
||||
for name, data in pairs(players) do
|
||||
if type(data) ~= "table" then
|
||||
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
|
||||
@@ -428,6 +549,15 @@ local passiveChatEvents = {
|
||||
}
|
||||
|
||||
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_LOGOUT")
|
||||
@@ -446,6 +576,9 @@ for chatEvent in pairs(passiveChatEvents) do
|
||||
end
|
||||
|
||||
libunitscan:SetScript("OnEvent", function()
|
||||
local event = event
|
||||
local chatEvent, _, chatSender = API.GetCurrentChatEvent()
|
||||
if chatEvent then event = chatEvent end
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
-- SavedVariables are written after this event. Re-run the migration here so
|
||||
-- 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
|
||||
-- it earlier while restoring UI state. From here onward SavedVariables are
|
||||
-- settled, so future cache reads can skip migration checks completely.
|
||||
EnsurePlayerCache()
|
||||
local _, realmdb = EnsurePlayerCache()
|
||||
cacheMigrationFinalized = true
|
||||
|
||||
-- One cheap cleanup pass at login. Known classes are kept forever.
|
||||
PrunePassivePlayers()
|
||||
-- PLAYER_ENTERING_WORLD also fires at every loading screen. Expire and
|
||||
-- cap the cache once per session only.
|
||||
if not prunedThisSession then
|
||||
prunedThisSession = true
|
||||
PrunePlayers(realmdb)
|
||||
end
|
||||
|
||||
-- update own character details
|
||||
local name = UnitName("player")
|
||||
local _, class = UnitClass("player")
|
||||
local level = UnitLevel("player")
|
||||
local guid = API and API.UnitGUID and API.UnitGUID("player")
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless("player", class))
|
||||
|
||||
elseif event == "NAME_PLATE_UNIT_ADDED" then
|
||||
-- ClassicAPI exposes an exact nameplateN unit token here. Cache players
|
||||
@@ -481,59 +617,70 @@ libunitscan:SetScript("OnEvent", function()
|
||||
local name = UnitName(unit)
|
||||
local _, class = UnitClass(unit)
|
||||
local level = UnitLevel(unit)
|
||||
local guid = API and API.UnitGUID and API.UnitGUID(unit)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless(unit, class))
|
||||
end
|
||||
|
||||
elseif passiveChatEvents[event] then
|
||||
-- Passive cache only: remember speakers already delivered by the client.
|
||||
-- No /who, no TargetByName queue, no timer and no additional server query.
|
||||
RememberPlayer(arg2)
|
||||
RememberPlayer(chatSender or arg2)
|
||||
|
||||
elseif event == "FRIENDLIST_UPDATE" then
|
||||
local name, class, level
|
||||
local now = time()
|
||||
for i = 1, GetNumFriends() do
|
||||
name, level, class = GetFriendInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
-- 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
|
||||
AddData("players", name, class, level)
|
||||
AddData("players", name, class, level, nil, nil, now)
|
||||
end
|
||||
|
||||
elseif event == "GUILD_ROSTER_UPDATE" then
|
||||
local name, class, level, _
|
||||
for i = 1, GetNumGuildMembers() do
|
||||
name, _, _, level, class = GetGuildRosterInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
AddData("players", name, class, level)
|
||||
-- Many addons request the roster every few seconds. A class and level
|
||||
-- cache gains nothing from rescanning a large guild each time, so scan
|
||||
-- at most once a minute unless the member count changed.
|
||||
local count = GetNumGuildMembers()
|
||||
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
|
||||
|
||||
elseif event == "RAID_ROSTER_UPDATE" then
|
||||
local name, class, SubGroup, level, _
|
||||
local now = time()
|
||||
for i = 1, GetNumRaidMembers() do
|
||||
name, _, SubGroup, level, class = GetRaidRosterInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
local guid = API and API.UnitGUID and API.UnitGUID("raid" .. i)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless("raid" .. i, class), now)
|
||||
end
|
||||
|
||||
elseif event == "PARTY_MEMBERS_CHANGED" then
|
||||
local name, class, level, unit, _
|
||||
local now = time()
|
||||
for i = 1, GetNumPartyMembers() do
|
||||
unit = "party" .. i
|
||||
_, class = UnitClass(unit)
|
||||
name = UnitName(unit)
|
||||
level = UnitLevel(unit)
|
||||
local guid = API and API.UnitGUID and API.UnitGUID(unit)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless(unit, class), now)
|
||||
end
|
||||
|
||||
elseif event == "WHO_LIST_UPDATE" then
|
||||
local name, class, level, _
|
||||
local now = time()
|
||||
for i = 1, GetNumWhoResults() do
|
||||
name, _, level, _, class, _ = GetWhoInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
AddData("players", name, class, level)
|
||||
AddData("players", name, class, level, nil, nil, now)
|
||||
end
|
||||
|
||||
elseif event == "UPDATE_MOUSEOVER_UNIT" or event == "PLAYER_TARGET_CHANGED" then
|
||||
@@ -543,8 +690,7 @@ libunitscan:SetScript("OnEvent", function()
|
||||
_, class = UnitClass(scan)
|
||||
level = UnitLevel(scan)
|
||||
name = UnitName(scan)
|
||||
local guid = API and API.UnitGUID and API.UnitGUID(scan)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless(scan, class))
|
||||
else
|
||||
_, class = UnitClass(scan)
|
||||
elite = UnitClassification(scan)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -8,8 +8,6 @@ ShaguTweaks_translation["enUS"] = {
|
||||
["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,
|
||||
@@ -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,
|
||||
["Cursor"] = 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 the remaining duration as text on every cooldown."] = nil,
|
||||
["Draw the mini map in a squared shape instead of a round one."] = nil,
|
||||
@@ -33,8 +32,10 @@ ShaguTweaks_translation["enUS"] = {
|
||||
["General"] = nil,
|
||||
["Hide Errors"] = nil,
|
||||
["Hide Gryphons"] = nil,
|
||||
["Hide Pet Unit Frame"] = 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 Pet unitframe below the player in pet classes."] = 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,
|
||||
@@ -45,9 +46,6 @@ ShaguTweaks_translation["enUS"] = {
|
||||
["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,
|
||||
@@ -62,7 +60,8 @@ ShaguTweaks_translation["enUS"] = {
|
||||
["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 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 the vendor sell values on all item tooltips."] = nil,
|
||||
["Social & Chat"] = nil,
|
||||
@@ -75,7 +74,6 @@ ShaguTweaks_translation["enUS"] = {
|
||||
["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,
|
||||
@@ -83,4 +81,102 @@ ShaguTweaks_translation["enUS"] = {
|
||||
["WorldMap Window"] = nil,
|
||||
["World & MiniMap"] = 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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"] = "Ваш хлам продан, и вы заработали",
|
||||
}
|
||||
@@ -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"] = "你的垃圾物品已出售,你获得了:",
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -43,17 +43,71 @@ ShaguTweaks:RegisterEvent("VARIABLES_LOADED")
|
||||
ShaguTweaks:SetScript("OnEvent", function()
|
||||
-- flag all external modules as unofficial
|
||||
if event == "ADDON_LOADED" then
|
||||
-- Most Extras ClassicAPI modules are built in now. With the old addon
|
||||
-- still enabled, its copies replace the built-in ones (same titles).
|
||||
if arg1 == "ShaguTweaks-extras" then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00ShaguTweaks:|r the Extras modules are now built in. Disable the |cffffcc00ShaguTweaks Extras ClassicAPI|r addon so they only run once.")
|
||||
end
|
||||
|
||||
ShaguTweaks.provider = nil
|
||||
official = false
|
||||
return
|
||||
end
|
||||
|
||||
-- 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()
|
||||
|
||||
-- initialize empty config
|
||||
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
|
||||
ShaguTweaks_config.overwrites = ShaguTweaks_config.overwrites or {}
|
||||
|
||||
@@ -85,6 +139,11 @@ ShaguTweaks:SetScript("OnEvent", function()
|
||||
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
|
||||
-- get one final activation pass after every enabled module has registered
|
||||
-- its callbacks.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -11,39 +11,153 @@ local module = ShaguTweaks:register({
|
||||
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)
|
||||
-- ClassicAPI can resolve a real spell/item action and run the same
|
||||
-- range-only engine test directly against the target unit. This avoids
|
||||
-- relying on the old action-slot heuristic for normal spell buttons.
|
||||
if not UnitExists("target") then return nil end
|
||||
|
||||
if API and API.GetActionInfo then
|
||||
local actionType, id = API.GetActionInfo(action)
|
||||
|
||||
if actionType == "spell" and id and API.IsSpellInRange then
|
||||
local inRange = API.IsSpellInRange(id, "target")
|
||||
if inRange ~= nil then
|
||||
return inRange and 1 or 0
|
||||
end
|
||||
elseif actionType == "item" and id and API.IsItemInRange then
|
||||
local inRange = API.IsItemInRange(id, "target")
|
||||
if inRange ~= nil then
|
||||
return inRange and 1 or 0
|
||||
if actionType == "spell" and id and API.spellrange
|
||||
and API.IsSpellInRange then
|
||||
-- ClassicAPI's check is geometric: it does not reject the wrong
|
||||
-- faction. A friendly buff should not turn red against an enemy.
|
||||
local spells = _G.C_Spell
|
||||
if spells then
|
||||
if type(spells.IsSpellHelpful) == "function"
|
||||
and spells.IsSpellHelpful(id) and type(UnitCanAssist) == "function"
|
||||
and not UnitCanAssist("player", "target") then return nil end
|
||||
if type(spells.IsSpellHarmful) == "function"
|
||||
and spells.IsSpellHarmful(id) and type(UnitCanAttack) == "function"
|
||||
and not UnitCanAttack("player", "target") then return nil 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
|
||||
|
||||
-- Macros, unresolved bag-item actions and older ClassicAPI versions keep
|
||||
-- Vanilla's native action-slot range check as the compatibility fallback.
|
||||
return IsActionInRange(action)
|
||||
-- Macros, unresolved actions and clients without the native capability
|
||||
-- retain the action-slot compatibility fallback.
|
||||
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
|
||||
|
||||
module.enable = function(self)
|
||||
if EnableNativeRange() then return end
|
||||
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
|
||||
this.rangeTimer = this.rangeTimer - elapsed
|
||||
if this.rangeTimer <= 0.2 then
|
||||
local timer = (this.shaguRangeTimer or 0) - (elapsed or 0)
|
||||
if timer > 0 then
|
||||
this.shaguRangeTimer = timer
|
||||
else
|
||||
this.shaguRangeTimer = RANGE_INTERVAL
|
||||
local action = ActionButton_GetPagedID(this)
|
||||
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 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
|
||||
end
|
||||
icon:SetVertexColor(this.r, this.g, this.b, this.a)
|
||||
elseif IsUsableAction(action) then
|
||||
icon:SetVertexColor(1, 1, 1, 1)
|
||||
else
|
||||
-- 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
|
||||
|
||||
this.rangeTimer = TOOLTIP_UPDATE_TIME
|
||||
end
|
||||
end
|
||||
end, true)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -3,7 +3,7 @@ local cmatch = ShaguTweaks.cmatch
|
||||
|
||||
local module = ShaguTweaks:register({
|
||||
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 },
|
||||
category = T["Action Bar"],
|
||||
enabled = true,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -16,11 +16,11 @@ local COOLDOWN = 70
|
||||
-- how often (in seconds) stale entries may be pruned when chat traffic arrives
|
||||
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
|
||||
-- get treated as a duplicate when it also needs to show in another tab
|
||||
-- (e.g. a custom "World" tab)
|
||||
local cache = { {}, {}, INDEX = 1 }
|
||||
local cache = {}
|
||||
local last_cleanup = 0
|
||||
|
||||
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
|
||||
last_cleanup = now
|
||||
|
||||
local index = cache.INDEX
|
||||
local newindex = (index == 1) and 2 or 1
|
||||
cache[newindex] = {}
|
||||
|
||||
for frame, frameCache in pairs(cache[index]) do
|
||||
for frame, frameCache in pairs(cache) do
|
||||
for msg, seen_at in pairs(frameCache) do
|
||||
if (seen_at + COOLDOWN) > now then
|
||||
cache[newindex][frame] = cache[newindex][frame] or {}
|
||||
cache[newindex][frame][msg] = seen_at
|
||||
end
|
||||
if (seen_at + COOLDOWN) <= now then frameCache[msg] = nil end
|
||||
end
|
||||
if not next(frameCache) then cache[frame] = nil end
|
||||
end
|
||||
|
||||
cache[index] = {}
|
||||
cache.INDEX = newindex
|
||||
end
|
||||
|
||||
-- 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()
|
||||
PruneCache(now)
|
||||
|
||||
local index = cache.INDEX
|
||||
local frameCache = cache[index][frame]
|
||||
local frameCache = cache[frame]
|
||||
if not frameCache then
|
||||
frameCache = {}
|
||||
cache[index][frame] = frameCache
|
||||
cache[frame] = frameCache
|
||||
end
|
||||
|
||||
local seen_at = frameCache[msg]
|
||||
@@ -91,24 +81,29 @@ module.enable = function(self)
|
||||
local Original_ChatFrame_OnEvent = ChatFrame_OnEvent
|
||||
|
||||
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
|
||||
if strfind(arg1 or "", "^#showtooltip") then
|
||||
if strfind(message or "", "^#showtooltip") then
|
||||
return
|
||||
end
|
||||
|
||||
-- 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
|
||||
end
|
||||
|
||||
-- hide repeated messages in say/yell/channel chat, and emotes from
|
||||
-- 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 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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -18,6 +18,90 @@ local module = ShaguTweaks:register({
|
||||
-- by one complete clock cycle.
|
||||
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()
|
||||
-- hide frames without parent
|
||||
local parent = this:GetParent()
|
||||
@@ -73,9 +157,7 @@ local function CreateCoolDown(cooldown)
|
||||
cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString(nil, "OVERLAY")
|
||||
|
||||
-- detect dynamic font size
|
||||
local size = parent:GetHeight() or 0
|
||||
size = size > 0 and size * .64 or 12
|
||||
size = size > 14 and 14 or size
|
||||
local size = FontSize(parent)
|
||||
|
||||
-- set fonts
|
||||
cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE")
|
||||
@@ -83,16 +165,55 @@ local function CreateCoolDown(cooldown)
|
||||
cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate)
|
||||
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
|
||||
-- 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
|
||||
this.cooldowntext:Hide()
|
||||
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
|
||||
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
|
||||
if not duration or duration < 2 then
|
||||
-- hide if already existing
|
||||
@@ -108,7 +229,7 @@ local function SetCooldown(this, start, duration, enable)
|
||||
end
|
||||
|
||||
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:Show()
|
||||
else
|
||||
|
||||
@@ -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
|
||||
@@ -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 = {
|
||||
["Solid Texture"] = true,
|
||||
["WHITE8X8"] = true,
|
||||
@@ -53,11 +73,7 @@ local regionskips = {
|
||||
}
|
||||
|
||||
local backgrounds = {
|
||||
["^SpellBookFrame$"] = { 325, 355, 17, -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 = {
|
||||
@@ -81,6 +97,9 @@ local borders = {
|
||||
["ChatFrame(.+)Button"] = -2,
|
||||
["PetFrameHappiness"] = 2,
|
||||
["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 = {
|
||||
@@ -97,6 +116,7 @@ local addonframes = {
|
||||
["Blizzard_RaidUI"] = { "ReadyCheckFrame" },
|
||||
["Blizzard_TradeSkillUI"] = { "TradeSkillFrame" },
|
||||
["Blizzard_TrainerUI"] = { "ClassTrainerFrame" },
|
||||
["Artisan"] = { "ArtisanFrame" },
|
||||
}
|
||||
|
||||
-- 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 }
|
||||
end
|
||||
|
||||
local function IsBlacklisted(texture)
|
||||
local name = texture:GetName()
|
||||
local texture = texture:GetTexture()
|
||||
if not texture then return true end
|
||||
-- Frame art that ships inside an addon (the "AddOns" entry above skips
|
||||
-- addon textures, which are mostly icons) but should follow the palette.
|
||||
local whitelist = {
|
||||
"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
|
||||
for entry in pairs(blacklist) do
|
||||
if string.find(name, entry, 1) then return true end
|
||||
end
|
||||
end
|
||||
|
||||
for entry in pairs(blacklist) do
|
||||
if string.find(texture, entry, 1) then return true end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -140,50 +208,257 @@ local function AddSpecialBackground(frame, w, h, x, y)
|
||||
frame.Material:SetTexture("Interface\\Stationery\\StationeryTest1")
|
||||
frame.Material:SetWidth(w)
|
||||
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)
|
||||
end
|
||||
|
||||
local function DarkenFrame(frame, r, g, b, a)
|
||||
-- dont't do anything if disabled
|
||||
if not ShaguTweaks.DarkMode then return end
|
||||
local function IsSlotArtwork(path)
|
||||
if type(path) ~= "string" 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
|
||||
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
|
||||
|
||||
-- iterate through all subframes
|
||||
if frame and frame.GetChildren then
|
||||
if capture then scan.tables = scan.tables + 1 end
|
||||
for _, frame in pairs({frame:GetChildren()}) do
|
||||
DarkenFrame(frame, r, g, b, a)
|
||||
DarkenFrame(frame, r, g, b, a, scan)
|
||||
end
|
||||
end
|
||||
|
||||
-- set vertex on all regions
|
||||
if frame and frame.GetRegions then
|
||||
if capture then scan.tables = scan.tables + 1 end
|
||||
-- read name
|
||||
local name = frame.GetName and frame:GetName()
|
||||
|
||||
-- set a dark 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)
|
||||
-- set the palette's backdrop border color everywhere
|
||||
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
|
||||
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
|
||||
|
||||
-- add black borders around specified buttons
|
||||
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
|
||||
|
||||
-- scan through all regions (textures)
|
||||
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:GetTexture() and string.find(region:GetTexture(), "UI%-Panel%-Button%-Up") then
|
||||
-- monochrome buttons
|
||||
-- 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
|
||||
-- skip special regions
|
||||
elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then
|
||||
@@ -191,18 +466,251 @@ local function DarkenFrame(frame, r, g, b, a)
|
||||
elseif IsBlacklisted(region) then
|
||||
-- skip blacklisted texture names
|
||||
else
|
||||
if capture then capture.textureWrites = capture.textureWrites + 1 end
|
||||
region:SetVertexColor(r,g,b,a)
|
||||
if name == "SpellBookFrame" then RestoreSpellbookParchment(frame, region) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if frame and frame.GetRegions then RestoreQuestParchment(frame) end
|
||||
if frame and frame.GetRegions then RestoreQuestLogParchment(frame) end
|
||||
end
|
||||
|
||||
-- register darken frame to global
|
||||
ShaguTweaks.DarkenFrame = DarkenFrame
|
||||
|
||||
module.enable = function(self)
|
||||
ShaguTweaks.DarkMode = true
|
||||
-- Other addons that draw their own tooltip-style borders. Their frames (and
|
||||
-- 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 hookBuffButton_Update = BuffButton_Update
|
||||
@@ -220,23 +728,45 @@ module.enable = function(self)
|
||||
elseif not original and _G[name] then
|
||||
-- tbc buff buttons don't have borders, so we
|
||||
-- need to manually add a dark one.
|
||||
AddBorder(_G[name], 2, module.config["darkmode.color"])
|
||||
AddBorder(_G[name], 2, edge)
|
||||
end
|
||||
end
|
||||
|
||||
TOOLTIP_DEFAULT_COLOR.r = module.config["darkmode.color"].r
|
||||
TOOLTIP_DEFAULT_COLOR.g = module.config["darkmode.color"].g
|
||||
TOOLTIP_DEFAULT_COLOR.b = module.config["darkmode.color"].b
|
||||
TOOLTIP_DEFAULT_COLOR.r = edge.r
|
||||
TOOLTIP_DEFAULT_COLOR.g = edge.g
|
||||
TOOLTIP_DEFAULT_COLOR.b = edge.b
|
||||
|
||||
TOOLTIP_DEFAULT_BACKGROUND_COLOR.r = module.config["darkmode.color"].r
|
||||
TOOLTIP_DEFAULT_BACKGROUND_COLOR.g = module.config["darkmode.color"].g
|
||||
TOOLTIP_DEFAULT_BACKGROUND_COLOR.b = module.config["darkmode.color"].b
|
||||
-- Darkened UI also greys the tooltip background; Golden UI keeps it.
|
||||
if darkBackgrounds then
|
||||
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(WorldMapFrame)
|
||||
DarkenFrame(DropDownList1)
|
||||
DarkenFrame(DropDownList2)
|
||||
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
|
||||
local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "Shapeshift" }
|
||||
@@ -259,7 +789,7 @@ module.enable = function(self)
|
||||
if button[func] then
|
||||
local tex = button[func](button)
|
||||
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
|
||||
@@ -268,12 +798,12 @@ module.enable = function(self)
|
||||
HookAddonOrVariable("Blizzard_AuctionUI", function()
|
||||
for i = 1, 15 do
|
||||
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
|
||||
|
||||
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.."Right"]: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(tint.r, tint.g, tint.b, 1)
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -413,3 +943,253 @@ module.enable = function(self)
|
||||
table.insert(ShaguTweaks.libnameplate.OnShow, ReapplyShaguDarkMode)
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -10,11 +10,186 @@ local module = ShaguTweaks:register({
|
||||
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)
|
||||
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
|
||||
or not API.iteminventorytype or not API.iteminventoryslotkey then
|
||||
return
|
||||
end
|
||||
self.installed = true
|
||||
|
||||
local sides = { "Left", "Right" }
|
||||
|
||||
@@ -129,6 +304,7 @@ module.enable = function(self)
|
||||
ShoppingTooltip1:SetInventoryItem("player", slotID)
|
||||
ShoppingTooltip1:Show()
|
||||
AddHeader(ShoppingTooltip1)
|
||||
MatchBackground(ShoppingTooltip1, tooltip)
|
||||
|
||||
if slotNames[2] then
|
||||
local secondSlotID = GetInventorySlotInfo(slotNames[2])
|
||||
@@ -142,6 +318,7 @@ module.enable = function(self)
|
||||
ShoppingTooltip2:SetInventoryItem("player", secondSlotID)
|
||||
ShoppingTooltip2:Show()
|
||||
AddHeader(ShoppingTooltip2)
|
||||
MatchBackground(ShoppingTooltip2, tooltip)
|
||||
end
|
||||
|
||||
activeTooltip = tooltip
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
local _G = ShaguTweaks.GetGlobalEnv()
|
||||
local L, T = ShaguTweaks.L, ShaguTweaks.T
|
||||
local T = ShaguTweaks.T
|
||||
local API = ShaguTweaks.API
|
||||
|
||||
local module = ShaguTweaks:register({
|
||||
|
||||
@@ -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
|
||||