--[[-------------------------------------------------------------------------- !Maniax335Compat — one authoritative 3.3.5a shim for the Maniax add-on pack. Several add-ons in this bundle are backports from modern clients, and each carries its own hand-rolled compat layer guarded by `if not SomeAPI then`. That guard means the *first* add-on to load defines the global and every later definition is skipped — so whichever implementation sorts earliest alphabetically wins, complete or not. This add-on's folder starts with "!" so it loads before all of them. Its implementations are complete, they claim the globals first, and every add-on's own partial shim then quietly skips its own guard. Add only APIs that are (a) genuinely absent on 3.3.5a and (b) actually called by something in the bundle. This is not a general polyfill. Type /maniaxcompat in game to see what this actually applied. Every shim records whether it took effect, because the failure mode that matters here is a shim that silently does nothing and looks exactly like not being installed at all. NOT handled here on purpose: CombatLogGetCurrentEventInfo. Both SpellActivationOverlay and TidyPlates_ThreatPlates define it, and SAO's version is the correct one — it normalises the raw 8-field 3.3.5a payload to the full 11-field modern layout, while ThreatPlates' inserts hideCaster but omits srcRaidFlags and dstRaidFlags, landing dstGUID and spellID two slots early. SAO sorts first, so the good one wins and the bug stays latent. Taking it over would mean owning a CLEU capture frame to replace a working implementation. If SpellActivationOverlay is ever removed from the bundle, ThreatPlates' broken version becomes live and this decision must be revisited. ----------------------------------------------------------------------------]] ManiaxCompat = { version = "1.6.0", applied = {}, skipped = {}, failed = {} } local function note(list, api, detail) table.insert(ManiaxCompat[list], detail and (api .. " (" .. detail .. ")") or api) end --------------------------------------------------------------------------- -- CreateColor (Legion+) -- -- SpellActivationOverlay ships a deliberately minimal shim returning a bare -- {r,g,b,a}, and sorts ahead of TidyPlates_ThreatPlates, whose RGB_WITH_HEX -- then calls :GenerateHexColor() on it and throws. Real CreateColor returns a -- ColorMixin, so provide the mixin surface the bundle actually uses. --------------------------------------------------------------------------- if not CreateColor then local function toHex(v) return string.format("%02x", math.floor((v or 1) * 255 + 0.5)) end function CreateColor(r, g, b, a) local color = { r = r or 0, g = g or 0, b = b or 0, a = a or 1 } -- Blizzard returns AARRGGBB, which is the order the |c escape wants. function color:GenerateHexColor() return toHex(self.a) .. toHex(self.r) .. toHex(self.g) .. toHex(self.b) end function color:GetRGB() return self.r, self.g, self.b end function color:GetRGBA() return self.r, self.g, self.b, self.a end function color:SetRGBA(nr, ng, nb, na) self.r, self.g, self.b, self.a = nr or 0, ng or 0, nb or 0, na or 1 end function color:WrapTextInColorCode(text) return "|c" .. self:GenerateHexColor() .. tostring(text) .. "|r" end return color end note("applied", "CreateColor") else note("skipped", "CreateColor", "already defined before us") end --------------------------------------------------------------------------- -- Texture:SetColorTexture (Legion+) -- -- Nothing else in the bundle provides this. It matters beyond the add-on that -- calls it, because LibStub hands the highest-versioned library to *everyone*: -- TidyPlates_ThreatPlates and Questie-335 ship AceGUI-3.0 MINOR 41, which -- calls SetColorTexture, and that beats the MINOR 33 copies fourteen other -- add-ons ship. A missing method here breaks config windows pack-wide. -- -- On 3.3.5a the solid-colour form is SetTexture(r, g, b, a). Patching the -- shared Texture metatable reaches every texture, including ones already -- created — AceGUI builds its widgets long after load. --------------------------------------------------------------------------- do local function shim(self, r, g, b, a) -- Alpha is optional on the modern API and defaults to opaque. self:SetTexture(r, g, b, a == nil and 1 or a) end local probe = UIParent:CreateTexture() if probe.SetColorTexture then note("skipped", "SetColorTexture", "client already has it") else local mt = getmetatable(probe) local methods = mt and mt.__index if type(methods) == "table" then methods.SetColorTexture = shim -- Prove it took: a metatable we cannot write to would leave this nil, -- and the symptom would be indistinguishable from a missing add-on. if UIParent:CreateTexture().SetColorTexture then note("applied", "SetColorTexture", "Texture metatable") else note("failed", "SetColorTexture", "metatable write did not stick") end else note("failed", "SetColorTexture", "__index is " .. type(methods) .. ", not a table") end end if probe.Hide then probe:Hide() end end --------------------------------------------------------------------------- -- EditBox:Enable() / EditBox:Disable() (Cataclysm 4.0) -- -- On 3.3.5a an EditBox has EnableMouse, EnableKeyboard and ClearFocus, but not -- the Enable/Disable pair Blizzard added later. Buttons and CheckButtons do -- have them, which is why this is easy to miss. -- -- Auctionator's inventory pane walks five edit boxes during -- Atr_Inventory_Init and calls Disable on each. That threw, aborting Atr_Init -- part-way, so gCurrentPane was never created — and Atr_OnUpdate then indexed -- it every frame, producing hundreds of errors a minute and an auction window -- that renders completely empty. One missing method, whole add-on dead. -- -- Worth noting the add-on already guards SetNumeric on the line above with -- "if editBox.SetNumeric then", so the author knew this class of gap existed -- and simply missed these two. --------------------------------------------------------------------------- -- REMOVED. Do not reintroduce. -- -- This added Enable/Disable to getmetatable(CreateFrame("EditBox")).__index. -- That table is NOT private to EditBox — HidingBar's own compat layer states it -- outright: "all frame types share one metatable; one pass is enough". So it -- was patching every frame in the game to satisfy two call sites in one add-on. -- -- It broke the keyboard twice. The first version also called -- EnableKeyboard(true), which makes a frame capture all keyboard input; -- removing that fixed the obvious symptom and left the real mistake in place. -- Escape and Jump then died again with a version that touches no keyboard API -- at all — verified on the live client: 1.5.1, zero EnableKeyboard calls, -- byte-identical to the served copy, keyboard dead the moment it was enabled. -- -- The mechanism was never fully explained, and that is exactly the point: a -- global metatable patch has a blast radius nobody can reason about from -- outside the client. Two reproductions beat one theory. -- -- The two calls that needed it are guarded at the call site in -- AuctionatorInventory.lua instead, matching the "if editBox.SetNumeric then" -- pattern its own author used one function below. If another add-on ever needs -- EditBox:Enable/Disable, guard it there too. --------------------------------------------------------------------------- -- C_Timer (Warlords of Draenor 6.0) -- -- The pack makes 176 calls to this — 113 After, 51 NewTicker, 12 NewTimer — -- and exactly one add-on defines the global: Details, in its compat.lua. -- Questie is the well-behaved exception; it keeps its own namespaced -- QuestieCompat.C_Timer and never touches the global. -- -- So the whole pack's timer support hangs off Details being enabled, and -- Details is optional and tickable. Untick it and every unguarded caller -- starts throwing "attempt to index global 'C_Timer' (a nil value)" — -- including Auctionator's buy-query and cancel-batch paths, which is a silent -- break in an add-on that looks fine until you use it. -- -- Defining it here removes that dependency. Details' own definition is -- "C_Timer = C_Timer or {}" followed by unconditional function assignments, so -- it will overwrite these with an equivalent implementation when it loads; -- that is harmless. What matters is that the global exists for everyone even -- when Details does not. --------------------------------------------------------------------------- if not C_Timer or not C_Timer.After then C_Timer = C_Timer or {} local timers = {} -- { remaining, callback, interval, iterations } local driver = CreateFrame("Frame") driver:SetScript("OnUpdate", function(_, elapsed) if #timers == 0 then return end -- Iterate a snapshot: a callback is free to schedule or cancel timers, and -- mutating the list underneath the loop would skip or double-fire entries. local due, keep = {}, {} for i = 1, #timers do local t = timers[i] if t.cancelled then -- dropped elseif t.remaining - elapsed <= 0 then due[#due + 1] = t if t.interval and (t.iterations == nil or t.iterations > 1) then t.remaining = t.interval if t.iterations then t.iterations = t.iterations - 1 end keep[#keep + 1] = t end else t.remaining = t.remaining - elapsed keep[#keep + 1] = t end end timers = keep for i = 1, #due do -- pcall so one bad callback cannot stall every other timer in the queue. local ok, err = pcall(due[i].callback) if not ok then geterrorhandler()(err) end end end) local function schedule(delay, callback, interval, iterations) if type(callback) ~= "function" then return end local t = { remaining = tonumber(delay) or 0, callback = callback, interval = interval, iterations = iterations, } timers[#timers + 1] = t -- Blizzard returns a handle with :Cancel() and :IsCancelled(). return { Cancel = function() t.cancelled = true end, IsCancelled = function() return t.cancelled == true end, } end function C_Timer.After(delay, callback) schedule(delay, callback) end function C_Timer.NewTimer(delay, callback) return schedule(delay, callback) end function C_Timer.NewTicker(interval, callback, iterations) return schedule(interval, callback, tonumber(interval) or 0, iterations) end note("applied", "C_Timer", "After/NewTimer/NewTicker") else note("skipped", "C_Timer", "already defined before us") end --------------------------------------------------------------------------- -- Blizzard's own error frame recurses into a C stack overflow -- -- Blizzard_DebugTools renders each captured error with -- -- format("Message: %s\nTime: %s\nCount: %s\nStack: %s\nLocals: %s", ...) -- -- and the locals argument is nil for errors raised across a pcall boundary — -- which is exactly how AceAddon surfaces a failure during EnableAddon. format() -- then throws "bad argument #6 to 'format' (string expected, got nil)", the -- error handler runs ScriptErrorsFrame_OnError, which calls _Update, which -- throws again, forever. Seen live at Count 420, with the error window blank: -- the frame dies before it can draw the error it was opened to show. -- -- That makes any error carrying nil locals into a client hang, and hides the -- original error while doing it. Two layers, because the hang matters more than -- the trigger: -- -- 1. debuglocals() never returns nil, which removes the trigger. -- 2. ScriptErrorsFrame_Update refuses to re-enter, which makes the runaway -- impossible whatever the trigger turns out to be. -- -- Deliberately not "/console scriptErrors 0". That stops the loop by stopping -- error reporting, and BugSack is optional in this pack — a player without it -- would then see no errors at all. Silently blinding people is worse than the -- bug. --------------------------------------------------------------------------- do if type(debuglocals) == "function" then local realDebugLocals = debuglocals function debuglocals(...) local ok, s = pcall(realDebugLocals, ...) if ok and type(s) == "string" then return s end return "" end note("applied", "debuglocals", "nil-safe") end -- Blizzard_DebugTools is load-on-demand, so the updater usually does not -- exist yet at startup. Guard it now if it does, otherwise wait for it. local guarded = false local function guardErrorFrame() if guarded or type(ScriptErrorsFrame_Update) ~= "function" then return guarded end local realUpdate = ScriptErrorsFrame_Update local inside = false ScriptErrorsFrame_Update = function(...) if inside then return -- re-entry: this is where the runaway stops end inside = true -- pcall as well as the flag: an error escaping here is what invokes the -- handler that calls us again. pcall(realUpdate, ...) inside = false end guarded = true return true end if guardErrorFrame() then note("applied", "ScriptErrorsFrame_Update", "re-entry guard") else local watcher = CreateFrame("Frame") watcher:RegisterEvent("ADDON_LOADED") watcher:SetScript("OnEvent", function(self, _, addon) if addon == "Blizzard_DebugTools" and guardErrorFrame() then self:UnregisterEvent("ADDON_LOADED") end end) note("applied", "ScriptErrorsFrame_Update", "guard armed for load-on-demand") end end --------------------------------------------------------------------------- -- Status readout. The point of this add-on is that it is invisible when it -- works, which makes "is it even loaded?" the first question every time -- something breaks. /maniaxcompat answers it in one line. --------------------------------------------------------------------------- local function say(msg) DEFAULT_CHAT_FRAME:AddMessage(msg) end -- /maniaxcompat tex — is the art failing to LOAD, or loading and rendering -- wrong? -- -- Several add-on panels draw checkboxes, window titles and resize grips as flat -- red while sliders, dropdowns and tabs from the same widget library draw -- correctly. Every one of those files was confirmed present in -- locale-enUS.MPQ, so the split is not a missing asset — but "present in the -- archive" and "loads on this machine" are different claims. -- -- A deliberately bogus path goes first, to establish what failure actually -- looks like on this client. If the red textures behave like the bogus one, -- they are not loading. If they behave like the controls, they load fine and -- the fault is downstream in rendering. local function textureReport() local probe = UIParent:CreateTexture() local function check(kind, path) probe:SetTexture(nil) local ok, err = pcall(probe.SetTexture, probe, path) local got = probe:GetTexture() local w, h = probe:GetWidth(), probe:GetHeight() say(string.format(" |cffaaaaaa%-9s|r %-46s set=%s get=%s size=%.0fx%.0f", kind, path, ok and "ok" or ("ERR:" .. tostring(err)), got and "path" or "|cffff4040nil|r", w or 0, h or 0)) end say("|cff00ff00Maniax Compat|r texture probe:") check("BOGUS", [[Interface\Buttons\ThisFileDoesNotExist]]) check("red", [[Interface\Buttons\UI-CheckBox-Up]]) check("red", [[Interface\DialogFrame\UI-DialogBox-Header]]) check("ok", [[Interface\Buttons\UI-SliderBar-Border]]) check("ok", [[Interface\Tooltips\UI-Tooltip-Border]]) say(" |cffaaaaaaIf the reds match BOGUS they are not loading; if they match") say(" the oks they load fine and the fault is in rendering.|r") if probe.Hide then probe:Hide() end end SLASH_MANIAXCOMPAT1 = "/maniaxcompat" SlashCmdList["MANIAXCOMPAT"] = function(arg) if arg and arg:lower():find("tex") then local ok, err = pcall(textureReport) if not ok then say("|cffff4040texture probe failed:|r " .. tostring(err)) end return end local function line(label, list, colour) if #list == 0 then return end say(" |cff" .. colour .. label .. "|r " .. table.concat(list, ", ")) end say("|cff00ff00Maniax 3.3.5a Compat|r v" .. ManiaxCompat.version .. " is loaded.") line("applied:", ManiaxCompat.applied, "00ff00") line("skipped:", ManiaxCompat.skipped, "ffff00") line("FAILED: ", ManiaxCompat.failed, "ff4040") say(" |cffaaaaaa/maniaxcompat tex|r — probe the red-texture problem") end