6 Commits

Author SHA1 Message Date
Duvel c81ce85950 Merge pull request #13 from DuvelCorp/V1.4.1
v1.4.1 event leak fixes, chronometer trap fix, profile memory, autost…
2026-03-24 14:24:08 +01:00
DuvelCorp 680ddcba8a v1.4.1 event leak fixes, chronometer trap fix, profile memory, autostrip alloc fix 2026-03-24 14:21:58 +01:00
Duvel 5c08656532 Merge pull request #12 from DuvelCorp/V1.4.1
V1.4.1 performance fixes
2026-03-24 14:15:25 +01:00
DuvelCorp 8d80d0663c V1.4.1 performance fixes 2026-03-24 14:14:33 +01:00
Duvel 43e23e7616 Merge pull request #10 from DuvelCorp/v1.4.0
v1.4.0 Twow 1.18.1
2026-03-23 22:28:09 +01:00
Duvel f7b0318179 Add model viewer feature to README
Added model viewer feature for all beasts in the game.
2026-03-19 16:36:01 +01:00
27 changed files with 302 additions and 120 deletions
+25 -3
View File
@@ -2,17 +2,39 @@
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. Additionaly, the beast inspector has been reformated and all meaningful beast's data is displayed nicely.
- 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.
- All new and missing **Beasts Models** have been (re)scanned from MPQ game files and are viewable in the Beast Lore page, and browsable in the Family Model viewer.
- Added **MM widget** module: tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock and Load procs, and has adynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. Lets Make MM great again.
- Added **MM widget** module: tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock and Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. Lets Make MM great again.
- Added new **Moth** pet family.
@@ -46,7 +68,7 @@ All notable changes to MetaHunt will be documented in this file.
### Fixed
- Fixed **Chronometer** bars appearing from nearby players' combat events (e.g. another hunter's traps or DoTs triggering your own bars).
- Fixed the list in Families Book's page, that wasnt tall enough to display all famillies (wolves were hidden).
- Fixed the list in Families Book's page, that wasn't tall enough to display all families (wolves were hidden).
## [1.3.0] - 2026-03-19
+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.
+8 -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,12 +63,16 @@ 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
The Book is the main GUI of MetaHunt, it allows to display the data constantly collected about you and your pet, and browse the addon's data to find anything a Hunter needs.
- Browse the MetaHunt datastores of NPCs (Beast, vendors, masters) and open the map to locate them like in pfQuest
- Model viewer of all beasts in the game.
- Track all info about the pet abilities that you know and don't know yet
- Track all info about your stabled pets
- Keep an history of all your pets after you abandon them or if they run away.
+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