Files
MetaHunt/api/core-framework.lua
T
2026-02-24 23:00:59 +01:00

1510 lines
44 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
MTH = MTH or {
version = "1.0.3",
name = "MetaHunt",
modules = {},
config = {},
debug = false,
}
local MTH_CHAT_PREFIX = "|cFFFFFFFF[|r|cFFD14A4AMeta|r|cFFABD473Hunt|r|cFFFFFFFF]|r "
local MTH_NON_HUNTER_BLOCK_MESSAGE = "You are not a hunter and you are not allowed here. Disable the add-on for this character."
local function MTH_ClassGateTrace(_step, _detail)
return
end
local function MTH_ClassGateClearAnnounceFrame(self)
if not self or not self._classGateAnnounceFrame then
return
end
self._classGateAnnounceFrame:UnregisterAllEvents()
self._classGateAnnounceFrame:SetScript("OnEvent", nil)
self._classGateAnnounceFrame:SetScript("OnUpdate", nil)
self._classGateAnnounceFrame = nil
end
local function MTH_ClassGateEnsureAnnounceFrame(self)
if not self or self._classGateAnnounceFrame then
return
end
local frame = CreateFrame("Frame", "MTH_ClassGateAnnounceFrame")
if not frame then
return
end
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:SetScript("OnEvent", function()
if MTH and MTH.AnnounceClassGateBlocked then
MTH:AnnounceClassGateBlocked()
end
end)
frame._mthElapsed = 0
frame:SetScript("OnUpdate", function(self, elapsed)
self._mthElapsed = (self._mthElapsed or 0) + (elapsed or arg1 or 0)
if self._mthElapsed < 0.5 then
return
end
self._mthElapsed = 0
if MTH and MTH._classGateAnnouncedChat then
MTH_ClassGateClearAnnounceFrame(MTH)
return
end
if MTH and MTH._classGateAnnouncePending and MTH.AnnounceClassGateBlocked then
MTH:AnnounceClassGateBlocked()
end
end)
self._classGateAnnounceFrame = frame
end
local function MTH_TryOutputClassGateMessage(text)
local msg = tostring(text or "")
if msg == "" then
return nil
end
local formattedMsg = MTH_CHAT_PREFIX .. msg
if MTH and MTH.Log and DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
MTH:Log(msg)
return "chat"
end
local shown = false
local sink = nil
local function tryFrame(frame)
if frame and frame.AddMessage then
frame:AddMessage(formattedMsg, 1, 0.25, 0.25)
shown = true
sink = "chat"
end
end
tryFrame(DEFAULT_CHAT_FRAME)
if not shown and type(getglobal) == "function" then
tryFrame(getglobal("ChatFrame1"))
if not shown then
for i = 2, 7 do
tryFrame(getglobal("ChatFrame" .. tostring(i)))
if shown then break end
end
end
end
if not shown and _G then
tryFrame(_G["ChatFrame1"])
if not shown then
for i = 2, 7 do
tryFrame(_G["ChatFrame" .. tostring(i)])
if shown then break end
end
end
end
if shown then
return sink
end
if UIErrorsFrame and UIErrorsFrame.AddMessage then
UIErrorsFrame:AddMessage(formattedMsg, 1, 0.25, 0.25, 1.0)
return "uierrors"
end
if type(print) == "function" then
print(formattedMsg)
return "print"
end
return nil
end
function MTH:IsClassGateBlocked()
return self and self._classGateBlocked and true or false
end
function MTH:IsHardBlocked()
return self and self._hardBlocked and true or false
end
function MTH:CheckClassGate()
if type(UnitClass) ~= "function" then
return self:IsClassGateBlocked()
end
local className, classToken = UnitClass("player")
if type(classToken) == "string" and classToken ~= "" then
self._playerClassToken = classToken
self._classGateBlocked = classToken ~= "HUNTER"
end
return self:IsClassGateBlocked()
end
function MTH:AnnounceClassGateBlocked()
if self._classGateAnnouncedChat then
return
end
if type(IsLoggedIn) == "function" and not IsLoggedIn() then
self._classGateAnnouncePending = true
MTH_ClassGateEnsureAnnounceFrame(self)
return
end
local sink = MTH_TryOutputClassGateMessage(MTH_NON_HUNTER_BLOCK_MESSAGE)
if sink == "chat" or sink == "uierrors" then
self._classGateAnnouncedChat = true
self._classGateAnnounced = true
self._classGateAnnouncePending = nil
MTH_ClassGateClearAnnounceFrame(self)
return
end
self._classGateAnnouncePending = true
MTH_ClassGateEnsureAnnounceFrame(self)
end
function MTH:ApplyClassGate(_source)
local blocked = self:CheckClassGate()
if blocked then
self:AnnounceClassGateBlocked()
if self.ShutdownForNonHunter then
self:ShutdownForNonHunter(_source or "class-gate")
end
end
return blocked
end
MTH:ApplyClassGate("startup")
if not MTH._classGateLifecycleFrame then
local classGateFrame = CreateFrame("Frame", "MTH_ClassGateLifecycleFrame")
if classGateFrame then
classGateFrame:RegisterEvent("PLAYER_LOGIN")
classGateFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
classGateFrame:SetScript("OnEvent", function(self, eventName)
self = self or this
eventName = eventName or event
if MTH and eventName == "PLAYER_LOGIN" then
MTH._classGateAnnouncedChat = nil
MTH._classGateAnnounced = nil
MTH._classGateAnnouncePending = nil
end
if MTH and MTH.ApplyClassGate and MTH:ApplyClassGate(eventName) and MTH._classGateAnnouncedChat then
self:UnregisterAllEvents()
self:SetScript("OnEvent", nil)
end
end)
MTH._classGateLifecycleFrame = classGateFrame
end
end
MTH.Const = MTH.Const or {}
if type(MTH.Const.SlashAliases) ~= "table" then
MTH.Const.SlashAliases = { "/mth", "/metahunt" }
end
if not ITEM_QUALITY_COLORS then
ITEM_QUALITY_COLORS = {
[0] = { r = 0.62, g = 0.62, b = 0.62 },
[1] = { r = 1.00, g = 1.00, b = 1.00 },
[2] = { r = 0.12, g = 1.00, b = 0.00 },
[3] = { r = 0.00, g = 0.44, b = 0.87 },
[4] = { r = 0.64, g = 0.21, b = 0.93 },
[5] = { r = 1.00, g = 0.50, b = 0.00 },
}
end
MTH_ITEM_QUALITY_COLORS = ITEM_QUALITY_COLORS
function MTH:RegisterSlashAliases(key, aliases)
if type(key) ~= "string" or key == "" then
return
end
if type(aliases) ~= "table" then
aliases = self.Const and self.Const.SlashAliases or nil
end
if type(aliases) ~= "table" then
return
end
for i = 1, table.getn(aliases) do
local alias = aliases[i]
if type(alias) == "string" and alias ~= "" then
_G["SLASH_" .. key .. i] = alias
end
end
end
if type(SlashCmdList) ~= "table" then
SlashCmdList = {}
end
if type(MTH_SavedVariables) ~= "table" then
MTH_SavedVariables = {}
end
if type(MTH_SavedVariables.modules) ~= "table" then
MTH_SavedVariables.modules = {}
end
if type(MTH_CharSavedVariables) ~= "table" then
MTH_CharSavedVariables = {}
end
if type(MTH_CharSavedVariables.modules) ~= "table" then
MTH_CharSavedVariables.modules = {}
end
local function MTH_ModuleStateDebug(message)
local text = "[MODULE STATE] " .. tostring(message or "")
if MTH and MTH.debug and MTH.Log then
MTH:Log(text, "debug")
elseif MTH and MTH.debug and MTH.Print then
MTH:Print(text, "debug")
end
end
local function MTH_IsPetUiFrameName(frameName)
local name = tostring(frameName or "")
if name == "" then
return false
end
return (
string.find(name, "PetFrame", 1, true)
or string.find(name, "PetName", 1, true)
or string.find(name, "PetActionButton", 1, true)
or string.find(name, "PetActionBar", 1, true)
or string.find(name, "PetBar", 1, true)
or string.find(name, "PetButton", 1, true)
or string.find(name, "ShaguPet", 1, true)
or string.find(name, "pfPet", 1, true)
or string.find(name, "DominosActionButton", 1, true)
or string.find(name, "BT4PetButton", 1, true)
or string.find(name, "DominosPet", 1, true)
) and true or false
end
local function MTH_DetectPetUiFromFrameChain(frame)
local state = {
matched = false,
matchedFrameName = nil,
}
local current = frame
local depth = 0
while current and depth < 6 do
local frameName = (type(current.GetName) == "function") and current:GetName() or ""
if MTH_IsPetUiFrameName(frameName) then
state.matched = true
state.matchedFrameName = tostring(frameName or "")
return state
end
if type(current.GetParent) == "function" then
current = current:GetParent()
else
current = nil
end
depth = depth + 1
end
return state
end
function MTH_IsMouseoverOwnPet()
if type(UnitClass) == "function" then
local _, classToken = UnitClass("player")
if tostring(classToken or "") ~= "HUNTER" then
return false
end
end
if type(UnitExists) == "function" and type(UnitIsUnit) == "function" then
if UnitExists("pet") and UnitExists("mouseover") then
local ok, sameUnit = pcall(UnitIsUnit, "mouseover", "pet")
if ok and sameUnit then
return true
end
end
end
if GameTooltip and type(GameTooltip.GetOwner) == "function" then
local owner = GameTooltip:GetOwner()
local tooltipCheck = MTH_DetectPetUiFromFrameChain(owner)
if tooltipCheck and tooltipCheck.matched then
return true
end
end
local getMouseFocus = (_G and _G["GetMouseFocus"]) or nil
if type(getMouseFocus) ~= "function" then
return false
end
local focus = getMouseFocus()
local focusCheck = MTH_DetectPetUiFromFrameChain(focus)
return focusCheck and focusCheck.matched and true or false
end
function MTH_GetMouseoverOwnPetState()
local state = {
isHunter = false,
hasPet = false,
mouseoverIsPetUnit = false,
mouseoverIsPetFrame = false,
matchedFrameName = nil,
detected = false,
}
if type(UnitClass) == "function" then
local _, classToken = UnitClass("player")
state.isHunter = tostring(classToken or "") == "HUNTER"
else
state.isHunter = true
end
if type(UnitExists) == "function" then
state.hasPet = UnitExists("pet") and true or false
end
if type(UnitExists) == "function" and type(UnitIsUnit) == "function" then
if UnitExists("pet") and UnitExists("mouseover") then
local ok, sameUnit = pcall(UnitIsUnit, "mouseover", "pet")
state.mouseoverIsPetUnit = ok and sameUnit and true or false
end
end
local getMouseFocus = (_G and _G["GetMouseFocus"]) or nil
if type(getMouseFocus) == "function" then
local focus = getMouseFocus()
local focusCheck = MTH_DetectPetUiFromFrameChain(focus)
if focusCheck and focusCheck.matched then
state.mouseoverIsPetFrame = true
state.matchedFrameName = focusCheck.matchedFrameName
end
end
if not state.mouseoverIsPetFrame and GameTooltip and type(GameTooltip.GetOwner) == "function" then
local owner = GameTooltip:GetOwner()
local tooltipCheck = MTH_DetectPetUiFromFrameChain(owner)
if tooltipCheck and tooltipCheck.matched then
state.mouseoverIsPetFrame = true
state.matchedFrameName = tooltipCheck.matchedFrameName
end
end
state.detected = state.isHunter and (state.mouseoverIsPetUnit or state.mouseoverIsPetFrame) and true or false
return state
end
local function MTH_NormalizeModuleName(name)
if type(name) ~= "string" then
return name
end
local lowered = string.lower(name)
if lowered == "zhunter" then
return "zhunter"
end
return name
end
local function MTH_ModuleNameCandidates(name)
local normalized = MTH_NormalizeModuleName(name)
return { normalized }
end
local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
if type(name) ~= "string" or name == "" then
MTH_ModuleStateDebug("read <invalid-name> fallback-default=" .. tostring(defaultEnabled and true or false))
return defaultEnabled and true or false
end
local resolvedName = MTH_NormalizeModuleName(name)
local candidates = MTH_ModuleNameCandidates(name)
if type(MTH_SavedVariables) ~= "table" then
MTH_ModuleStateDebug("read " .. tostring(name) .. " no-savedvars fallback-default=" .. tostring(defaultEnabled and true or false))
return defaultEnabled and true or false
end
if type(MTH_SavedVariables.moduleStates) == "table" then
for i = 1, table.getn(candidates) do
local key = candidates[i]
if MTH_SavedVariables.moduleStates[key] ~= nil then
local resolved = MTH_SavedVariables.moduleStates[key] and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from moduleStates." .. tostring(key) .. "=" .. tostring(resolved))
MTH_SavedVariables.moduleStates[resolvedName] = resolved
return resolved
end
end
end
if type(MTH_CharSavedVariables) == "table"
and type(MTH_CharSavedVariables.moduleStates) == "table"
then
for i = 1, table.getn(candidates) do
local key = candidates[i]
if MTH_CharSavedVariables.moduleStates[key] ~= nil then
local resolved = MTH_CharSavedVariables.moduleStates[key] and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from char.moduleStates." .. tostring(key) .. "=" .. tostring(resolved))
if type(MTH_SavedVariables.moduleStates) ~= "table" then
MTH_SavedVariables.moduleStates = {}
end
MTH_SavedVariables.moduleStates[resolvedName] = resolved
MTH_CharSavedVariables.moduleStates[resolvedName] = resolved
return resolved
end
end
end
if type(MTH_SavedVariables.modules) == "table" then
for i = 1, table.getn(candidates) do
local key = candidates[i]
if type(MTH_SavedVariables.modules[key]) == "table" and MTH_SavedVariables.modules[key].enabled ~= nil then
local resolved = MTH_SavedVariables.modules[key].enabled and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from modules." .. tostring(key) .. ".enabled=" .. tostring(resolved))
if type(MTH_SavedVariables.modules[resolvedName]) ~= "table" then
MTH_SavedVariables.modules[resolvedName] = MTH_SavedVariables.modules[key]
end
return resolved
end
end
end
for i = 1, table.getn(candidates) do
local key = candidates[i]
if type(MTH_SavedVariables[key]) == "table" and MTH_SavedVariables[key].enabled ~= nil then
local resolved = MTH_SavedVariables[key].enabled and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from legacy-top-level." .. tostring(key) .. ".enabled=" .. tostring(resolved))
if type(MTH_SavedVariables.modules) ~= "table" then
MTH_SavedVariables.modules = {}
end
if type(MTH_SavedVariables.modules[resolvedName]) ~= "table" then
MTH_SavedVariables.modules[resolvedName] = MTH_SavedVariables[key]
end
if type(MTH_SavedVariables.moduleStates) ~= "table" then
MTH_SavedVariables.moduleStates = {}
end
MTH_SavedVariables.moduleStates[resolvedName] = resolved
return resolved
end
end
MTH_ModuleStateDebug("read " .. tostring(name) .. " no-persisted-value fallback-default=" .. tostring(defaultEnabled and true or false))
return defaultEnabled and true or false
end
local function MTH_SetPersistedModuleEnabled(name, enabled)
if type(name) ~= "string" or name == "" then
return
end
name = MTH_NormalizeModuleName(name)
if type(MTH_SavedVariables) ~= "table" then
MTH_SavedVariables = {}
end
if type(MTH_SavedVariables.moduleStates) ~= "table" then
MTH_SavedVariables.moduleStates = {}
end
if type(MTH_SavedVariables.modules) ~= "table" then
MTH_SavedVariables.modules = {}
end
if type(MTH_SavedVariables.modules[name]) ~= "table" then
MTH_SavedVariables.modules[name] = {}
end
if type(MTH_CharSavedVariables) ~= "table" then
MTH_CharSavedVariables = {}
end
if type(MTH_CharSavedVariables.moduleStates) ~= "table" then
MTH_CharSavedVariables.moduleStates = {}
end
enabled = enabled and true or false
MTH_SavedVariables.moduleStates[name] = enabled
MTH_SavedVariables.modules[name].enabled = enabled
MTH_SavedVariables[name] = MTH_SavedVariables.modules[name]
MTH_CharSavedVariables.moduleStates[name] = enabled
MTH_ModuleStateDebug("write " .. tostring(name) .. " enabled=" .. tostring(enabled))
end
function MTH:InitEventRouter()
if self._eventRouter and self._eventRouter.frame then
return self._eventRouter.frame
end
local frame = CreateFrame("Frame", "MTH_EventRouterFrame")
frame:SetScript("OnEvent", function(self, evt, a1, a2, a3, a4, a5, a6, a7, a8, a9)
if MTH and MTH.DispatchEvent then
MTH:DispatchEvent(evt, a1, a2, a3, a4, a5, a6, a7, a8, a9)
end
end)
self._eventRouter = {
frame = frame,
eventRefs = {},
}
return frame
end
function MTH:RegisterModuleEvents(name, events)
local module = self.modules[name]
if not module then
return false, "module not registered"
end
local eventSet = {}
if type(events) == "table" then
for key, value in pairs(events) do
if type(key) == "number" and type(value) == "string" and value ~= "" then
eventSet[value] = true
elseif type(key) == "string" and value then
eventSet[key] = true
end
end
end
module._mth_eventSet = eventSet
return true
end
function MTH:_SetModuleEventSubscriptions(name, enabled)
local module = self.modules[name]
if not module or not module._mth_eventSet then
return
end
local frame = self:InitEventRouter()
local refs = self._eventRouter.eventRefs
for evt in pairs(module._mth_eventSet) do
if enabled then
refs[evt] = (refs[evt] or 0) + 1
if refs[evt] == 1 then
frame:RegisterEvent(evt)
end
else
if refs[evt] and refs[evt] > 0 then
refs[evt] = refs[evt] - 1
if refs[evt] <= 0 then
refs[evt] = nil
frame:UnregisterEvent(evt)
end
end
end
end
end
function MTH:DispatchEvent(evt, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
for name, module in pairs(self.modules) do
if module.enabled and module.onEvent then
local allow = true
if module._mth_eventSet then
allow = module._mth_eventSet[evt] and true or false
end
if allow then
local ok, err = pcall(function()
module:onEvent(evt, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
end)
if not ok then
self:Print("Module '" .. name .. "' event error on '" .. tostring(evt) .. "': " .. tostring(err))
MTH:Print("[MTH] Module event error (" .. name .. ", " .. tostring(evt) .. "): " .. tostring(err), "error")
end
end
end
end
end
function MTH:RegisterModule(name, module)
if self:IsHardBlocked() then
self:AnnounceClassGateBlocked()
return false
end
MTH_ClassGateTrace("RegisterModule enter", "name=" .. tostring(name) .. " blocked=" .. tostring(self:IsClassGateBlocked()))
if self:IsClassGateBlocked() then
MTH_ClassGateTrace("RegisterModule blocked", "name=" .. tostring(name))
self:AnnounceClassGateBlocked()
return false
end
if type(name) ~= "string" or name == "" then
self:Print("Cannot register module: invalid module name")
return false
end
if type(module) ~= "table" then
self:Print("Cannot register module '" .. name .. "': module must be a table")
return false
end
if self.modules[name] then
if self.debug then print("|cFFFFAA00MetaHunt|r: Module '" .. name .. "' already registered") end
return false
end
module.name = module.name or name
module._mth_initialized = false
local defaultEnabled = true
if module.enabled ~= nil then
defaultEnabled = module.enabled and true or false
end
module._mth_defaultEnabled = defaultEnabled
local initialEnabled = MTH_GetPersistedModuleEnabled(name, defaultEnabled)
module.enabled = initialEnabled and true or false
MTH_ModuleStateDebug("register " .. tostring(name) .. " default=" .. tostring(defaultEnabled) .. " initial=" .. tostring(module.enabled))
self.modules[name] = module
self:RegisterModuleEvents(name, module.events)
if module.init and type(module.init) == "function" then
MTH_ClassGateTrace("RegisterModule init begin", "name=" .. tostring(name))
local ok, err = pcall(function()
module:init()
end)
if not ok then
MTH_ClassGateTrace("RegisterModule init failed", "name=" .. tostring(name) .. " err=" .. tostring(err))
module.enabled = false
self:Print("Module '" .. name .. "' init failed: " .. tostring(err))
MTH:Print("[MTH] Module init failed (" .. name .. "): " .. tostring(err), "error")
return false
end
MTH_ClassGateTrace("RegisterModule init end", "name=" .. tostring(name))
module._mth_initialized = true
end
if module.enabled then
self:_SetModuleEventSubscriptions(name, true)
end
if self.debug then print("|cFF00AA00MetaHunt|r: Module '" .. name .. "' registered") end
return true
end
function MTH:GetModule(name)
name = MTH_NormalizeModuleName(name)
return self.modules[name]
end
function MTH:IsModuleEnabled(name, defaultEnabled)
if self:IsHardBlocked() then
return false
end
if self:IsClassGateBlocked() then
return false
end
if defaultEnabled == nil then
defaultEnabled = true
end
if type(name) ~= "string" or name == "" then
return defaultEnabled and true or false
end
name = MTH_NormalizeModuleName(name)
local module = self:GetModule(name)
if not module then
return defaultEnabled and true or false
end
return module.enabled and true or false
end
function MTH:SetModuleEnabled(name, enabled)
if self:IsHardBlocked() then
if enabled then
self:AnnounceClassGateBlocked()
return false, "Class gate blocked"
end
return true
end
if self:IsClassGateBlocked() then
if enabled then
self:AnnounceClassGateBlocked()
return false, "Class gate blocked"
end
return true
end
name = MTH_NormalizeModuleName(name)
local module = self:GetModule(name)
if not module then
return false, "Module '" .. tostring(name) .. "' not found"
end
enabled = enabled and true or false
if module.enabled == enabled then
MTH_SetPersistedModuleEnabled(name, enabled)
if self.SetConfig then
self:SetConfig(name, "enabled", enabled)
end
MTH_ModuleStateDebug("request " .. tostring(name) .. " no-op runtime=" .. tostring(module.enabled) .. " persisted-synced")
return true
end
if module.setEnabled and type(module.setEnabled) == "function" then
local ok, err = pcall(function()
module:setEnabled(enabled)
end)
if not ok then
self:Print("Module '" .. name .. "' state change failed: " .. tostring(err))
MTH:Print("[MTH] Module state change failed (" .. name .. "): " .. tostring(err), "error")
return false, tostring(err)
end
end
self:_SetModuleEventSubscriptions(name, enabled)
module.enabled = enabled
MTH_SetPersistedModuleEnabled(name, enabled)
if self.SetConfig then
self:SetConfig(name, "enabled", enabled)
end
MTH_ModuleStateDebug("request " .. tostring(name) .. " applied runtime=" .. tostring(module.enabled))
return true
end
function MTH:UnloadModule(name)
local module = self:GetModule(name)
if module then
if module.enabled then
self:_SetModuleEventSubscriptions(name, false)
end
if module.cleanup and type(module.cleanup) == "function" then
local ok, err = pcall(function()
module:cleanup()
end)
if not ok then
self:Print("Module '" .. name .. "' cleanup failed: " .. tostring(err))
MTH:Print("[MTH] Module cleanup failed (" .. name .. "): " .. tostring(err), "error")
end
end
self.modules[name] = nil
end
end
function MTH:FireEvent(event, arg1, arg2, arg3, arg4, arg5)
self:DispatchEvent(event, arg1, arg2, arg3, arg4, arg5)
end
local function MTH_GetGlobalByName(name)
if type(name) ~= "string" or name == "" then
return nil
end
if type(getglobal) == "function" then
return getglobal(name)
end
if _G then
return _G[name]
end
return nil
end
local function MTH_SetGlobalByName(name, value)
if type(name) ~= "string" or name == "" then
return
end
if type(setglobal) == "function" then
setglobal(name, value)
return
end
if _G then
_G[name] = value
end
end
local function MTH_GetHookOwnerStore()
if not MTH._globalHookOwners then
MTH._globalHookOwners = {}
end
return MTH._globalHookOwners
end
function MTH:GetGlobalHookOwner(globalName)
if type(globalName) ~= "string" or globalName == "" then
return nil
end
local owners = MTH_GetHookOwnerStore()
local owner = owners[globalName]
if type(owner) ~= "table" then
return nil
end
return owner.key
end
function MTH:GetGlobalHookOwnersSnapshot()
local snapshot = {}
local owners = MTH_GetHookOwnerStore()
for globalName, owner in pairs(owners) do
if type(globalName) == "string" and type(owner) == "table" then
local active = false
if type(owner.hookFunc) == "function" then
active = MTH_GetGlobalByName(globalName) == owner.hookFunc
end
snapshot[globalName] = {
key = owner.key,
active = active,
}
end
end
return snapshot
end
function MTH:PruneInactiveGlobalHookOwners()
local owners = MTH_GetHookOwnerStore()
local removed = 0
for globalName, owner in pairs(owners) do
local keep = false
if type(globalName) == "string" and type(owner) == "table" and type(owner.key) == "string" and owner.key ~= "" then
if type(owner.hookFunc) == "function" and MTH_GetGlobalByName(globalName) == owner.hookFunc then
keep = true
end
end
if not keep then
owners[globalName] = nil
removed = removed + 1
end
end
return removed
end
function MTH:CaptureHookBoundary(key, defs)
if type(key) ~= "string" or key == "" then
return nil, "invalid boundary key"
end
if type(defs) ~= "table" then
return nil, "invalid boundary definitions"
end
self._hookBoundaries = self._hookBoundaries or {}
local boundary = self._hookBoundaries[key]
if boundary and boundary.captured then
return boundary
end
boundary = {
captured = true,
entries = {},
}
local owners = MTH_GetHookOwnerStore()
for _, def in ipairs(defs) do
if type(def) == "table" and type(def.globalName) == "string" and def.globalName ~= "" then
local currentGlobal = MTH_GetGlobalByName(def.globalName)
local entry = {
globalName = def.globalName,
originalName = def.originalName,
hookFunc = currentGlobal,
originalFunc = MTH_GetGlobalByName(def.originalName),
}
local existingOwner = owners[def.globalName]
if type(existingOwner) == "table"
and existingOwner.key ~= key
and existingOwner.hookFunc
and currentGlobal == existingOwner.hookFunc
then
entry.ownerConflict = existingOwner.key
else
owners[def.globalName] = {
key = key,
hookFunc = currentGlobal,
}
end
table.insert(boundary.entries, entry)
end
end
self._hookBoundaries[key] = boundary
return boundary
end
function MTH:RestoreHookBoundary(key)
if type(key) ~= "string" or key == "" then
return false, "invalid boundary key"
end
local boundary = self._hookBoundaries and self._hookBoundaries[key]
if not boundary or not boundary.captured then
return false
end
local owners = MTH_GetHookOwnerStore()
for _, entry in ipairs(boundary.entries) do
if type(entry) == "table" and type(entry.globalName) == "string" then
local owner = owners[entry.globalName]
local ownerAllowsRestore = true
if type(owner) == "table" and owner.key and owner.key ~= key then
ownerAllowsRestore = false
end
local current = MTH_GetGlobalByName(entry.globalName)
if ownerAllowsRestore and type(entry.originalFunc) == "function" and current == entry.hookFunc then
MTH_SetGlobalByName(entry.globalName, entry.originalFunc)
end
if type(owner) == "table" and owner.key == key then
owners[entry.globalName] = nil
end
end
end
boundary.captured = false
return true
end
local function MTH_NormalizeSeverity(severity)
if severity == "debug" or severity == "error" then
return severity
end
return ""
end
local function MTH_BuildChatPrefix(severity)
return "|cFFFFFFFF[|r|cFFD14A4AMeta|r|cFFABD473Hunt|r|cFFFFFFFF]|r "
end
function MTH:Log(msg, severity)
local text = tostring(msg or "")
local level = MTH_NormalizeSeverity(severity)
local chatText = MTH_BuildChatPrefix(level) .. text
if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
if level == "debug" then
DEFAULT_CHAT_FRAME:AddMessage(chatText, 0.4, 0.8, 1)
elseif level == "error" then
DEFAULT_CHAT_FRAME:AddMessage(chatText, 1, 0.2, 0.2)
else
DEFAULT_CHAT_FRAME:AddMessage(chatText, 1, 0.9, 0.3)
end
else
print(chatText)
end
if MTH_DebugFrame then
if level == "debug" and MTH_DebugFrame.AddInfo then
MTH_DebugFrame:AddInfo(text)
elseif level == "error" and MTH_DebugFrame.AddError then
MTH_DebugFrame:AddError(text)
end
end
return chatText
end
function MTH:Print(msg, severity)
return self:Log(msg, severity)
end
function MTH:DebugPrint(msg)
if self.debug then
self:Print("[DEBUG] " .. tostring(msg), "debug")
end
end
function MTH_Log(message, severity)
if MTH and MTH.Log then
return MTH:Log(message, severity)
end
if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
DEFAULT_CHAT_FRAME:AddMessage("[MetaHunt] " .. tostring(message or ""), 1, 0.9, 0.3)
else
print("[MetaHunt] " .. tostring(message or ""))
end
end
local function MTH_SplitMoney(money)
local value = math.floor(tonumber(money) or 0)
if value < 0 then value = 0 end
local gold = math.floor(value / 10000)
local silver = math.floor((value - (gold * 10000)) / 100)
local copper = value - (gold * 10000) - (silver * 100)
return gold, silver, copper
end
function MTH_FormatMoney(money)
local value = math.floor(tonumber(money) or 0)
if value < 0 then value = 0 end
local gold, silver, copper = MTH_SplitMoney(value)
local text = ""
if gold > 0 then
text = text .. "|cffffffff" .. tostring(gold) .. "|cffffd700g"
end
if silver > 0 or gold > 0 then
text = text .. "|cffffffff " .. tostring(silver) .. "|cffc7c7cfs"
end
text = text .. "|cffffffff " .. tostring(copper) .. "|cffeda55fc"
return text
end
function MTH:FormatMoney(money)
return MTH_FormatMoney(money)
end
function MTH_IsModuleEnabled(name, defaultEnabled)
if not (MTH and MTH.IsModuleEnabled) then
if defaultEnabled == nil then
return true
end
return defaultEnabled and true or false
end
return MTH:IsModuleEnabled(name, defaultEnabled)
end
function MTH:InitSavedVariables()
if not MTH_SavedVariables then
MTH_SavedVariables = {
modules = {},
feedomatic = {},
zhunter = {},
}
end
if not MTH_SavedVariables.modules then
MTH_SavedVariables.modules = {}
end
if not MTH_SavedVariables.moduleStates then
MTH_SavedVariables.moduleStates = {}
end
if not MTH_SavedVariables.modules.feedomatic then
MTH_SavedVariables.modules.feedomatic = MTH_SavedVariables.feedomatic or {}
end
if not MTH_SavedVariables.modules.zhunter then
MTH_SavedVariables.modules.zhunter = MTH_SavedVariables.zhunter or {}
end
MTH_SavedVariables.feedomatic = MTH_SavedVariables.modules.feedomatic
MTH_SavedVariables.zhunter = MTH_SavedVariables.modules.zhunter
if not MTH_CharSavedVariables then
MTH_CharSavedVariables = {}
end
if not MTH_CharSavedVariables.modules then
MTH_CharSavedVariables.modules = {}
end
if not MTH_CharSavedVariables.moduleStates then
MTH_CharSavedVariables.moduleStates = {}
end
if not MTH_CharSavedVariables.petTraining then
MTH_CharSavedVariables.petTraining = {
spellMap = {},
lastScan = 0,
lastPrompt = 0,
hasCompletedPetScan = nil,
}
end
if not MTH_CharSavedVariables.trainScan then
MTH_CharSavedVariables.trainScan = MTH_CharSavedVariables.petTraining
end
if MTH_CharSavedVariables.petTraining ~= MTH_CharSavedVariables.trainScan then
MTH_CharSavedVariables.petTraining = MTH_CharSavedVariables.trainScan
end
if MTH_CharSavedVariables.petTraining.hasCompletedPetScan == nil then
local pt = MTH_CharSavedVariables.petTraining
if tonumber(pt.lastScan) and tonumber(pt.lastScan) > 0 then
pt.hasCompletedPetScan = 1
elseif pt.spellMap then
for _ in pairs(pt.spellMap) do
pt.hasCompletedPetScan = 1
break
end
end
end
if type(MTH_PETS_GetRootStore) == "function" then
local pets = MTH_PETS_GetRootStore()
if type(pets) == "table" and type(pets.trainScan or pets.petTraining) == "table" then
MTH_CharSavedVariables.trainScan = pets.trainScan or pets.petTraining
MTH_CharSavedVariables.petTraining = MTH_CharSavedVariables.trainScan
if MTH_CharSavedVariables.petTraining.hasCompletedPetScan == nil then
local pt = MTH_CharSavedVariables.petTraining
if (tonumber(pt.lastScan) or 0) > 0 then
pt.hasCompletedPetScan = 1
elseif type(pt.spellMap) == "table" then
for _ in pairs(pt.spellMap) do
pt.hasCompletedPetScan = 1
break
end
end
end
end
end
end
function MTH:GetModuleSavedVariables(name)
name = MTH_NormalizeModuleName(name)
if not MTH_SavedVariables then
self:InitSavedVariables()
end
if not MTH_SavedVariables.modules then
MTH_SavedVariables.modules = {}
end
if not MTH_SavedVariables.modules[name] then
MTH_SavedVariables.modules[name] = {}
end
return MTH_SavedVariables.modules[name]
end
function MTH:GetModuleCharSavedVariables(name)
name = MTH_NormalizeModuleName(name)
if not MTH_CharSavedVariables then
self:InitSavedVariables()
end
if not MTH_CharSavedVariables.modules then
MTH_CharSavedVariables.modules = {}
end
if not MTH_CharSavedVariables.modules[name] then
MTH_CharSavedVariables.modules[name] = {}
end
return MTH_CharSavedVariables.modules[name]
end
function MTH:ApplyPersistedModuleStates(source)
if self:IsClassGateBlocked() then
return
end
self:InitSavedVariables()
for name, module in pairs(self.modules) do
local defaultEnabled = module._mth_defaultEnabled
if defaultEnabled == nil then
defaultEnabled = module.enabled and true or false
end
local persistedEnabled = MTH_GetPersistedModuleEnabled(name, defaultEnabled)
MTH_ModuleStateDebug("bootstrap " .. tostring(source or "") .. " " .. tostring(name) .. " persisted=" .. tostring(persistedEnabled) .. " runtime=" .. tostring(module.enabled and true or false))
if module.enabled ~= persistedEnabled then
local ok, err = self:SetModuleEnabled(name, persistedEnabled)
if not ok then
MTH_ModuleStateDebug("bootstrap apply failed " .. tostring(name) .. ": " .. tostring(err))
end
else
MTH_SetPersistedModuleEnabled(name, persistedEnabled)
end
end
end
function MTH:EnsureModuleStateBootstrap()
if self:IsClassGateBlocked() then
return nil
end
if self._moduleStateBootstrapFrame then
return self._moduleStateBootstrapFrame
end
local frame = CreateFrame("Frame", "MTH_ModuleStateBootstrapFrame")
if not frame then
return nil
end
frame:RegisterEvent("VARIABLES_LOADED")
frame:SetScript("OnEvent", function(self, eventName)
self = self or this
eventName = eventName or event
if eventName ~= "VARIABLES_LOADED" then
return
end
if MTH and MTH.ApplyPersistedModuleStates then
MTH:ApplyPersistedModuleStates("VARIABLES_LOADED")
end
if not self then return end
self:UnregisterAllEvents()
self:SetScript("OnEvent", nil)
end)
self._moduleStateBootstrapFrame = frame
return frame
end
function MTH:AnnounceLoadComplete()
if self:IsClassGateBlocked() then
return
end
if self._loadCompleteAnnounced then
return
end
local enabled = {}
for name, module in pairs(self.modules or {}) do
if module and module.enabled then
table.insert(enabled, tostring(name))
end
end
table.sort(enabled)
local moduleList = "none"
if table.getn(enabled) > 0 then
moduleList = table.concat(enabled, ", ")
end
self:Print("Check ignition: OK! Version " .. tostring(self.version or "unknown") .. " loaded.")
--self:Print("Modules enabled : " .. moduleList .. ".")
local petTrainingStore = nil
if type(MTH_PETS_GetRootStore) == "function" then
local pets = MTH_PETS_GetRootStore()
if type(pets) == "table" and type(pets.trainScan or pets.petTraining) == "table" then
petTrainingStore = pets.trainScan or pets.petTraining
if MTH_CharSavedVariables then
MTH_CharSavedVariables.trainScan = petTrainingStore
MTH_CharSavedVariables.petTraining = petTrainingStore
end
end
end
if not petTrainingStore then
petTrainingStore = MTH_CharSavedVariables and (MTH_CharSavedVariables.trainScan or MTH_CharSavedVariables.petTraining)
end
local hasCompletedPetScan = false
if petTrainingStore then
if petTrainingStore.hasCompletedPetScan then
hasCompletedPetScan = true
elseif (tonumber(petTrainingStore.lastScan) or 0) > 0 then
hasCompletedPetScan = true
elseif type(petTrainingStore.spellMap) == "table" then
for _ in pairs(petTrainingStore.spellMap) do
hasCompletedPetScan = true
break
end
elseif type(petTrainingStore.hunterKnownMap) == "table" then
for _ in pairs(petTrainingStore.hunterKnownMap) do
hasCompletedPetScan = true
break
end
end
end
if not hasCompletedPetScan then
local playerLevel = (type(UnitLevel) == "function") and (tonumber(UnitLevel("player")) or 0) or 0
if playerLevel >= 12 then
self:Print("You should open your Beast Training once to save your learned pet abilities.")
self._loadCompleteAnnounced = true
return
end
end
local welcomeMessages = {
"You are now shipping the latest cutting edge technology.",
"SHELLS ARE FOR PUSSIES.",
"Sometimes, you just need a little less gun.",
"Repeat 5 times : \"Un chasseur sachant chasser sans son chien\".",
"Welcome back, hunter — your pet filed three HR complaints while you were offline.",
"Fresh build loaded: now with 12% more pew and 38% less panic.",
"If it moves, track it. If it doesn't move, its probably a trap you already dropped.",
"Your arrows are sorted, your bars are neat, and your excuses are currently on cooldown.",
"Remember: missing shots are just warning shots for nearby wildlife.",
"Pet status: loyal, hungry, and judging your last pull.",
"Consider joining <Atlantis> if you like being called a Silly Goose.",
"Todays strategy: confidence, chaos, and pretending it was all planned.",
"You dont need luck — just line of sight and questionable self-control.",
"Chronometer is awake, traps are armed, and diplomacy has left the chat.",
"Go make bad decisions at range.",
"Hunter online. Morals offline. Ammo questionable.",
"If your pull feels safe, you probably pulled the wrong pack.",
"Pet AI says \"follow.\" Pet heart says \"chaos.\"",
"Todays build is stable, unlike your threat meter.",
"Get ready for a bloody hunting session.",
"You miss 100% of the shots you dont blame on lag.",
"Traps are temporary, embarrassment in party chat is forever.",
"Your quiver is full, your bags are not, your choices are yours.",
"Keep calm and feign absolutely everything.",
"If diplomacy fails, send the pet. If pet fails, send apology.",
"Welcome back. Try not to invent a new wipe mechanic.",
"Welcome back, hunter. Your pet says it carried the last dungeon and requests hazard pay.",
"MetaHunt loaded. Confidence initialized. Accuracy still loading...",
"Arrows: stocked. Traps: armed. Group chat patience: limited.",
"If the pull looks clean, you forgot at least one patrol.",
"Your pet is ready, your bags are not, and destiny is slightly overpulled.",
"Good news: your aim is back. Bad news: so is your aggro.",
"The cake is a lie.",
"Todays forecast: scattered crits with a high chance of panic kiting.",
"Welcome back. Remember: Feign Death is not a lifestyle, its a tool.",
"Fresh session started. Please keep all limbs inside line of sight.",
"Your pet has accepted this mission. Your healer has not.",
"Calm mind, steady hand, absolutely chaotic pull order.",
"Reload complete. Morale high. Ammo probably in the wrong bag.",
"You bring the damage, the pet brings the drama, the party brings questions.",
"Traps placed with confidence and minimal understanding of consequences.",
"You are now entering a no-fault zone. Any wipes are purely atmospheric.",
"Welcome back, ranger. Please ping responsibly and overpull professionally.",
"Target acquired. Plan prepared. Plan ignored.",
"Pet mood: loyal. Pet hunger: eternal. Pet pathing: experimental.",
"Another day, another masterpiece in ranged chaos.",
"MetaHunt online. Go forth and make very confident tactical mistakes.",
"Dry sarcasm mode activated. Expectations lowered to safe operating levels.",
"Great news: you logged in. Even better news: your pet still believes in you.",
"You are absolutely prepared for this pull, in the same way a torch is prepared for rain.",
"Welcome back. Your cooldowns are ready and your judgment is on a short break.",
"If this run goes smoothly, well all pretend it was intentional.",
"You bring precision, your pet brings enthusiasm, and reality brings pathing.",
"Todays objective: do your best, then blame line of sight professionally.",
"Friendly reminder: overconfidence is a buff with a very short duration.",
"Youre not behind schedule; youre operating on cinematic timing.",
"Your pet is doing its best, which is more than we can ask from stairs.",
"New day, new hunt, same wholesome chaos with extra snacks for the pet.",
"Youve got this — and if not, youve got Feign Death, which is basically plan B.",
"Be kind, pull smart, and let your arrows do the confident talking.",
"Your healer appreciates communication. Your tank appreciates fewer surprises.",
"May your crits be high and your repair bill spiritually manageable.",
"Extra savage forecast: 100% chance of \"I meant to do that.\"",
"Pull like a legend, recover like a professional, explain like a politician.",
"If your trap catches nothing, call it zoning control and move on.",
"Your threat meter is not a suggestion box.",
"Go forth, ranged menace — be accurate, be humble, and be slightly less feral than your pet.",
"Welcome back, outlaw — your pet already blamed you in guild chat.",
"MetaHunt online. Good decisions remain optional.",
"You logged in with confidence and approximately zero accountability.",
"Your pull plan is bold, reckless, and legally fascinating.",
"Arrows loaded, ego loaded, consequences loading...",
"Hunter mode engaged: precision first, apologies later.",
"Your healer saw you log in and started pre-drinking mana potions.",
"Cooldowns ready. Morals unavailable in your region.",
"Pet is loyal, hungry, and deeply concerned about your strategy.",
"Welcome back, menace. Try not to speedrun another wipe.",
"You bring damage, drama, and highly questionable target priority.",
"If this goes smoothly, nobody will believe you.",
"Your threat meter is a warning, not a lifestyle choice.",
"Traps armed. Safety standards disarmed.",
"You are one bad pull away from becoming a cautionary tale.",
"Great posture, sharp aim, catastrophic impulse control.",
"MetaHunt loaded. The chaos now has proper tooling.",
"Your pet follows orders. Pathing follows dark magic.",
"Todays objective: hit crits and dodge responsibility.",
"Go forth, badboy ranger — leave footprints, not explanations.",
}
local welcomeIndex = 1
local welcomeCount = table.getn(welcomeMessages)
if welcomeCount > 1 then
if type(random) == "function" then
welcomeIndex = random(1, welcomeCount)
elseif type(math) == "table" and type(math.random) == "function" then
welcomeIndex = math.random(1, welcomeCount)
end
end
self:Print(welcomeMessages[welcomeIndex])
self._loadCompleteAnnounced = true
end
function MTH:EnsureLoadCompleteAnnouncement()
if self:IsHardBlocked() then
return nil
end
if self:IsClassGateBlocked() then
return nil
end
if self._loadCompleteFrame then
return self._loadCompleteFrame
end
local frame = CreateFrame("Frame", "MTH_LoadCompleteAnnounceFrame")
if not frame then
return nil
end
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:SetScript("OnEvent", function(self, eventName)
self = self or this
eventName = eventName or event
if eventName ~= "PLAYER_ENTERING_WORLD" then
return
end
if MTH and MTH.AnnounceLoadComplete then
MTH:AnnounceLoadComplete()
end
if not self then return end
self:UnregisterAllEvents()
self:SetScript("OnEvent", nil)
end)
self._loadCompleteFrame = frame
return frame
end
MTH:EnsureModuleStateBootstrap()
MTH:EnsureLoadCompleteAnnouncement()
function MTH:ShutdownForNonHunter(source)
if self:IsHardBlocked() then
return true
end
self._hardBlocked = true
MTH_ClassGateTrace("ShutdownForNonHunter", "source=" .. tostring(source))
for name, module in pairs(self.modules or {}) do
if module and module.enabled then
self:_SetModuleEventSubscriptions(name, false)
end
if module and module.setEnabled and type(module.setEnabled) == "function" then
pcall(function()
module:setEnabled(false)
end)
end
if module and module.cleanup and type(module.cleanup) == "function" then
pcall(function()
module:cleanup()
end)
end
if module then
module.enabled = false
end
end
if self._eventRouter and self._eventRouter.frame then
self._eventRouter.frame:UnregisterAllEvents()
self._eventRouter.frame:SetScript("OnEvent", nil)
self._eventRouter.eventRefs = {}
end
if self._moduleStateBootstrapFrame then
self._moduleStateBootstrapFrame:UnregisterAllEvents()
self._moduleStateBootstrapFrame:SetScript("OnEvent", nil)
self._moduleStateBootstrapFrame = nil
end
if self._loadCompleteFrame then
self._loadCompleteFrame:UnregisterAllEvents()
self._loadCompleteFrame:SetScript("OnEvent", nil)
self._loadCompleteFrame = nil
end
if type(MTH_TR_ShutdownService) == "function" then
pcall(function()
MTH_TR_ShutdownService("class-gate")
end)
elseif type(MTH_PT_ShutdownService) == "function" then
pcall(function()
MTH_PT_ShutdownService("class-gate")
end)
end
if type(MTH_PS_ShutdownService) == "function" then
pcall(function()
MTH_PS_ShutdownService("class-gate")
end)
end
if type(MTH_ST_ShutdownService) == "function" then
pcall(function()
MTH_ST_ShutdownService("class-gate")
end)
end
return true
end