v1.4.1 event leak fixes, chronometer trap fix, profile memory, autostrip alloc fix

This commit is contained in:
DuvelCorp
2026-03-24 14:21:58 +01:00
parent 8d80d0663c
commit 680ddcba8a
27 changed files with 298 additions and 117 deletions
+22
View File
@@ -2,10 +2,32 @@
All notable changes to MetaHunt will be documented in this file.
## [1.4.1] - 2026-03-24
### Added
- **zTrack**: added `Find Trees` in tracking spells.
### Changed
- **Performance — MM Widget / ExpAmmo**: Replaced tooltip-based aura scanning (which triggered hidden tooltip rendering on every aura check) with direct texture/icon comparison via `UnitBuff`/`UnitDebuff`. Added a 150 ms throttle on `PLAYER_AURAS_CHANGED`, which fires 1050+ times per second in combat. Lock and Load detection and ammo cycle recovery after `/reload` both updated to use the new zero-cost texture path.
- **Performance — event frame leaks**: Some addon features were registering WoW game events at load time and never unregistering them when the feature was disabled from the options, causing their `OnEvent` handlers to fire even with the option turned off. Fixed for: Auto-Strip (`PLAYER_REGEN_*`), Smart Ammo (`SPELLCAST_*`, `START/STOP_AUTOREPEAT_SPELL`), and some zButton frames (Pet, Mounts, Track, Trap, Toys). Events are now properly registered on enable and unregistered on disable.
- **Chronometer — trap effects from other hunters**: Trap effect bars (`Immolation Trap Effect`, `Explosive Trap Effect`, `Freezing Trap Effect`, `Frost Trap Aura`) were incorrectly appearing when another hunter's trap triggered on a mob you were also targeting. .
- **Version check broadcast interval**: Increased update notification broadcast cooldown from 10 minutes to 1 hour to reduce channel traffic now that the user base has grown.
- **Profile snapshot memory**: The auto `BuildSnapshot` (called on every `/reload` via `PLAYER_LOGOUT`) was deep-copying `feedomatic.legacy`, a large migration table mirroring `FOM_Cooking`, `FOM_QuestFood`, `FOM_AddedFoods` etc. These are runtime/operational blobs, not user configuration settings, and should never be included in a profile. Introduced `MTH_Profile_CopyModules` which skips `legacy` and `history` keys, reducing the snapshot allocation from ~1 MB to a few KB.
## [1.4.0] - 2026-03-23
### TWoW 1.18.1 "Nightmares of Ursol" support
### Added
- **Complete beast database overhaul**: The MTH beast datastore was reconstructed from scratch, based on a full (beasts) 1.18.1 extract from live DB, gladly provided by Twow staff (thanks @Haaxor!). The beast data should now be 100% accurate, and notably all spawn coordinates, respawn times and pet abilities learnable on all beasts in the world.
- The **Beast Lore** page of the Book now allows to filter the list by Zone. Additionally, the beast inspector has been reformatted and all meaningful beast's data is displayed nicely.
+1 -1
View File
@@ -1,5 +1,5 @@
## Interface: 11200
## Version: 1.4.0
## Version: 1.4.1
## Title: MetaHunt - |cff00ff00Hunter
## Author: Metasploit and his Copilot ;)
## Notes: Unified addon suite for huntards making old addons compatible with TurtleWoW, and adding an arsenal of never seen Hunter's utilities.
+7 -4
View File
@@ -1,12 +1,12 @@
# MetaHunt
MetaHunt is a Turtle WoW Hunter toolkit.
It is a modern modular addon that combines never-seen features, and improves/fixes a few old hunter add-ons.
It provides a bunch useful tools for all huntards, wheter they are still levelling, lone-wolf, or HL raider.
It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons, that were broken on twow.
It provides a bunch of useful tools for all huntards, wheter they are still levelling, lone-wolf, or HL raider.
## Twow Data
MetaHunt ships with large up-to-date datastores for all Turtle WOW data a hunter needs :
MetaHunt ships with large Turtle WOW datastores :
- 100% accurate beasts' data, their locations, and the abilities they can learn you
- All Pet families, their abilities and their diet
@@ -31,7 +31,7 @@ MetaHunt ships with large up-to-date datastores for all Turtle WOW data a hunter
- Vendors : Shows if a vendor sells Arrows and/or Bullets.
- Your pet : Shows your pet's XP, loyalty, happiness and how much you fed it this session.
## zBouttons
## zButtons
Old zHunterMod addon on steroids. Fully compatible with Twow, and enhanced with new functionalities
@@ -63,6 +63,9 @@ Define your own simple rules for auto buy ammo and pet food when you open a vend
Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. Additionally you can have an enhanced tooltip that display your actual Scorpok-related items when you mouseover the mobs involved.
## Beast model Viewer
Display the model of all beasts in the world. Always know what you are going to tame exactly before running there ! You can also browse all skin models of the same pet family to find the best-looking pet according to your tastes.
## The Great Book of Huntards
+18 -6
View File
@@ -150,6 +150,10 @@ function AutoStrip_SetAutoStripToggle(enabled, silent)
if enabled then
saved["autostrip"] = 1
AutoStrip_On = 1
if AutoStrip_Frame then
AutoStrip_Frame:RegisterEvent("PLAYER_REGEN_DISABLED")
AutoStrip_Frame:RegisterEvent("PLAYER_REGEN_ENABLED")
end
if AutoStripDisplay and AutoStripDisplay:IsVisible() and AutoStripDisplayAutoCast then
AutoStripDisplayAutoCast:Show()
end
@@ -159,6 +163,10 @@ function AutoStrip_SetAutoStripToggle(enabled, silent)
else
saved["autostrip"] = nil
AutoStrip_On = nil
if AutoStrip_Frame then
AutoStrip_Frame:UnregisterEvent("PLAYER_REGEN_DISABLED")
AutoStrip_Frame:UnregisterEvent("PLAYER_REGEN_ENABLED")
end
if AutoStripDisplayAutoCast then
AutoStripDisplayAutoCast:Hide()
end
@@ -252,10 +260,12 @@ function AutoStrip_UnequipAll(weaponsOnly)
end
local function AutoStrip_OnEvent()
AutoStrip_Trace("OnEvent evt=" .. tostring(event or "")
.. " runtime=" .. tostring(AutoStrip_On and 1 or 0)
.. " wasInCombat=" .. tostring(AutoStrip_WasInCombat and 1 or 0)
.. " playerInCombat=" .. tostring((type(UnitAffectingCombat) == "function" and UnitAffectingCombat("player")) and 1 or 0))
if AutoStrip_TraceEnabled then
AutoStrip_Trace("OnEvent evt=" .. tostring(event or "")
.. " runtime=" .. tostring(AutoStrip_On and 1 or 0)
.. " wasInCombat=" .. tostring(AutoStrip_WasInCombat and 1 or 0)
.. " playerInCombat=" .. tostring((type(UnitAffectingCombat) == "function" and UnitAffectingCombat("player")) and 1 or 0))
end
if event == "VARIABLES_LOADED" then
local saved = AutoStrip_GetSaved()
AutoStrip_Trace("VARIABLES_LOADED saved.autostrip=" .. tostring(saved["autostrip"] and 1 or 0)
@@ -318,9 +328,11 @@ end
AutoStrip_Frame = CreateFrame("Frame", "MTH_AutoStripEvent")
if AutoStrip_Frame then
-- PLAYER_REGEN_DISABLED / PLAYER_REGEN_ENABLED are intentionally NOT registered here.
-- They are registered only when the feature is enabled via AutoStrip_SetAutoStripToggle(true),
-- and unregistered when it is disabled. This prevents any combat event overhead when the
-- feature is off.
AutoStrip_Frame:RegisterEvent("VARIABLES_LOADED")
AutoStrip_Frame:RegisterEvent("PLAYER_REGEN_DISABLED")
AutoStrip_Frame:RegisterEvent("PLAYER_REGEN_ENABLED")
AutoStrip_Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
AutoStrip_Frame:RegisterEvent("PLAYER_LOGOUT")
AutoStrip_Frame:SetScript("OnEvent", AutoStrip_OnEvent)
+1 -1
View File
@@ -1,5 +1,5 @@
MTH_CONST = MTH_CONST or {}
MTH_CONST.version = MTH_CONST.version or "1.4.0"
MTH_CONST.version = MTH_CONST.version or "1.4.1"
MTH_CONST.WEAPON_TYPES = {
BOWS = "Bows",
+1 -1
View File
@@ -1,5 +1,5 @@
MTH = MTH or {
version = "1.4.0",
version = "1.4.1",
name = "MetaHunt",
modules = {},
config = {},
+26 -2
View File
@@ -19,6 +19,30 @@ local function MTH_Profile_DeepCopy(orig)
return copy
end
-- Shallow-copy a module's saved data, skipping keys that hold large operational
-- blobs (e.g. feedomatic's "legacy" table which mirrors FOM_Cooking, FOM_QuestFood
-- etc.) — those are runtime migration artifacts, not user configuration settings.
local MTH_PROFILE_SKIP_KEYS = { legacy = true, history = true }
local function MTH_Profile_CopyModules(src)
if type(src) ~= "table" then return {} end
local out = {}
for modName, modData in src do
if type(modData) == "table" then
local modCopy = {}
for k, v in modData do
if not MTH_PROFILE_SKIP_KEYS[k] then
modCopy[MTH_Profile_DeepCopy(k)] = MTH_Profile_DeepCopy(v)
end
end
out[modName] = modCopy
else
out[modName] = modData
end
end
return out
end
local function MTH_Profile_EnsureStore()
if type(MTH_SavedVariables) ~= "table" then
MTH_SavedVariables = {}
@@ -47,13 +71,13 @@ local function MTH_Profile_BuildSnapshot()
-- Account-wide module option values (e.g. feedomatic)
snap.config = (type(MTH_SavedVariables) == "table"
and type(MTH_SavedVariables.modules) == "table")
and MTH_Profile_DeepCopy(MTH_SavedVariables.modules)
and MTH_Profile_CopyModules(MTH_SavedVariables.modules)
or {}
-- Per-character module option values (ICU, Chronometer, AutoBuy, SmartAmmo, etc.)
snap.charConfig = (type(MTH_CharSavedVariables) == "table"
and type(MTH_CharSavedVariables.modules) == "table")
and MTH_Profile_DeepCopy(MTH_CharSavedVariables.modules)
and MTH_Profile_CopyModules(MTH_CharSavedVariables.modules)
or {}
-- Per-character module enabled/disabled states
+2 -2
View File
@@ -221,8 +221,8 @@ end
function VC:ResetPublishDelay()
local now = VC_GetTimeNow()
if self.lastPublishedAt and (now - self.lastPublishedAt) < 600 then
return -- 10-minute cooldown between publishes
if self.lastPublishedAt and (now - self.lastPublishedAt) < 3600 then
return -- 1-hour cooldown between publishes
end
self.nextPublishAt = now + math.random(10, 20)
end
+1 -1
View File
@@ -6,7 +6,7 @@
local MTH_AutoBuy = {
name = "autobuy",
enabled = false,
version = "1.4.0",
version = "1.4.1",
events = {
"VARIABLES_LOADED",
"MERCHANT_SHOW",
+1 -1
View File
@@ -6,7 +6,7 @@
local MTH_AutoQuest = {
name = "autoquest",
enabled = false,
version = "1.4.0",
version = "1.4.1",
events = {
"GOSSIP_SHOW",
"QUEST_GREETING",
+7 -4
View File
@@ -40,10 +40,13 @@ function MTH_ChronometerHunterInstallData(engine)
engine:AddTimer(engine.SPELL, BS["Wyvern Sting"], 12, 1, 0, 0, { gr = 2, cr = "MAROON", rc = true })
engine:AddTimer(engine.SPELL, BS["Feed Pet"], 20, 0, 0, 0, { cr = "MAROON" })
engine:AddTimer(engine.EVENT, BS["Explosive Trap Effect"], 20, 1, 0, 1, { gr = 1, cr = "ORANGE", a = 1, xn = BS["Explosive Trap"] })
engine:AddTimer(engine.EVENT, BS["Freezing Trap Effect"], 10, 1, 0, 1, { gr = 1, cr = "CYAN", a = 1, d = { rs = 5, tn = BS["Clever Traps"], tb = 0.15, tp = 1 }, xn = BS["Freezing Trap"] })
engine:AddTimer(engine.EVENT, BS["Frost Trap Aura"], 30, 0, 0, 0, { gr = 1, cr = "CYAN", a = 1, d = { tn = BS["Clever Traps"], tb = 0.15, tp = 1 }, xn = BS["Frost Trap"] })
engine:AddTimer(engine.EVENT, BS["Immolation Trap Effect"], 15, 1, 0, 1, { gr = 1, cr = "ORANGE", a = 1, xn = BS["Immolation Trap"] })
-- Trap effects: no "a=1" here — primed by the SPELL timer's "ea" chain (cast hook sets timer.v
-- and timer.t). Using "a=1" would trigger on any hunter's affliction message since
-- CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS never embeds a caster name.
engine:AddTimer(engine.EVENT, BS["Explosive Trap Effect"], 20, 1, 0, 1, { gr = 1, cr = "ORANGE", xn = BS["Explosive Trap"] })
engine:AddTimer(engine.EVENT, BS["Freezing Trap Effect"], 10, 1, 0, 1, { gr = 1, cr = "CYAN", d = { rs = 5, tn = BS["Clever Traps"], tb = 0.15, tp = 1 }, xn = BS["Freezing Trap"] })
engine:AddTimer(engine.EVENT, BS["Frost Trap Aura"], 30, 0, 0, 0, { gr = 1, cr = "CYAN", d = { tn = BS["Clever Traps"], tb = 0.15, tp = 1 }, xn = BS["Frost Trap"] })
engine:AddTimer(engine.EVENT, BS["Immolation Trap Effect"], 15, 1, 0, 1, { gr = 1, cr = "ORANGE", xn = BS["Immolation Trap"] })
engine:AddTimer(engine.EVENT, BS["Improved Concussive Shot"], 3, 1, 0, 0, { a = 1, cr = "BLUE", xn = BS["Concussive Shot"] })
engine:AddTimer(engine.EVENT, BS["Improved Wing Clip"], 5, 1, 0, 0, { a = 1, cr = "MAROON", xn = BS["Wing Clip"] })
engine:AddTimer(engine.EVENT, BS["Lock and Load"], 10, 0, 1, 1, { a = 1, cr = "MAROON", tx = "Interface\\Icons\\ability_hunter_lockandload" }) -- 10s (tooltip confirmed; expires on Aimed Shot cast)
+39 -6
View File
@@ -580,14 +580,18 @@ function Chronometer:OnEnable()
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_AURA_GONE_SELF", function(event, info) self:SPELL_FADE(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_AURA_GONE_OTHER", function(event, info) self:SPELL_FADE(event, info) end)
-- Registered events:
-- SELF_BUFFS / SELF_DAMAGE → our own procs & self-damage (source == ParserLib_SELF confirmed)
-- CREATURE_DAMAGE → DOT ticks on creatures ("from your X" embeds source; filtered in SPELL_PERIODIC)
-- CREATURE_BUFFS → debuff APPLICATION on creatures ("X is afflicted by Y"; no source embedded).
-- Needed for non-ticking debuffs: Poisonous/Enchanted Ammunition, Piercing Shot.
-- Foreign-bar risk is mitigated in SPELL_PERIODIC by requiring victim == target
-- when source is nil and no confirmed cast record exists.
-- FRIENDLYPLAYER_*, HOSTILEPLAYER_* are other players' events → intentionally NOT registered.
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
local enableRoM = false
for _, timer in pairs(self.timers[self.SPELL] or {}) do
@@ -1274,11 +1278,20 @@ end
function Chronometer:SPELL_PERIODIC(event, info)
local aura, rank, unit, isgain
-- Ignore events not caused by the player (e.g. other players' DOTs/traps nearby)
-- Primary filter: explicit non-self source → always ignore (other player's confirmed event)
if info.source and info.source ~= ParserLib_SELF then return end
-- Secondary filter: creature/friendly periodic events do NOT embed a caster name,
-- so info.source is nil even for other players' effects. Track whether this event
-- was provably ours (SELF_BUFFS / SELF_DAMAGE events give source == ParserLib_SELF).
local sourceIsConfirmedSelf = (info.source == ParserLib_SELF)
if info.type == "buff" then
isgain = 1
-- Buff gained by someone other than self with no source confirmation → another player's proc
if not sourceIsConfirmedSelf and info.victim ~= nil and info.victim ~= ParserLib_SELF then
return
end
elseif info.type == "debuff" then
isgain = nil
elseif not info.isDOT then
@@ -1306,7 +1319,27 @@ function Chronometer:SPELL_PERIODIC(event, info)
if timer.k.t then
if not unit then unit = UnitName("player") end
if timer.k.s then
if timer.t and timer.t ~= unit then return end
-- Selforselect: four-tier confirmation.
-- 1. Confirmed cast record (timer.t set by CastSpell hook): require target match.
-- 2. Confirmed self source (SELF_BUFFS / SELF_DAMAGE events, source == SELF): accept.
-- 3. Cast-window confirmed (timer.v fresh, timer.t nil): untargeted spells like traps
-- where the caster is known but the trigger victim cannot be predicted in advance.
-- Accept unconditionally — the mob that walked into the trap may not be our target.
-- 4. Source unconfirmed AND no cast record (CREATURE_BUFFS affliction messages never
-- embed a caster name): fall back to target-check. This lets non-ticking debuffs
-- (Poisonous Ammo, Enchanted Ammo, Piercing Shot…) show when applied to our current
-- target, while blocking the same debuff on a mob we are NOT targeting (i.e. another
-- player's debuff on a different dummy).
if timer.t then
if timer.t ~= unit then return end
elseif sourceIsConfirmedSelf then
-- source confirmed → accept unconditionally
elseif timer.v and timer.v > GetTime() then
-- cast-window confirmed, timer.t nil → untargeted cast (e.g. trap placed on ground)
-- victim unknown at cast time; accept whatever mob triggered it
elseif not UnitExists("target") or unit ~= UnitName("target") then
return
end
else
if not UnitExists("target") or unit ~= UnitName("target") then return end
end
+1 -1
View File
@@ -1,7 +1,7 @@
local MTH_ChronometerModule = {
name = "chronometer",
enabled = true,
version = "1.4.0",
version = "1.4.1",
events = {},
initialized = false,
}
+81 -46
View File
@@ -169,28 +169,66 @@ local function MTH_EA_GetLnLRemaining()
return nil
end
-- Scan all player auras (buffs then debuffs) for an active ammo state.
-- Returns the matching STATE_ORDER key, or nil if none found.
local function MTH_EA_FindAmmoAura()
-- Try buffs first
-- ── Fast texture-based aura scanners (no tooltip, no pcall) ─────────────────
-- UnitBuff("player", i) → icon texture path for buff slot i (nil = end)
-- UnitDebuff("player", i) → icon texture path for debuff slot i (nil = end)
-- We match by icon basename instead of buff name: zero rendering cost.
-- Known icon basenames (lower-case, no path prefix, no extension):
local MTH_EA_AMMO_TEX = {
[EXPLOSIVE] = "ability_searingarrow",
[POISONOUS] = "ability_poisonarrow",
[ENCHANTED] = "ability_theblackarrow",
}
local MTH_EA_LNL_TEX = "lockandload" -- matches "ability_hunter_lockandload"
local function MTH_EA_IconBasename(texturePath)
if not texturePath then return "" end
local s = string.lower(tostring(texturePath))
local _, _, base = string.find(s, "([^\\]+)$")
return base or s
end
-- Scan player buffs+debuffs for an active ammo state via texture comparison.
local function MTH_EA_FindAmmoStateByTexture()
for i = 1, 32 do
local name = MTH_EA_GetAuraName(false, i)
if not name then break end
for _, stateKey in ipairs(STATE_ORDER) do
if name == STATE_BUFF[stateKey] then return stateKey end
local t = UnitBuff("player", i)
if not t then break end
local base = MTH_EA_IconBasename(t)
for stateKey, frag in pairs(MTH_EA_AMMO_TEX) do
if base == frag then return stateKey end
end
end
-- Fall back to debuffs ("You are afflicted by" = player debuff)
for i = 1, 16 do
local name = MTH_EA_GetAuraName(true, i)
if not name then break end
for _, stateKey in ipairs(STATE_ORDER) do
if name == STATE_BUFF[stateKey] then return stateKey end
local t = UnitDebuff("player", i)
if not t then break end
local base = MTH_EA_IconBasename(t)
for stateKey, frag in pairs(MTH_EA_AMMO_TEX) do
if base == frag then return stateKey end
end
end
return nil
end
-- Returns true if LnL buff is currently on the player.
local function MTH_EA_HasLnLByTexture()
for i = 1, 32 do
local t = UnitBuff("player", i)
if not t then break end
if string.find(MTH_EA_IconBasename(t), MTH_EA_LNL_TEX, 1, true) then
return true
end
end
return false
end
-- Scan all player auras (buffs then debuffs) for an active ammo state.
-- Returns the matching STATE_ORDER key, or nil if none found.
-- (Delegates to the fast texture path; tooltip path kept for debug use only.)
local function MTH_EA_FindAmmoAura()
return MTH_EA_FindAmmoStateByTexture()
end
-- Restore cycle state from active auras (survives /reload within session).
local function MTH_EA_RecoverStateFromBuffs()
local stateKey = MTH_EA_FindAmmoAura()
@@ -234,6 +272,10 @@ end
-- Combat state flag (avoids needing InCombatLockdown, which doesn't exist in 1.12)
local inCombat = false
-- Throttle for PLAYER_AURAS_CHANGED: even with the fast texture path this event
-- fires 10-50+/sec in combat, so cap processing at ~6/sec.
local MTH_EA_AurasChangedLastTime = 0
-- UI frame handles — declared here so MTH_EA_RefreshVisibility (and every
-- function defined below) all close over the SAME locals.
local strip = nil -- root drag frame (sized to 3×1 row)
@@ -834,47 +876,40 @@ frame:SetScript("OnEvent", function()
if event == "PLAYER_AURAS_CHANGED" then
if not MTH_EA_IsEnabled() then return end
-- Pending LnL icon capture: buff is now on the unit, do the diff.
-- Throttle: cap processing at ~6/sec. Chat-message handlers cover all
-- real-time state transitions; this path is only for /reload recovery
-- and LnL sync, so a 150 ms gate is perfectly safe.
local now = GetTime()
if (now - MTH_EA_AurasChangedLastTime) < 0.15 then return end
MTH_EA_AurasChangedLastTime = now
-- Pending LnL icon capture (no-op stub; kept to avoid errors).
if lnlPendingCapture then
MTH_EA_TryCaptureNewBuffIcon()
end
-- ── LnL detection: always sync timer from tooltip ────────────────
-- PLAYER_AURAS_CHANGED fires for every buff change (unrelated to LnL),
-- so we CANNOT just reset to DURATION on each fire. Instead we read
-- the actual remaining time from the game's own buff tooltip — this
-- gives correct values for both initial proc AND mid-buff refresh.
local lnlBuffIndex = nil
for i = 1, 32 do
local name = MTH_EA_GetAuraName(false, i)
if not name then break end
if string.find(name, "Lock and Load", 1, true) then
lnlBuffIndex = i
break
end
end
if lnlBuffIndex then
local rem = MTH_EA_GetLnLRemaining()
if rem and rem > 0 then
-- Sync to game's real remaining time (handles refresh correctly too).
-- ── LnL sync: texture-based, zero tooltip cost ───────────────────
-- We only SET active here (initial gain / missed chat message).
-- LOSS is authoritative from the AURA_GONE_SELF chat handler + OnUpdate
-- expiry, so we deliberately do NOT clear lnl.active when not found —
-- AURAS_CHANGED fires for every unrelated buff change and a transient
-- miss during slot reordering would prematurely kill the timer.
if MTH_EA_HasLnLByTexture() then
if not lnl.active then
lnl.active = true
lnl.expiresAt = GetTime() + rem
elseif not lnl.active then
-- Tooltip parse failed; first-gain fallback only.
lnl.active = true
lnl.expiresAt = GetTime() + lnl.DURATION
lnl.expiresAt = now + lnl.DURATION
end
-- If already active and parse failed: leave expiresAt as-is.
lnlPendingCapture = false
end
-- NOTE: do NOT set lnl.active=false here when the buff is not found.
-- AURAS_CHANGED fires for every unrelated buff change and the tooltip
-- scan can transiently miss LnL during slot reordering.
-- Loss is handled by the "fades from you" chat handler + OnUpdate expiry.
-- Recovery path: resync ammo state after /reload.
-- ── Recovery: resync ammo state after /reload ─────────────────────
if rt.state == IDLE then
local before = rt.state
MTH_EA_RecoverStateFromBuffs()
if MTH_EA_DebugMode and rt.state ~= before then
local stateKey = MTH_EA_FindAmmoStateByTexture()
if stateKey then
rt.state = stateKey
rt.expiresAt = now + CYCLE_DURATION
rt.consumed = false
end
MTH_EA_UpdateUI()
end
+24 -24
View File
@@ -926,7 +926,7 @@ function FOM_OnEvent(event, arg1)
end
if (foodID) then
FOM_LastFood = GFWUtils.ItemLink(foodID);
GFWUtils.DebugLog("Manually fed "..FOM_LastFood);
FOM_DebugLog("Manually fed "..FOM_LastFood);
end
end
return;
@@ -1245,31 +1245,31 @@ end
function FOM_CanFeed()
local petInfo = MTH_FOM_GetCorePetInfo();
if ( not (petInfo and petInfo.liveExists) ) then
GFWUtils.DebugLog("Can't feed; pet doesn't exist.");
FOM_DebugLog("Can't feed; pet doesn't exist.");
return false;
end
if ( tonumber(petInfo.health) and tonumber(petInfo.health) <= 0 ) then
GFWUtils.DebugLog("Can't feed; pet is dead.");
FOM_DebugLog("Can't feed; pet is dead.");
return false;
end
if ( UnitHealth("player") <= 0 ) then
GFWUtils.DebugLog("Can't feed; I'm dead.");
FOM_DebugLog("Can't feed; I'm dead.");
return false;
end
if ( CastingBarFrameStatusBar:IsVisible() ) then
GFWUtils.DebugLog("Can't feed; casting a spell / tradeksill.");
FOM_DebugLog("Can't feed; casting a spell / tradeksill.");
return false;
end
if ( UnitOnTaxi("player") ) then
GFWUtils.DebugLog("Can't feed; flying.");
FOM_DebugLog("Can't feed; flying.");
return false;
end
if ( FOM_State.InCombat ) or ( PlayerFrame.inCombat ) then
GFWUtils.DebugLog("Can't feed; in combat.");
FOM_DebugLog("Can't feed; in combat.");
return false;
end
if ( LootFrame:IsVisible() ) then
GFWUtils.DebugLog("Shouldn't feed; loot window is open.");
FOM_DebugLog("Shouldn't feed; loot window is open.");
return false;
end
@@ -1290,7 +1290,7 @@ function FOM_CanFeed()
if ( buff ~= nil) then
for _, buffTexture in dontFeedBuffTextures do
if ( buff == buffTexture ) then
GFWUtils.DebugLog("Can't feed; currently, eating, drinking, or feigning death.");
FOM_DebugLog("Can't feed; currently, eating, drinking, or feigning death.");
return false;
end
end
@@ -1303,7 +1303,7 @@ function FOM_CanFeed()
msg = string.lower(msg);
for _, mountName in FOM_MOUNT_NAME_SUBSTRINGS do
if (string.find(msg, mountName)) then
GFWUtils.DebugLog("Can't feed; mounted.");
FOM_DebugLog("Can't feed; mounted.");
return false;
end
end
@@ -1856,13 +1856,13 @@ end
function FOM_AddFood(diet, food)
if (FOM_Foods[diet] == nil) then
GFWUtils.DebugLog("FOM_Foods[diet] == nil");
FOM_DebugLog("FOM_Foods[diet] == nil");
end
if (FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil) then
GFWUtils.DebugLog("FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil");
FOM_DebugLog("FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil");
end
if (FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil) then
GFWUtils.DebugLog("FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil");
FOM_DebugLog("FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil");
end
if ( GFWTable.IndexOf(FOM_Foods[diet], food) == 0 ) then
if (FOM_AddedFoods == nil) then
@@ -1898,13 +1898,13 @@ end
function FOM_RemoveFood(diet, food)
if (FOM_Foods[diet] == nil) then
GFWUtils.DebugLog("FOM_Foods[diet] == nil");
FOM_DebugLog("FOM_Foods[diet] == nil");
end
if (FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil) then
GFWUtils.DebugLog("FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil");
FOM_DebugLog("FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil");
end
if (FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil) then
GFWUtils.DebugLog("FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil");
FOM_DebugLog("FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil");
end
if ( GFWTable.IndexOf(FOM_Foods[diet], food) ~= 0 ) then
if (FOM_RemovedFoods == nil) then
@@ -2341,7 +2341,7 @@ function FOM_Feed(aFood, options)
foodLevel = selectedFoodLevel,
})
GFWUtils.DebugLog("Picked "..FOM_LastFood.." (bag "..foodBag..", slot "..foodItem..") for feeding.");
FOM_DebugLog("Picked "..FOM_LastFood.." (bag "..foodBag..", slot "..foodItem..") for feeding.");
if (FOM_Config.Debug) then
-- don't actually feed anything, just show what we would choose
return false;
@@ -2708,12 +2708,12 @@ end
function FOM_IsUsefulFood(itemID, quantity)
local foodName = GetItemInfo(itemID);
if (foodName == nil) then
GFWUtils.DebugLog("Can't get info for item ID "..itemID..", assuming it's OK to eat.");
FOM_DebugLog("Can't get info for item ID "..itemID..", assuming it's OK to eat.");
return false;
end
if (FOM_Cooking and FOM_Cooking[FOM_RealmPlayer] and FOM_Cooking[FOM_RealmPlayer][itemID]) then
if (FOM_Cooking[FOM_RealmPlayer][itemID] >= FOM_Config.SaveForCookingLevel) then
GFWUtils.DebugLog("Skipping "..quantity.."x "..foodName.."; is good for cooking.");
FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; is good for cooking.");
return true;
end
end
@@ -2725,19 +2725,19 @@ function FOM_IsUsefulFood(itemID, quantity)
FOM_Quantity[foodName] = FOM_Quantity[foodName] + quantity;
end
if (FOM_Quantity[foodName] > FOM_QuestFood[FOM_RealmPlayer][foodName]) then
GFWUtils.DebugLog("Not skipping "..quantity.."x "..foodName.."; is needed for quest, but we have more than enough.");
FOM_DebugLog("Not skipping "..quantity.."x "..foodName.."; is needed for quest, but we have more than enough.");
return false;
else
GFWUtils.DebugLog("Skipping "..quantity.."x "..foodName.."; is needed for quest.");
FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; is needed for quest.");
return true;
end
end
end
if (FOM_Config.AvoidBonusFood and FOM_IsInDiet(itemID, FOM_DIET_BONUS)) then
GFWUtils.DebugLog("Skipping "..quantity.."x "..foodName.."; has bonus effect when eaten by player.");
FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; has bonus effect when eaten by player.");
return true;
end
--GFWUtils.DebugLog("Not skipping "..quantity.."x "..foodName.."; doesn't have other uses.");
--FOM_DebugLog("Not skipping "..quantity.."x "..foodName.."; doesn't have other uses.");
return false;
end
@@ -2775,7 +2775,7 @@ function FOM_IsInDiet(food, dietList)
diet = "";
end
if (FOM_Foods[diet] == nil) then
GFWUtils.DebugLog("FOM_Foods[diet] == nil");
FOM_DebugLog("FOM_Foods[diet] == nil");
end
if (FOM_RemovedFoods ~= nil and FOM_RemovedFoods[diet] ~= nil and GFWTable.IndexOf(FOM_RemovedFoods[diet], food) ~= 0) then
return false;
+1 -1
View File
@@ -6,7 +6,7 @@
local MTH_FeedOMatic = {
name = "feedomatic",
enabled = false,
version = "1.4.0",
version = "1.4.1",
events = {
"VARIABLES_LOADED",
"MERCHANT_SHOW",
+1 -1
View File
@@ -6,7 +6,7 @@
local MTH_ICU = {
name = "icu",
enabled = false,
version = "1.4.0",
version = "1.4.1",
events = {
"VARIABLES_LOADED",
},
+21
View File
@@ -48,10 +48,25 @@ end
local MTHSmartAmmo_InitializeHooks
MTHSmartAmmo_EnsureHooks = nil
local MTH_SA_SPELL_EVENTS = {
"START_AUTOREPEAT_SPELL",
"STOP_AUTOREPEAT_SPELL",
"SPELLCAST_INTERRUPTED",
"SPELLCAST_FAILED",
"SPELLCAST_STOP",
"SPELLCAST_DELAYED",
}
function MTHSmartAmmo_SetSmartEnabled(enabled, silent)
local saved = MTHSmartAmmo_GetSaved()
if enabled then
saved["enabled"] = 1
local evFrame = getglobal("MTH_SA_EventFrame")
if evFrame then
for _, ev in ipairs(MTH_SA_SPELL_EVENTS) do
evFrame:RegisterEvent(ev)
end
end
if MTHSmartAmmo_InitializeHooks then
MTHSmartAmmo_InitializeHooks()
end
@@ -63,6 +78,12 @@ function MTHSmartAmmo_SetSmartEnabled(enabled, silent)
end
else
saved["enabled"] = false
local evFrame = getglobal("MTH_SA_EventFrame")
if evFrame then
for _, ev in ipairs(MTH_SA_SPELL_EVENTS) do
evFrame:UnregisterEvent(ev)
end
end
if not silent and DEFAULT_CHAT_FRAME then
MTH_SA_Print("Smart Ammo Disabled.")
end
+1 -1
View File
@@ -7,7 +7,7 @@ MTH_SA_MANAGED_HOOKS = true
local MTH_SmartAmmo = {
name = "smartammo",
enabled = true,
version = "1.4.0",
version = "1.4.1",
events = {
"VARIABLES_LOADED",
-- PLAYER_ENTERING_WORLD handled by engine's own frame
+1 -1
View File
@@ -6,7 +6,7 @@
local MTH_Tooltips = {
name = "tooltips",
enabled = true,
version = "1.4.0",
version = "1.4.1",
events = {
"UPDATE_MOUSEOVER_UNIT",
"UNIT_NAME_UPDATE",
+1
View File
@@ -86,6 +86,7 @@ ZHUNTER_TRACK_GIANTS = "Track Giants"
ZHUNTER_TRACK_MINERALS = "Find Minerals"
ZHUNTER_TRACK_HERBS = "Find Herbs"
ZHUNTER_TRACK_TREASURE = "Find Treasure"
ZHUNTER_TRACK_TREES = "Find Trees"
ZHUNTER_TRAP_FREEZING = "Freezing Trap"
ZHUNTER_TRAP_FROST = "Frost Trap"
+1 -1
View File
@@ -8,7 +8,7 @@ MTH_ZH_MANAGED_HOOKS = true
local MTH_ZHunter = {
name = "zhunter",
enabled = true,
version = "1.4.0",
version = "1.4.1",
events = {
"VARIABLES_LOADED",
-- PLAYER_ENTERING_WORLD handled by bootstrap frame + adjustment frames
+5 -2
View File
@@ -249,7 +249,8 @@ end
function zButtonMounts_SetupSizeAndPosition()
local saved = zButtonMounts_GetSaved()
if saved["enabled"] == false or saved["enabled"] == 0 then
if MTH_ZH_MountsAdjust and MTH_ZH_MountsAdjust.SetScript then
if MTH_ZH_MountsAdjust then
MTH_ZH_MountsAdjust:UnregisterAllEvents()
MTH_ZH_MountsAdjust:SetScript("OnEvent", nil)
end
if zButtonMounts and zButtonMounts.Hide then
@@ -257,7 +258,9 @@ function zButtonMounts_SetupSizeAndPosition()
end
return
end
if MTH_ZH_MountsAdjust and MTH_ZH_MountsAdjust.SetScript then
if MTH_ZH_MountsAdjust then
MTH_ZH_MountsAdjust:RegisterEvent("SPELLS_CHANGED")
MTH_ZH_MountsAdjust:RegisterEvent("PLAYER_ENTERING_WORLD")
MTH_ZH_MountsAdjust:SetScript("OnEvent", MTH_ZH_MountsAdjust_OnEvent)
end
local displayCount = zButtonMounts.found or 0
+10 -2
View File
@@ -235,7 +235,8 @@ end
function zButtonPet_SetupSizeAndPosition()
local saved = zButtonPet_GetSaved()
if saved["enabled"] == false or saved["enabled"] == 0 then
if MTH_ZH_PetAdjust and MTH_ZH_PetAdjust.SetScript then
if MTH_ZH_PetAdjust then
MTH_ZH_PetAdjust:UnregisterAllEvents()
MTH_ZH_PetAdjust:SetScript("OnEvent", nil)
end
if zButtonPet and zButtonPet.Hide then
@@ -243,7 +244,14 @@ function zButtonPet_SetupSizeAndPosition()
end
return
end
if MTH_ZH_PetAdjust and MTH_ZH_PetAdjust.SetScript then
if MTH_ZH_PetAdjust then
MTH_ZH_PetAdjust:RegisterEvent("UNIT_HEALTH")
MTH_ZH_PetAdjust:RegisterEvent("UNIT_HAPPINESS")
MTH_ZH_PetAdjust:RegisterEvent("UNIT_PET")
MTH_ZH_PetAdjust:RegisterEvent("PET_BAR_UPDATE")
MTH_ZH_PetAdjust:RegisterEvent("PLAYER_ENTERING_WORLD")
MTH_ZH_PetAdjust:RegisterEvent("SPELLS_CHANGED")
MTH_ZH_PetAdjust:RegisterEvent("LEARNED_SPELL_IN_TAB")
MTH_ZH_PetAdjust:SetScript("OnEvent", MTH_ZH_PetAdjust_OnEvent)
end
local displayCount = zButtonPet.found or ZHUNTER_PET_MAX
+5 -2
View File
@@ -249,7 +249,8 @@ end
function zButtonToys_SetupSizeAndPosition()
local saved = zButtonToys_GetSaved()
if saved["enabled"] == false or saved["enabled"] == 0 then
if MTH_ZH_ToysAdjust and MTH_ZH_ToysAdjust.SetScript then
if MTH_ZH_ToysAdjust then
MTH_ZH_ToysAdjust:UnregisterAllEvents()
MTH_ZH_ToysAdjust:SetScript("OnEvent", nil)
end
if zButtonToys and zButtonToys.Hide then
@@ -257,7 +258,9 @@ function zButtonToys_SetupSizeAndPosition()
end
return
end
if MTH_ZH_ToysAdjust and MTH_ZH_ToysAdjust.SetScript then
if MTH_ZH_ToysAdjust then
MTH_ZH_ToysAdjust:RegisterEvent("SPELLS_CHANGED")
MTH_ZH_ToysAdjust:RegisterEvent("PLAYER_ENTERING_WORLD")
MTH_ZH_ToysAdjust:SetScript("OnEvent", MTH_ZH_ToysAdjust_OnEvent)
end
local displayCount = zButtonToys.found or 0
+11 -4
View File
@@ -5,7 +5,7 @@ end
local root = zButtonTrack_GetRoot()
if not root["zButtonTrack"] then
root["zButtonTrack"] = {}
root["zButtonTrack"]["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
root["zButtonTrack"]["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
root["zButtonTrack"]["rows"] = 1
root["zButtonTrack"]["horizontal"] = nil
root["zButtonTrack"]["vertical"] = nil
@@ -32,7 +32,8 @@ ZHunterMod_Track_Spells = {
ZHUNTER_TRACK_GIANTS,
ZHUNTER_TRACK_MINERALS,
ZHUNTER_TRACK_HERBS,
ZHUNTER_TRACK_TREASURE
ZHUNTER_TRACK_TREASURE,
ZHUNTER_TRACK_TREES
}
local ZHUNTER_TRACK_MAX = table.getn(ZHunterMod_Track_Spells)
@@ -311,7 +312,8 @@ function zButtonTrack_SetupSizeAndPosition()
zButtonTrack_EnsureConfig()
local saved = zButtonTrack_GetSaved()
if saved["enabled"] == false or saved["enabled"] == 0 then
if MTH_ZH_TrackAdjust and MTH_ZH_TrackAdjust.SetScript then
if MTH_ZH_TrackAdjust then
MTH_ZH_TrackAdjust:UnregisterAllEvents()
MTH_ZH_TrackAdjust:SetScript("OnEvent", nil)
end
if zButtonTrack and zButtonTrack.Hide then
@@ -319,7 +321,12 @@ function zButtonTrack_SetupSizeAndPosition()
end
return
end
if MTH_ZH_TrackAdjust and MTH_ZH_TrackAdjust.SetScript then
if MTH_ZH_TrackAdjust then
MTH_ZH_TrackAdjust:RegisterEvent("MINIMAP_UPDATE_TRACKING")
MTH_ZH_TrackAdjust:RegisterEvent("PLAYER_ENTERING_WORLD")
MTH_ZH_TrackAdjust:RegisterEvent("SPELLS_CHANGED")
MTH_ZH_TrackAdjust:RegisterEvent("CHARACTER_POINTS_CHANGED")
MTH_ZH_TrackAdjust:RegisterEvent("LEARNED_SPELL_IN_TAB")
MTH_ZH_TrackAdjust:SetScript("OnEvent", MTH_ZH_TrackAdjust_OnEvent)
end
local displayCount = zButtonTrack.found or ZHUNTER_TRACK_MAX
+8 -2
View File
@@ -154,7 +154,8 @@ function zButtonTrap_SetupSizeAndPosition()
zButtonTrap_EnsureConfig()
local saved = zButtonTrap_GetSaved()
if saved["enabled"] == false or saved["enabled"] == 0 then
if MTH_ZH_TrapAdjust and MTH_ZH_TrapAdjust.SetScript then
if MTH_ZH_TrapAdjust then
MTH_ZH_TrapAdjust:UnregisterAllEvents()
MTH_ZH_TrapAdjust:SetScript("OnEvent", nil)
end
if zButtonTrap and zButtonTrap.Hide then
@@ -162,7 +163,12 @@ function zButtonTrap_SetupSizeAndPosition()
end
return
end
if MTH_ZH_TrapAdjust and MTH_ZH_TrapAdjust.SetScript then
if MTH_ZH_TrapAdjust then
MTH_ZH_TrapAdjust:RegisterEvent("PLAYER_REGEN_ENABLED")
MTH_ZH_TrapAdjust:RegisterEvent("PLAYER_REGEN_DISABLED")
MTH_ZH_TrapAdjust:RegisterEvent("PLAYER_ENTERING_WORLD")
MTH_ZH_TrapAdjust:RegisterEvent("SPELLS_CHANGED")
MTH_ZH_TrapAdjust:RegisterEvent("CHARACTER_POINTS_CHANGED")
MTH_ZH_TrapAdjust:SetScript("OnEvent", MTH_ZH_TrapAdjust_OnEvent)
end
local displayCount = zButtonTrap.found or ZHUNTER_TRAP_MAX