From dfe8cea6926e5e0908518b2465cdf9c410a43fad Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Wed, 4 Nov 2026 14:03:16 -0400 Subject: [PATCH] Release 3.8.1: streamline ClassicAPI support and fix action bars --- CHANGELOG.md | 11 + Docs/EXTRAS_MERGE.md | 13 + README.md | 10 +- ShaguTweaks.toc | 5 +- api.lua | 32 +-- libs/libdebuff.lua | 378 ------------------------------ libs/libnameplate.lua | 15 +- locales/translations_enUS.lua | 2 - main.lua | 8 + mods/actionbar-grid.lua | 27 +++ mods/auto-dismount.lua | 121 ---------- mods/turtle-wow.lua | 48 ---- mods/unitframes-bighealth.lua | 10 +- tests/README.md | 2 + tests/test_actionbar_grid.lua | 16 ++ tests/test_bighealth_split.lua | 14 +- tests/test_classicapi_runtime.lua | 42 ++++ 17 files changed, 154 insertions(+), 600 deletions(-) delete mode 100644 libs/libdebuff.lua create mode 100644 mods/actionbar-grid.lua delete mode 100644 mods/auto-dismount.lua create mode 100644 tests/test_actionbar_grid.lua create mode 100644 tests/test_classicapi_runtime.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 8834bd1..dbd816f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ Changes in this fork (OEVG/ShaguTweaks-ClassicAPI), newest first. Each entry is This fork is based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at commit `4391564` (2026-09-08). Earlier changes are in that repository's history. +## 3.8.1 — 2026-11-04 — ClassicAPI baseline and legacy cleanup + +- Remove Auto Dismount, its TurtleWoW patches, unused API wrappers and obsolete saved toggle in favor of ClassicAPI's Auto Dismount, Auto Stand and Auto Cancel Form settings. Retain Auto Stance: it enters a required stance/form rather than cancelling one. +- Extend the native Always Show Action Bars preference to empty main/bonus-bar slots, preserving native drag-grid references and setting toggles. +- Fix disappearing Big Health fills after health updates in standalone mode: copy the hidden name background's RGB color without its zero alpha. +- Require ClassicAPI 1.18.10 or newer and check the loaded DLL plus core timer/nameplate APIs before enabling tweaks. Missing or outdated runtimes show an update/restart message. +- Remove the unused legacy debuff-timing library after Debuff Timer's removal in 3.8, including its TurtleWoW Paladin/Warlock spell-damage listener. +- Remove per-frame legacy nameplate discovery. Remaining theme integration uses deferred native nameplate events and stays dormant without consumers. +- Retain optional feature and missing-data checks, SuperWoW cast support, and compatibility paths not yet proven redundant. This is a selective cleanup, not removal of every fallback. +- Fifteen Lua 5.0 regression suites pass. In-game rendering, startup on installed client versions and CPU/memory differences still require user verification. + ## 3.8 — 2026-11-04 — Player frames, movable bars and UI polish ### Added diff --git a/Docs/EXTRAS_MERGE.md b/Docs/EXTRAS_MERGE.md index 8d0d9f1..a0f639e 100644 --- a/Docs/EXTRAS_MERGE.md +++ b/Docs/EXTRAS_MERGE.md @@ -52,6 +52,7 @@ The textures these modules use (the gryphons and the XP bar) moved to this addon |---|---|---| | **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. See [replacement settings below](#auto-dismount-replacement-381). Auto Stance remains a separate feature. | | **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 @@ -61,3 +62,15 @@ 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. + +### Auto Dismount replacement (3.8.1) + +ShaguTweaks' **Auto Dismount** option was removed because ClassicAPI provides +**Auto Dismount** (`autoDismount`), **Auto Stand** (`autoStand`) and +**Auto Cancel Form** (`autoUnshift`) in its options. The old module combined +these behaviors; enable the ClassicAPI options you want. ShaguTweaks does not +change these settings automatically. + +**Auto Stance** remains available: it tries to switch into a warrior stance or +druid form required by a spell. ClassicAPI's **Auto Cancel Form** instead leaves +a form when a spell cannot be cast in it; it does not replace Auto Stance. diff --git a/README.md b/README.md index 1ad2855..c480fd7 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,17 @@ This is an English-only customization of [Dusk/ShaguTweaks-ClassicAPI](https://o > This fork requires ClassicAPI and targets **Turtle WoW-like server/client environments**. -**Version 3.8.** What changed in each update: see the [changelog](CHANGELOG.md). +**Version 3.8.1.** 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. ## 🔌 Requirements -- **[ClassicAPI](https://github.com/brues-code/ClassicAPI) — required.** Install both the DLL and its bundled `!!!ClassicAPI` addon, and restart the client after a DLL update. v1.18.10 or newer is recommended (it includes a crash fix for addons that restyle nameplates). The integration was last reviewed against v1.18.13; see [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md). +- **[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. The integration was last reviewed against v1.18.13; see [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md). - **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional / recommended.** Only used as an additional fallback for some cast/GUID information. - **[ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) — no longer needed.** 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. -ClassicAPI is the main API layer. +ClassicAPI is the main API layer. Use its **Auto Dismount**, **Auto Stand** and **Auto Cancel Form** settings; ShaguTweaks' duplicate Auto Dismount option was removed in 3.8.1. See the [migration guide](Docs/EXTRAS_MERGE.md#auto-dismount-replacement-381). ## 📦 Installation @@ -103,7 +103,6 @@ Modules that use ClassicAPI directly: - Range Color - AoE Loot -- Auto Dismount - Chat Tweaks - Equip Compare - Free Slot Count @@ -138,8 +137,7 @@ Names match the in-game options list. Modules marked *(off)* are disabled until ### Action Bar -- Auto Dismount -- Auto Stance +- Auto Stance (enters a required stance/form; distinct from ClassicAPI Auto Cancel Form) - Range Color *(off)* - Cooldown Numbers - Hide Gryphons *(off)* diff --git a/ShaguTweaks.toc b/ShaguTweaks.toc index 7b1ffe9..1ea44b1 100644 --- a/ShaguTweaks.toc +++ b/ShaguTweaks.toc @@ -2,7 +2,7 @@ ## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI] ## Author: Shagu ## Notes: A collection of tweaks and enhancements to the Default UI -## Version: 3.8 +## Version: 3.8.1 ## Dependencies: !!!ClassicAPI ## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache ## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache @@ -23,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 @@ -54,7 +53,6 @@ mods\move-unitframes.lua mods\target-castbar.lua mods\social-colors.lua mods\item-colors.lua -mods\auto-dismount.lua mods\auto-stance.lua mods\tooltip-details.lua mods\hide-errors.lua @@ -76,6 +74,7 @@ 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 diff --git a/api.lua b/api.lua index 69d763e..194ff84 100644 --- a/api.lua +++ b/api.lua @@ -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" @@ -130,11 +140,6 @@ API.aoeloot = type(_G.C_Loot) == "table" 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 @@ -756,23 +761,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) diff --git a/libs/libdebuff.lua b/libs/libdebuff.lua deleted file mode 100644 index f8868e5..0000000 --- a/libs/libdebuff.lua +++ /dev/null @@ -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 diff --git a/libs/libnameplate.lua b/libs/libnameplate.lua index 5389fa2..b231a4d 100644 --- a/libs/libnameplate.lua +++ b/libs/libnameplate.lua @@ -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() diff --git a/locales/translations_enUS.lua b/locales/translations_enUS.lua index a9a15ea..ba9bab2 100644 --- a/locales/translations_enUS.lua +++ b/locales/translations_enUS.lua @@ -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, diff --git a/main.lua b/main.lua index fb6d171..e5d892c 100644 --- a/main.lua +++ b/main.lua @@ -55,11 +55,19 @@ ShaguTweaks:SetScript("OnEvent", function() 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 + -- Retire the removed duplicate toggle; ClassicAPI owns these preferences. + ShaguTweaks_config[ShaguTweaks.T["Auto Dismount"]] = nil + -- Gold numbers belong to Real Health Numbers; retire the duplicate toggle. ShaguTweaks_config["Unit Frame Health Colors"] = nil diff --git a/mods/actionbar-grid.lua b/mods/actionbar-grid.lua new file mode 100644 index 0000000..513b3bb --- /dev/null +++ b/mods/actionbar-grid.lua @@ -0,0 +1,27 @@ +-- 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 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 +end +ShaguTweaks.hooksecurefunc("MultiActionBar_UpdateGridVisibility", UpdateGrid) +local events = CreateFrame("Frame") +events:RegisterEvent("PLAYER_ENTERING_WORLD") +events:SetScript("OnEvent", function() + if ShaguTweaks.API.IsSupportedRuntime() then UpdateGrid() end +end) diff --git a/mods/auto-dismount.lua b/mods/auto-dismount.lua deleted file mode 100644 index f3f9405..0000000 --- a/mods/auto-dismount.lua +++ /dev/null @@ -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 diff --git a/mods/turtle-wow.lua b/mods/turtle-wow.lua index 71bc23f..d247f72 100644 --- a/mods/turtle-wow.lua +++ b/mods/turtle-wow.lua @@ -80,56 +80,8 @@ module.enable = function(self) -- Vendor Values exposes a lazy ClassicAPI-backed SellValueDB compatibility -- table, so do not allocate a second Turtle-specific static price database. - -- add druids tree of life and fast travel form to autoshift - if ShaguTweaks.dismount then - table.insert(ShaguTweaks.dismount.shapeshifts, "ability_druid_treeoflife") - table.insert(ShaguTweaks.dismount.shapeshifts, "ability_druid_stagform") - end - -- update debuff durations L["debuffs"]["Hand of Reckoning"] = {[0]=3.0} L["debuffs"]['Insect Swarm'] = {[0]=18.0} L["debuffs"]['Moonfire'] = {[1]=9.0,[2]=18.0,[3]=18.0,[4]=18.0,[5]=18.0,[6]=18.0,[7]=18.0,[8]=18.0,[9]=18.0,[10]=18.0,[0]=18.0} end - --- Turtle WoW specific libdebuff patches. Only Paladins and Warlocks --- can trigger these cases, so other classes should not parse every spell-damage --- chat line for strings they can never produce. -local libdebuff = ShaguTweaks.libdebuff -local _, playerClass = UnitClass("player") -if playerClass == "PALADIN" or playerClass == "WARLOCK" then - local libdebuff_twow = CreateFrame("Frame") - libdebuff_twow:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") - libdebuff_twow:SetScript("OnEvent", function() - -- Break early on invalid data - if not arg1 or not arg2 then return end - - -- Holy Strike is a spell, but can refresh paladin judgements - -- Credits to @geojak - if playerClass == "PALADIN" and string.find(arg1, "Holy Strike") then - for seal in ShaguTweaks.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 - - -- refresh Immolate duration after cast Conflagrate - if playerClass == "WARLOCK" and string.find(arg1, "Conflagrate") then - local name = UnitName("target") - local level = UnitLevel("target") - if name and level and libdebuff.objects[name] and libdebuff.objects[name][level] - and libdebuff.objects[name][level]["Immolate"] - then - local duration = libdebuff.objects[name][level]["Immolate"].duration - libdebuff:UpdateDuration(name, level, "Immolate", duration - 3) - end - end - end) -end diff --git a/mods/unitframes-bighealth.lua b/mods/unitframes-bighealth.lua index 03ea6cb..fef8e77 100644 --- a/mods/unitframes-bighealth.lua +++ b/mods/unitframes-bighealth.lua @@ -143,14 +143,16 @@ local function Enable(player, target) local function ApplyPlayerHealthColor() if not PlayerFrameNameBackground or not playerSetStatusBarColor then return end - local r, g, b, a = PlayerFrameNameBackground:GetVertexColor() - playerSetStatusBarColor(PlayerFrameHealthBar, r, g, b, a) + local r, g, b = PlayerFrameNameBackground:GetVertexColor() + -- Standalone hides this color-source texture with alpha zero. Its + -- visibility must not propagate to the actual health fill on updates. + playerSetStatusBarColor(PlayerFrameHealthBar, r, g, b, 1) end local function ApplyTargetHealthColor() if not TargetFrameNameBackground or not targetSetStatusBarColor then return end - local r, g, b, a = TargetFrameNameBackground:GetVertexColor() - targetSetStatusBarColor(TargetFrameHealthBar, r, g, b, a) + local r, g, b = TargetFrameNameBackground:GetVertexColor() + targetSetStatusBarColor(TargetFrameHealthBar, r, g, b, 1) end -- Keep the Big Health colors without replacing SetStatusBarColor with a diff --git a/tests/README.md b/tests/README.md index edf9887..afa7a61 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,6 +16,8 @@ lua5.0 tests/test_castbar_mover.lua . lua5.0 tests/test_bighealth_split.lua . lua5.0 tests/test_loss_of_control_style.lua . lua5.0 tests/test_health_text_alignment.lua . +lua5.0 tests/test_classicapi_runtime.lua . +lua5.0 tests/test_actionbar_grid.lua . ``` These checks execute private helpers extracted from the production source with diff --git a/tests/test_actionbar_grid.lua b/tests/test_actionbar_grid.lua new file mode 100644 index 0000000..8a3a1c3 --- /dev/null +++ b/tests/test_actionbar_grid.lua @@ -0,0 +1,16 @@ +local root=(arg[1] or '.')..'/' +local update,events +ShaguTweaks={GetGlobalEnv=function() return getfenv(0) end,API={IsSupportedRuntime=function() return true end},hooksecurefunc=function(_,fn) update=fn end} +CreateFrame=function() events={RegisterEvent=function() end,SetScript=function(self,_,fn) self.fire=fn end};return events end +ActionButton_ShowGrid=function(b) b.showgrid=b.showgrid+1;b.shown=true end +ActionButton_HideGrid=function(b) b.showgrid=b.showgrid-1;if b.showgrid==0 and not b.action then b.shown=false end end +ActionButton1={showgrid=0,shown=false};ActionButton2={showgrid=0,shown=true,action=true} +BonusActionButton1={showgrid=0,shown=false} +dofile(root..'mods/actionbar-grid.lua') +ALWAYS_SHOW_MULTIBARS="1";events.fire();update();assert(ActionButton1.showgrid==1 and ActionButton1.shown and BonusActionButton1.shown) +ActionButton_ShowGrid(ActionButton1) -- dragging adds its own grid reference +ALWAYS_SHOW_MULTIBARS=0;update();assert(ActionButton1.showgrid==1 and ActionButton1.shown) +ActionButton_HideGrid(ActionButton1);assert(not ActionButton1.shown and ActionButton2.shown and not BonusActionButton1.shown) +update();assert(ActionButton1.showgrid==0) +ALWAYS_SHOW_MULTIBARS=1;update();assert(ActionButton1.shown and ActionButton1.showgrid==1) +print('Main/bonus action grids: native preference, idempotence, toggle off/on, drag references and filled buttons passed') diff --git a/tests/test_bighealth_split.lua b/tests/test_bighealth_split.lua index 20b80ea..e5c64ec 100644 --- a/tests/test_bighealth_split.lua +++ b/tests/test_bighealth_split.lua @@ -7,7 +7,7 @@ local function Check(player,target) function methods:SetHeight(v) self.height=v end function methods:SetPoint() end function methods:SetVertexColor() end - function methods:GetVertexColor() return .2,.5,.8,1 end + function methods:GetVertexColor() return .2,.5,.8,0 end -- suppressed standalone name background function methods:GetTexCoord() return 0,0,0,1,1,0,1,1 end function methods:GetWidth() return 256 end function methods:SetWidth() end @@ -16,11 +16,14 @@ local function Check(player,target) function methods:SetScript(k,v) self[k]=v end function methods:RegisterEvent() end function methods:UnregisterAllEvents() end - function methods:SetStatusBarColor() end + function methods:SetStatusBarColor(r,g,b,a) self.color={r,g,b,a} end for _,name in ipairs({'PlayerFrameTexture','PlayerStatusTexture','PlayerFrameHealthBar','PlayerFrameNameBackground','TargetFrameTexture','TargetFrameHealthBar','TargetFrameNameBackground'}) do _G[name]=obj() end ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end}),UITint={r=.5,g=.5,b=.5,a=1}} function ShaguTweaks:register(m) mods[m.title]=m;return m end - hooksecurefunc=function(object,key,fn) if type(object)=='string' then hooks[object]=key end end + hooksecurefunc=function(object,key,fn) + if type(object)=='string' then hooks[object]=key + else local original=object[key];object[key]=function(self,...) original(self,unpack(arg));fn(self,unpack(arg)) end end + end GetAddOnInfo=function() return nil end IsResting=function() return false end UnitClassification=function() return 'elite' end @@ -37,6 +40,11 @@ local function Check(player,target) assert((TargetFrameNameBackground.hidden or false)==target) assert((hooks.PlayerFrame_UpdateStatus~=nil)==player) assert((hooks.TargetFrame_CheckClassification~=nil)==target) + if player then + PlayerFrameHealthBar:SetStatusBarColor(1,0,0,1) -- health loss triggers native recoloring + assert(PlayerFrameHealthBar.color[1]==.2 and PlayerFrameHealthBar.color[4]==1) + end + if target then assert(TargetFrameHealthBar.color[4]==1) end end Check(false,false);Check(true,false);Check(false,true);Check(true,true) local f=assert(io.open(root..'main.lua'));local s=f:read('*a');f:close();assert(loadstring(s)) diff --git a/tests/test_classicapi_runtime.lua b/tests/test_classicapi_runtime.lua new file mode 100644 index 0000000..cf3ba8a --- /dev/null +++ b/tests/test_classicapi_runtime.lua @@ -0,0 +1,42 @@ +local root=(arg[1] or '.')..'/' +local f=assert(io.open(root..'api.lua'));local source=f:read('*a');f:close();assert(loadstring(source)) +local a=assert(string.find(source,'API.minimum_version =',1,true)) +local b=assert(string.find(source,'API.classicapi =',a,true)) +local API={} +API=assert(loadstring('local _G=getfenv(0);local API={};'..string.sub(source,a,b-1)..'return API'))() +CLASSIC_API_VERSION=11810 +C_Timer={After=function() end};C_EventUtils={IsEventValid=function() return true end} +C_NamePlate={GetNamePlateForUnit=function() end} +assert(API.IsSupportedRuntime()) +CLASSIC_API_VERSION=11809;assert(not API.IsSupportedRuntime()) +CLASSIC_API_VERSION=nil;assert(not API.IsSupportedRuntime()) +CLASSIC_API_VERSION=11900;assert(API.IsSupportedRuntime()) +C_NamePlate=nil;assert(not API.IsSupportedRuntime()) +C_NamePlate={GetNamePlateForUnit=function() end} +local frames,scans={},0 +CreateFrame=function() + local f={scripts={},events={}} + function f:SetScript(k,v) self.scripts[k]=v end + function f:GetScript(k) return self.scripts[k] end + function f:RegisterEvent(k) self.events[k]=true end + table.insert(frames,f);return f +end +WorldFrame={GetNumChildren=function() scans=scans+1;return 0 end,GetChildren=function() end} +ShaguTweaks={API=API,GetGlobalEnv=function() return getfenv(0) end,GetExpansion=function() return 'vanilla' end} +API.nameplateevents=true +dofile(root..'libs/libnameplate.lua') +local lib=ShaguTweaks.libnameplate +lib:EnableIfNeeded();assert(not lib.enabled and scans==0) +table.insert(lib.OnInit,function() end) +CLASSIC_API_VERSION=0;lib:EnableIfNeeded();assert(not lib.enabled) +CLASSIC_API_VERSION=11810;lib:EnableIfNeeded();assert(lib.enabled and not lib.scripts.OnUpdate and scans==0) +local driver,retry +for _,frame in ipairs(frames) do + if frame.events.NAME_PLATE_UNIT_ADDED then driver=frame end + if frame.scripts.OnUpdate then retry=frame end +end +assert(driver and retry) +this=retry;retry.scripts.OnUpdate();assert(scans==1 and not retry.scripts.OnUpdate) +driver.scripts.OnEvent();driver.scripts.OnEvent();assert(scans==1) +this=retry;retry.scripts.OnUpdate();assert(scans==2 and not retry.scripts.OnUpdate) +print('ClassicAPI runtime: minimum/missing APIs, dormant nameplate discovery and deferred event coalescing passed')