Files
ClassicAPI/AddOns/DebugTools/DebugTools.lua
T

1116 lines
42 KiB
Lua

------------------------------------------------------------------------------
-- DebugTools.lua (1.12 / Lua 5.0 backport of Blizzard_DebugTools 3.3.5)
--
-- Major changes from the 3.3.5 source:
-- * Lua 5.0 has no "..." varargs and no select(); script handlers take
-- explicit (this, event, arg1..arg9) parameters from XML inline scripts.
-- * RegisterAllEvents is used when available; the curated default list is
-- a fallback if the client doesn't expose it.
-- * No GameTooltip:SetFrameStack -- /framestack walks the engine's
-- frame list via EnumerateFrames(prev) and renders into our own
-- FrameStackTooltip instead. Catches anonymous frames the modern
-- engine method shows but a _G walk would miss.
-- * No UIPanelDialogTemplate, no parentKey, no clampedToScreen, no
-- OnSizeChanged in vanilla -- frames have fixed dimensions.
-- * No debuglocals
--
-- UX additions cherry-picked from modern Blizzard_EventTrace
-- (10.x+, anniversary _classic_era_):
-- * SavedVariables persist the user filter list, pause state, and
-- Args/Time display toggles across sessions (DebugToolsSavedVars).
-- * Toolbar row inside EventTraceFrame: Pause/Resume button + Args /
-- Time checkboxes + Search EditBox. Search filters the visible
-- rows by substring match against the event name.
-- * Args toggle appends a comma-separated arg dump after the event
-- name in the row (was tooltip-only before). Time toggle blanks
-- the time column without re-anchoring frames.
--
-- New: TableInspector window (see TableInspector.lua) — open via
-- `/tinspect <luaexpr>` with a navigable, breadcrumb-tracked view of
-- any table.
------------------------------------------------------------------------------
EVENT_TRACE_EVENT_HEIGHT = 16;
EVENT_TRACE_MAX_ENTRIES = 1000;
-- Toolbar above the event list (Pause + Args/Time toggles + Search) eats
-- one row's worth of vertical real estate; 26 visible rows instead of 28
-- keeps the frame the same height as before the enhancements landed.
EVENT_TRACE_NUM_BUTTONS = 26;
EVENT_TRACE_FRAME_HEIGHT = (EVENT_TRACE_NUM_BUTTONS * EVENT_TRACE_EVENT_HEIGHT) + 68;
DEBUGLOCALS_LEVEL = 4;
local _normalFontColor = { 1, .82, 0, 1 };
EVENT_TRACE_SYSTEM_TIMES = {};
EVENT_TRACE_SYSTEM_TIMES["System"] = true;
EVENT_TRACE_SYSTEM_TIMES["Elapsed"] = true;
EVENT_TRACE_EVENT_COLORS = {};
EVENT_TRACE_EVENT_COLORS["System"] = _normalFontColor;
EVENT_TRACE_EVENT_COLORS["Elapsed"] = { .6, .6, .6, 1 };
------------------------------------------------------------------------------
-- Saved variables — persist user filter list + display toggles across
-- sessions. WoW fills `DebugToolsSavedVars` from
-- `WTF\Account\<acct>\SavedVariables\DebugTools.lua` on ADDON_LOADED,
-- so any reference to it before that event fires is nil.
------------------------------------------------------------------------------
DebugToolsSavedVars = nil;
-- Defaults applied on a fresh install (no saved file yet) AND merged
-- field-by-field into older saved files so new fields don't require
-- a manual wipe to surface.
local _DEFAULT_FILTERS = {
"GLOBAL_MOUSE_DOWN", "GLOBAL_MOUSE_UP",
"MODIFIER_STATE_CHANGED",
"PLAYER_STARTED_LOOKING", "PLAYER_STOPPED_LOOKING",
"PLAYER_STARTED_TURNING", "PLAYER_STOPPED_TURNING",
"PLAYER_STARTED_MOVING", "PLAYER_STOPPED_MOVING",
"CHAT_MSG_ADDON",
};
local function _CloneArray(src)
local t = {};
for i = 1, table.getn(src) do t[i] = src[i]; end
return t;
end
local function _DefaultSavedVars()
return {
Filters = { User = _CloneArray(_DEFAULT_FILTERS) },
ShowArguments = false,
ShowTimestamp = true,
Paused = false,
};
end
local function _MergeDefaults(stored)
local defaults = _DefaultSavedVars();
if (type(stored) ~= "table") then return defaults; end
for k, v in pairs(defaults) do
if (stored[k] == nil) then stored[k] = v; end
end
if (type(stored.Filters) ~= "table") then
stored.Filters = { User = _CloneArray(_DEFAULT_FILTERS) };
end
if (type(stored.Filters.User) ~= "table") then
stored.Filters.User = _CloneArray(_DEFAULT_FILTERS);
end
return stored;
end
-- Apply the loaded saved-vars to the live frame state. Runs after both
-- the frame OnLoad (which sets _EventTraceFrame) and ADDON_LOADED
-- (which fills DebugToolsSavedVars). Idempotent — safe to re-run.
local function _ApplySavedVars()
if (not _EventTraceFrame or not DebugToolsSavedVars) then return; end
_EventTraceFrame.ignoredEvents = {};
for i = 1, table.getn(DebugToolsSavedVars.Filters.User) do
_EventTraceFrame.ignoredEvents[DebugToolsSavedVars.Filters.User[i]] = true;
end
_EventTraceFrame.paused = DebugToolsSavedVars.Paused and true or false;
_EventTraceFrame.showArguments = DebugToolsSavedVars.ShowArguments and true or false;
_EventTraceFrame.showTimestamp = DebugToolsSavedVars.ShowTimestamp and true or false;
-- Sync the toolbar widgets if they exist (XML may have loaded them
-- before or after this point depending on file order).
local pauseBtn = getglobal("EventTraceFramePauseButton");
if (pauseBtn) then
pauseBtn:SetText(_EventTraceFrame.paused and "Resume" or "Pause");
end
-- Vanilla 1.12 `CheckButton:SetChecked` takes 1 / nil, not true /
-- false — passing the bool directly silently leaves the box in
-- whatever state it had before, which is why the Time box looked
-- unchecked at first launch even though `showTimestamp` defaults
-- to true. (`true` was accepted starting in TBC.)
local argsChk = getglobal("EventTraceFrameArgsCheck");
if (argsChk) then
argsChk:SetChecked(_EventTraceFrame.showArguments and 1 or nil);
end
local timeChk = getglobal("EventTraceFrameTimeCheck");
if (timeChk) then
timeChk:SetChecked(_EventTraceFrame.showTimestamp and 1 or nil);
end
end
local _initFrame = CreateFrame("Frame");
_initFrame:RegisterEvent("ADDON_LOADED");
_initFrame:RegisterEvent("PLAYER_LOGOUT");
_initFrame:SetScript("OnEvent", function()
if (event == "ADDON_LOADED") then
if (arg1 ~= "DebugTools") then return; end
DebugToolsSavedVars = _MergeDefaults(DebugToolsSavedVars);
_ApplySavedVars();
_initFrame:UnregisterEvent("ADDON_LOADED");
elseif (event == "PLAYER_LOGOUT") then
-- Final sync on logout — captures any state changes made
-- after ADDON_LOADED (filter add/remove, toggle clicks).
if (DebugToolsSavedVars and _EventTraceFrame) then
DebugToolsSavedVars.Paused = _EventTraceFrame.paused;
DebugToolsSavedVars.ShowArguments = _EventTraceFrame.showArguments;
DebugToolsSavedVars.ShowTimestamp = _EventTraceFrame.showTimestamp;
-- Filters.User stays the source of truth — toolbar mutations
-- already write through to it directly.
end
end
end);
local _EventTraceFrame;
_framesSinceLast = 0;
_timeSinceLast = 0;
local _timer = CreateFrame("Frame");
_timer:SetScript("OnUpdate", function()
_framesSinceLast = _framesSinceLast + 1;
_timeSinceLast = _timeSinceLast + (arg1 or 0);
end);
------------------------------------------------------------------------------
-- EventTraceFrame
------------------------------------------------------------------------------
function EventTraceFrame_OnLoad(self)
self.buttons = {};
self.events = {};
self.times = {};
self.rawtimes = {};
self.args = { {}, {}, {}, {}, {}, {}, {}, {}, {} };
self.ignoredEvents = {};
self.lastIndex = 0;
self.visibleButtons = EVENT_TRACE_NUM_BUTTONS;
-- Display state defaults — overwritten by `_ApplySavedVars` once
-- ADDON_LOADED fires. Set here so the first Update before saved
-- vars come in has sane values.
self.paused = false;
self.showArguments = false;
self.showTimestamp = true;
self.searchText = nil;
self.filteredIndices = nil;
_EventTraceFrame = self;
-- If saved-vars beat the frame OnLoad (unusual file order), apply now.
_ApplySavedVars();
self:EnableMouse(true);
self:EnableMouseWheel(true);
self:SetScript("OnMouseWheel", function() EventTraceFrame_OnMouseWheel(this, arg1); end);
-- Pre-create the fixed list of buttons.
for i = 1, EVENT_TRACE_NUM_BUTTONS do
local button = CreateFrame("Button", "EventTraceFrameButton" .. i,
self, "EventTraceEventTemplate");
button:SetPoint("BOTTOMLEFT", 12, (EVENT_TRACE_EVENT_HEIGHT * (i - 1)) + 12);
button:SetPoint("RIGHT", -28, 0);
table.insert(self.buttons, button);
button:Show();
end
end
function EventTraceFrame_OnEvent(self, event, a1, a2, a3, a4, a5, a6, a7, a8, a9)
if (self.ignoredEvents[event]) then return; end
-- Pause swallows real engine events but lets the synthetic
-- "Begin Capture"/"End Capture"/"On Update" markers through so the
-- log records the boundary clearly.
if (self.paused and event ~= "Begin Capture" and event ~= "End Capture"
and event ~= "On Update") then
return;
end
if (not self.ignoreElapsed and _framesSinceLast ~= 0) then
self.ignoreElapsed = true;
EventTraceFrame_OnEvent(self, "On Update");
self.ignoreElapsed = nil;
end
local nextIndex = self.lastIndex + 1;
if (nextIndex > EVENT_TRACE_MAX_ENTRIES) then
local staleIndex = nextIndex - EVENT_TRACE_MAX_ENTRIES;
self.events[staleIndex] = nil;
self.times[staleIndex] = nil;
self.rawtimes[staleIndex] = nil;
for k, v in pairs(self.args) do
self.args[k][staleIndex] = nil;
end
end
if (string.find(event, "Begin Capture", 1, true) or
string.find(event, "End Capture", 1, true)) then
self.times[nextIndex] = "System";
if (self.eventsToCapture) then
self.events[nextIndex] = string.format("%s (%s events)", event,
tostring(self.eventsToCapture));
else
self.events[nextIndex] = event;
end
elseif (event == "On Update") then
self.times[nextIndex] = "Elapsed";
self.events[nextIndex] = string.format(
string.format("%.3f sec", _timeSinceLast) .. " - %d frame(s)",
_framesSinceLast);
_timeSinceLast = 0;
_framesSinceLast = 0;
else
self.events[nextIndex] = event;
local seconds = GetTime();
local minutes = math.floor(math.floor(seconds) / 60);
local hours = math.floor(minutes / 60);
seconds = seconds - 60 * minutes;
minutes = minutes - 60 * hours;
hours = math.mod(hours, 1000);
self.times[nextIndex] = string.format("%.2d:%.2d:%06.3f", hours, minutes, seconds);
local packed = { a1, a2, a3, a4, a5, a6, a7, a8, a9 };
local numArgs = 0;
for i = 9, 1, -1 do
if (packed[i] ~= nil) then
numArgs = i;
break;
end
end
for i = 1, numArgs do
if (not self.args[i]) then self.args[i] = {}; end
self.args[i][nextIndex] = packed[i];
end
if (self.eventsToCapture) then
self.eventsToCapture = self.eventsToCapture - 1;
end
end
self.rawtimes[nextIndex] = GetTime();
self.lastIndex = nextIndex;
-- Keep the filtered view in sync if a search is active. We append-
-- on-match here rather than re-walking the whole event list each
-- tick (would be O(n) per event for the most common case where
-- the search matches none / many).
if (self.filteredIndices and _EventTrace_MatchesSearch(self.events[nextIndex])) then
table.insert(self.filteredIndices, nextIndex);
-- Bound the filtered list to the same MAX_ENTRIES window the
-- raw store enforces. Drop the head when we overflow.
while (table.getn(self.filteredIndices) > 0 and
self.filteredIndices[1] < (nextIndex - EVENT_TRACE_MAX_ENTRIES + 1)) do
table.remove(self.filteredIndices, 1);
end
end
EventTraceFrame_Update();
if (self.eventsToCapture and self.eventsToCapture <= 0) then
self.eventsToCapture = nil;
EventTraceFrame_StopEventCapture();
end
end
function EventTraceFrame_OnUpdate(self, elapsed)
EventTraceFrame_Update();
end
------------------------------------------------------------------------------
-- EventTrace toolbar helpers — search filter, display flags, paused
-- toggle, filter list mutations. Keep these grouped together so the
-- toolbar XML scripts only need to reference one well-defined surface.
------------------------------------------------------------------------------
function _EventTrace_MatchesSearch(eventName)
if (not _EventTraceFrame) then return false; end
local needle = _EventTraceFrame.searchText;
if (not needle or needle == "") then return true; end
if (not eventName) then return false; end
return string.find(string.lower(eventName), needle, 1, true) ~= nil;
end
-- Walks the live event store and rebuilds the filtered-index list from
-- scratch. Called when search text changes; cheap (one pass over at
-- most EVENT_TRACE_MAX_ENTRIES entries) and matches the engine's own
-- "build then sort" pattern.
function _EventTrace_RebuildFilter()
if (not _EventTraceFrame) then return; end
local needle = _EventTraceFrame.searchText;
if (not needle or needle == "") then
_EventTraceFrame.filteredIndices = nil;
return;
end
needle = string.lower(needle);
local indices = {};
local first = math.max(1, (_EventTraceFrame.lastIndex or 0) - EVENT_TRACE_MAX_ENTRIES + 1);
for i = first, (_EventTraceFrame.lastIndex or 0) do
local ev = _EventTraceFrame.events[i];
if (ev and string.find(string.lower(ev), needle, 1, true)) then
table.insert(indices, i);
end
end
_EventTraceFrame.filteredIndices = indices;
end
-- Resolves a per-row event index. With a filter active, the scroll bar
-- range is over the filtered list — translate back to the underlying
-- event index here so the rest of the Update loop reads from `events[i]`
-- as usual.
function _EventTrace_ResolveIndex(scrollValue, rowOffset)
if (_EventTraceFrame.filteredIndices) then
return _EventTraceFrame.filteredIndices[scrollValue - rowOffset];
end
return scrollValue - rowOffset;
end
-- Returns (firstID, lastID) for the slider's current min/max. With a
-- filter active, the range is 1..#filtered. Without a filter it's the
-- raw event-store window.
function _EventTrace_ScrollRange()
if (_EventTraceFrame.filteredIndices) then
local n = table.getn(_EventTraceFrame.filteredIndices);
if (n == 0) then return 0, 0; end
return 1, n;
end
local first = math.max(1, _EventTraceFrame.lastIndex - EVENT_TRACE_MAX_ENTRIES + 1);
local last = _EventTraceFrame.lastIndex or 1;
return first, last;
end
-- Formats an event entry for in-line display. When `showArguments` is
-- on, appends a comma-separated rendering of the captured args after
-- the event name. System rows (Begin/End Capture, On Update markers)
-- never get args.
function _EventTrace_FormatRow(index)
local ev = _EventTraceFrame.events[index];
if (not ev) then return nil; end
if (not _EventTraceFrame.showArguments) then return ev; end
if (EVENT_TRACE_SYSTEM_TIMES[_EventTraceFrame.times[index]]) then return ev; end
local parts = {};
for i = 1, 9 do
local argList = _EventTraceFrame.args[i];
if (argList and argList[index] ~= nil) then
table.insert(parts, EventTrace_FormatArgValue(argList[index]));
end
end
if (table.getn(parts) == 0) then return ev; end
return ev .. " (" .. table.concat(parts, ", ") .. ")";
end
------------------------------------------------------------------------------
-- Toolbar widget handlers — wired up from EventTraceFrame.xml.
------------------------------------------------------------------------------
function EventTraceFrame_TogglePause()
if (not _EventTraceFrame) then return; end
_EventTraceFrame.paused = not _EventTraceFrame.paused;
if (DebugToolsSavedVars) then
DebugToolsSavedVars.Paused = _EventTraceFrame.paused;
end
local pauseBtn = getglobal("EventTraceFramePauseButton");
if (pauseBtn) then
pauseBtn:SetText(_EventTraceFrame.paused and "Resume" or "Pause");
end
end
function EventTraceFrame_ToggleArgs(checked)
if (not _EventTraceFrame) then return; end
_EventTraceFrame.showArguments = checked and true or false;
if (DebugToolsSavedVars) then
DebugToolsSavedVars.ShowArguments = _EventTraceFrame.showArguments;
end
EventTraceFrame_Update();
end
function EventTraceFrame_ToggleTimestamp(checked)
if (not _EventTraceFrame) then return; end
_EventTraceFrame.showTimestamp = checked and true or false;
if (DebugToolsSavedVars) then
DebugToolsSavedVars.ShowTimestamp = _EventTraceFrame.showTimestamp;
end
EventTraceFrame_Update();
end
function EventTraceFrame_OnSearchChanged(editBox)
if (not _EventTraceFrame) then return; end
local text = editBox:GetText() or "";
local clearBtn = getglobal("EventTraceFrameSearchClear");
if (clearBtn) then
if (text == "") then clearBtn:Hide(); else clearBtn:Show(); end
end
if (text == "") then text = nil; end
if (text == _EventTraceFrame.searchText) then return; end
_EventTraceFrame.searchText = text;
_EventTrace_RebuildFilter();
-- Reset scroll to the newest end of the filtered view so the user
-- sees fresh matches first.
local scroll = getglobal("EventTraceFrameScroll");
if (scroll) then
local mn, mx = _EventTrace_ScrollRange();
scroll:SetMinMaxValues(mn, math.max(mn, mx));
scroll:SetValue(math.max(mn, mx));
end
EventTraceFrame_Update();
end
-- Programmatic filter list mutation — also exposed via `/etrace ignore`.
-- Updates both the runtime ignoredEvents map and the saved-vars list
-- so the addition persists across sessions.
function EventTraceFrame_AddFilter(eventName)
if (not _EventTraceFrame or not eventName or eventName == "") then return; end
eventName = string.upper(eventName);
if (_EventTraceFrame.ignoredEvents[eventName]) then return; end
_EventTraceFrame.ignoredEvents[eventName] = true;
if (DebugToolsSavedVars and DebugToolsSavedVars.Filters
and DebugToolsSavedVars.Filters.User) then
table.insert(DebugToolsSavedVars.Filters.User, eventName);
end
end
function EventTraceFrame_RemoveFilter(eventName)
if (not _EventTraceFrame or not eventName) then return; end
eventName = string.upper(eventName);
_EventTraceFrame.ignoredEvents[eventName] = nil;
if (DebugToolsSavedVars and DebugToolsSavedVars.Filters
and DebugToolsSavedVars.Filters.User) then
local list = DebugToolsSavedVars.Filters.User;
for i = table.getn(list), 1, -1 do
if (list[i] == eventName) then table.remove(list, i); end
end
end
end
function EventTraceFrame_Update()
if (not _EventTraceFrame or not _EventTraceFrame.buttons) then
-- Slider OnValueChanged can fire during slider OnLoad, which happens
-- before EventTraceFrame_OnLoad in 1.12 XML load order.
return;
end
local scrollBar = getglobal("EventTraceFrameScroll");
if (not scrollBar) then return; end
local scrollBarValue = scrollBar:GetValue();
local minValue, maxValue = scrollBar:GetMinMaxValues();
local firstID, lastID = _EventTrace_ScrollRange();
if (firstID >= lastID) then
scrollBar:SetMinMaxValues(firstID - 1, lastID);
else
scrollBar:SetMinMaxValues(firstID, lastID);
end
if (scrollBarValue < firstID) then
scrollBar:SetValue(firstID);
scrollBarValue = firstID;
end
if (scrollBarValue < 1) then
scrollBarValue = 1;
elseif (not _EventTraceFrame.selectedEvent) then
if (scrollBarValue == maxValue) then
scrollBar:SetValue(lastID);
end
end
for i = 1, _EventTraceFrame.visibleButtons do
local button = _EventTraceFrame.buttons[i];
if (button) then
local index = _EventTrace_ResolveIndex(scrollBarValue, i - 1);
local event = index and _EventTraceFrame.events[index];
if (event) then
local timeString = _EventTraceFrame.times[index];
button.index = index;
local timeFS = getglobal(button:GetName() .. "Time");
local eventFS = getglobal(button:GetName() .. "Event");
-- ShowTimestamp toggle blanks the time column rather
-- than moving frames around — keeps the event column
-- left edge stable, which matters when toggling on/off
-- mid-session.
if (_EventTraceFrame.showTimestamp) then
timeFS:SetText(timeString);
else
timeFS:SetText("");
end
eventFS:SetText(_EventTrace_FormatRow(index));
local color = EVENT_TRACE_EVENT_COLORS[event]
or EVENT_TRACE_EVENT_COLORS[timeString];
if (color) then
timeFS:SetTextColor(unpack(color));
eventFS:SetTextColor(unpack(color));
else
timeFS:SetTextColor(1, 1, 1, 1);
eventFS:SetTextColor(1, 1, 1, 1);
end
button:Show();
if (_EventTraceFrame.selectedEvent) then
if (index == _EventTraceFrame.selectedEvent) then
EventTraceFrameEvent_DisplayTooltip(button);
button:GetHighlightTexture():SetVertexColor(.15, .25, 1, .35);
button:LockHighlight();
button.wasSelected = true;
elseif (button.wasSelected) then
button.wasSelected = nil;
button:GetHighlightTexture():SetVertexColor(.8, .8, 1, .15);
button:UnlockHighlight();
end
else
if (button.wasSelected) then
button.wasSelected = nil;
button:GetHighlightTexture():SetVertexColor(.8, .8, 1, .15);
button:UnlockHighlight();
end
if MouseIsOver(button) then
EventTraceFrameEvent_OnEnter(button);
end
end
else
button.index = index;
button:Hide();
end
end
end
end
function EventTraceFrame_StartEventCapture()
if (_EventTraceFrame.started) then return; end
_EventTraceFrame.started = true;
_framesSinceLast = 0;
_timeSinceLast = 0;
_EventTraceFrame:RegisterAllEvents();
EventTraceFrame_OnEvent(_EventTraceFrame, "Begin Capture");
end
function EventTraceFrame_StopEventCapture()
if (not _EventTraceFrame.started) then return; end
_EventTraceFrame.started = false;
_framesSinceLast = 0;
_timeSinceLast = 0;
_EventTraceFrame:UnregisterAllEvents();
EventTraceFrame_OnEvent(_EventTraceFrame, "End Capture");
end
local function _ChatPrint(msg)
DEFAULT_CHAT_FRAME:AddMessage("|cff88ff88EventTrace:|r " .. msg);
end
function EventTraceFrame_HandleSlashCmd(msg)
if (msg == nil) then msg = ""; end
msg = string.lower(msg);
-- /etrace add EVENT
local _, _, addEvent = string.find(msg, "^add%s+(%S+)");
if (addEvent) then
addEvent = string.upper(addEvent);
if (_EventTraceFrame.started) then
_EventTraceFrame:RegisterEvent(addEvent);
end
_ChatPrint("added " .. addEvent);
return;
end
-- /etrace remove EVENT
local _, _, rmEvent = string.find(msg, "^remove%s+(%S+)");
if (rmEvent) then
rmEvent = string.upper(rmEvent);
if (_EventTraceFrame.started) then
_EventTraceFrame:UnregisterEvent(rmEvent);
end
_ChatPrint("removed " .. rmEvent);
return;
end
-- /etrace ignore EVENT — persists across sessions via saved vars
local _, _, igEvent = string.find(msg, "^ignore%s+(%S+)");
if (igEvent) then
igEvent = string.upper(igEvent);
EventTraceFrame_AddFilter(igEvent);
_ChatPrint("ignoring " .. igEvent);
return;
end
-- /etrace unignore EVENT — opposite of ignore
local _, _, unEvent = string.find(msg, "^unignore%s+(%S+)");
if (unEvent) then
unEvent = string.upper(unEvent);
EventTraceFrame_RemoveFilter(unEvent);
_ChatPrint("unignored " .. unEvent);
return;
end
if (msg == "start") then
EventTraceFrame_StartEventCapture();
elseif (msg == "stop") then
EventTraceFrame_StopEventCapture();
elseif (msg == "pause") then
EventTraceFrame_TogglePause();
_ChatPrint(_EventTraceFrame.paused and "paused" or "resumed");
elseif (msg == "filters") then
if (DebugToolsSavedVars and DebugToolsSavedVars.Filters
and DebugToolsSavedVars.Filters.User) then
local list = DebugToolsSavedVars.Filters.User;
_ChatPrint(string.format("filters (%d):", table.getn(list)));
for i = 1, table.getn(list) do
_ChatPrint(" " .. list[i]);
end
else
_ChatPrint("no saved filters");
end
elseif (msg == "clear") then
_EventTraceFrame.events = {};
_EventTraceFrame.times = {};
_EventTraceFrame.rawtimes = {};
_EventTraceFrame.args = { {}, {}, {}, {}, {}, {}, {}, {}, {} };
_EventTraceFrame.lastIndex = 0;
_EventTraceFrame.filteredIndices = nil;
EventTraceFrame_Update();
elseif (tonumber(msg) and tonumber(msg) > 0) then
if (not _EventTraceFrame.started) then
_EventTraceFrame.eventsToCapture = tonumber(msg);
EventTraceFrame_StartEventCapture();
end
elseif (msg == "" or msg == "toggle") then
if (not _EventTraceFrame:IsShown()) then
_EventTraceFrame:Show();
if (_EventTraceFrame.started == nil) then
EventTraceFrame_StartEventCapture();
end
else
_EventTraceFrame:Hide();
end
else
_ChatPrint("usage:");
_ChatPrint(" /etrace -- toggle window (auto-start first time)");
_ChatPrint(" /etrace start -- begin event capture");
_ChatPrint(" /etrace stop -- end event capture");
_ChatPrint(" /etrace pause -- pause/resume in-place");
_ChatPrint(" /etrace <N> -- capture next N events");
_ChatPrint(" /etrace add <EV> -- register additional event");
_ChatPrint(" /etrace remove <EV> -- unregister event");
_ChatPrint(" /etrace ignore <EV> -- hide event (persists)");
_ChatPrint(" /etrace unignore <EV> -- remove from ignore list");
_ChatPrint(" /etrace filters -- list saved ignored events");
_ChatPrint(" /etrace clear -- clear log");
_ChatPrint(" (toolbar has Pause + Args/Time toggles + Search)");
end
end
function EventTraceFrame_OnMouseWheel(self, delta)
local scrollBar = getglobal("EventTraceFrameScroll");
local minVal, maxVal = scrollBar:GetMinMaxValues();
local currentValue = scrollBar:GetValue();
local newValue = currentValue - (delta * 3);
if (newValue < minVal) then newValue = minVal; end
if (newValue > maxVal) then newValue = maxVal; end
if (newValue ~= currentValue) then
scrollBar:SetValue(newValue);
end
end
local TIME_ENTRY_FORMAT = "Time:";
local DETAILS_ENTRY_FORMAT = "Details:";
local ARGUMENT_ENTRY_FORMAT = "arg %d:";
-- `||` is WoW's escape for a literal pipe in FontString display text.
-- Arg values carrying `|c...|r` color codes or `|Hitem:...|h` hyperlinks
-- (chat-event args, combat-log fields, anything from a Lua addon that
-- pushed a colored string) would otherwise get re-interpreted by the
-- renderer when concatenated into a row. A stray unterminated color
-- code leaks state into the rest of the row text.
function _EventTrace_EscapePipes(s)
return (string.gsub(s, "|", "||"));
end
-- Global (not local) because `_EventTrace_FormatRow` defined earlier in
-- this chunk calls it — a `local function` here wouldn't be in scope at
-- that earlier declaration site in Lua 5.0.
function EventTrace_FormatArgValue(val)
local t = type(val);
if (t == "string") then
return string.format('"%s"', _EventTrace_EscapePipes(val));
elseif (t == "number") then
return tostring(val);
elseif (t == "boolean") then
return string.format('|cffaaaaff%s|r', tostring(val));
elseif (t == "table" or t == "function" or t == "userdata") then
return string.format('|cffffaaaa%s|r',
_EventTrace_EscapePipes(tostring(val)));
end
return _EventTrace_EscapePipes(tostring(val));
end
function EventTraceFrameEvent_DisplayTooltip(eventButton)
local index = eventButton.index;
if (not index) then return; end
local tooltip = EventTraceTooltip;
tooltip:SetOwner(eventButton, "ANCHOR_NONE");
tooltip:SetPoint("TOPLEFT", eventButton, "TOPRIGHT", 24, 2);
local timeString = _EventTraceFrame.times[index];
if (EVENT_TRACE_SYSTEM_TIMES[timeString]) then
tooltip:AddLine(timeString, 1, 1, 1);
tooltip:AddDoubleLine(TIME_ENTRY_FORMAT,
tostring(_EventTraceFrame.rawtimes[index]),
1, .82, 0, 1, 1, 1);
tooltip:AddDoubleLine(DETAILS_ENTRY_FORMAT,
_EventTraceFrame.events[index],
1, .82, 0, 1, 1, 1);
else
tooltip:AddLine(_EventTraceFrame.events[index], 1, 1, 1);
tooltip:AddDoubleLine(TIME_ENTRY_FORMAT,
tostring(_EventTraceFrame.rawtimes[index]),
1, .82, 0, 1, 1, 1);
for k, v in ipairs(_EventTraceFrame.args) do
if (v[index] ~= nil) then
tooltip:AddDoubleLine(string.format(ARGUMENT_ENTRY_FORMAT, k),
EventTrace_FormatArgValue(v[index]),
1, .82, 0, 1, 1, 1);
end
end
end
tooltip:Show();
end
function EventTraceFrameEvent_OnEnter(self)
if (_EventTraceFrame.selectedEvent) then return; end
EventTraceFrameEvent_DisplayTooltip(self);
end
function EventTraceFrameEvent_OnLeave(self)
if (not _EventTraceFrame.selectedEvent) then
EventTraceTooltip:Hide();
end
end
function EventTraceFrameEvent_OnClick(self)
if (_EventTraceFrame.selectedEvent == self.index) then
_EventTraceFrame.selectedEvent = nil;
else
_EventTraceFrame.selectedEvent = self.index;
end
EventTraceFrame_Update();
end
------------------------------------------------------------------------------
-- ScriptErrorsFrame
------------------------------------------------------------------------------
local ERROR_FORMAT = "|cffffd200Message:|r|cffffffff %s|r\n"
.. "|cffffd200Time:|r|cffffffff %s|r\n"
.. "|cffffd200Count:|r|cffffffff %s|r\n"
.. "|cffffd200Stack:|r|cffffffff %s|r";
local INDEX_ORDER_FORMAT = "%d / %d";
local _ScriptErrorsFrame;
local _previousErrorHandler;
function ScriptErrorsFrame_OnLoad(self)
local titleFS = getglobal(self:GetName() .. "Title");
if (titleFS) then
titleFS:SetText("Lua Error");
end
self:RegisterForDrag("LeftButton");
self.seen = {};
self.order = {};
self.count = {};
self.messages = {};
self.times = {};
_ScriptErrorsFrame = self;
end
function ScriptErrorsFrame_OnShow(self)
ScriptErrorsFrame_Update();
end
function ScriptErrorsFrame_OnError(message, keepHidden)
if (message == nil) then return; end
local stack = debugstack(DEBUGLOCALS_LEVEL) or "";
local messageStack = message .. stack;
if (_ScriptErrorsFrame) then
local index = _ScriptErrorsFrame.seen[messageStack];
if (index) then
_ScriptErrorsFrame.count[index] = _ScriptErrorsFrame.count[index] + 1;
_ScriptErrorsFrame.messages[index] = message;
_ScriptErrorsFrame.times[index] = date();
else
table.insert(_ScriptErrorsFrame.order, stack);
index = table.getn(_ScriptErrorsFrame.order);
_ScriptErrorsFrame.count[index] = 1;
_ScriptErrorsFrame.messages[index] = message;
_ScriptErrorsFrame.times[index] = date();
_ScriptErrorsFrame.seen[messageStack] = index;
end
if (not _ScriptErrorsFrame:IsShown() and not keepHidden) then
_ScriptErrorsFrame.index = index;
_ScriptErrorsFrame:Show();
else
ScriptErrorsFrame_Update();
end
end
-- Chain through to whatever previous handler existed (e.g. BugGrabber)
if (_previousErrorHandler and _previousErrorHandler ~= ScriptErrorsFrame_OnError) then
pcall(_previousErrorHandler, message);
end
end
function ScriptErrorsFrame_Update()
local editBox = getglobal("ScriptErrorsFrameScrollFrameText");
local index = _ScriptErrorsFrame.index;
if (not index or not _ScriptErrorsFrame.order[index]) then
index = table.getn(_ScriptErrorsFrame.order);
_ScriptErrorsFrame.index = index;
end
if (index == 0) then
editBox:SetText("");
ScriptErrorsFrame_UpdateButtons();
return;
end
local text = string.format(ERROR_FORMAT,
_ScriptErrorsFrame.messages[index],
_ScriptErrorsFrame.times[index],
_ScriptErrorsFrame.count[index],
_ScriptErrorsFrame.order[index]);
editBox:SetText(text);
editBox:HighlightText(0);
editBox:SetCursorPosition(0);
local scrollFrame = getglobal("ScriptErrorsFrameScrollFrame");
if (scrollFrame and scrollFrame.SetVerticalScroll) then
scrollFrame:SetVerticalScroll(0);
end
ScriptErrorsFrame_UpdateButtons();
end
function ScriptErrorsFrame_UpdateButtons()
local index = _ScriptErrorsFrame.index;
local numErrors = table.getn(_ScriptErrorsFrame.order);
local nextBtn = getglobal("ScriptErrorsFrameNext");
local prevBtn = getglobal("ScriptErrorsFramePrevious");
local idxLbl = getglobal("ScriptErrorsFrameIndexLabel");
if (index == 0 or numErrors == 0) then
nextBtn:Disable();
prevBtn:Disable();
else
if (numErrors == 1) then
nextBtn:Disable();
prevBtn:Disable();
elseif (index == 1) then
nextBtn:Enable();
prevBtn:Disable();
elseif (index == numErrors) then
nextBtn:Disable();
prevBtn:Enable();
else
nextBtn:Enable();
prevBtn:Enable();
end
end
idxLbl:SetText(string.format(INDEX_ORDER_FORMAT, index, numErrors));
end
function ScriptErrorsFrameButton_OnClick(self)
local id = self:GetID();
if (id == 1) then
_ScriptErrorsFrame.index = _ScriptErrorsFrame.index + 1;
else
_ScriptErrorsFrame.index = _ScriptErrorsFrame.index - 1;
end
ScriptErrorsFrame_Update();
end
function ScriptErrorsFrame_TitleButton_OnDragStart()
_ScriptErrorsFrame.moving = true;
_ScriptErrorsFrame:StartMoving();
end
function ScriptErrorsFrame_TitleButton_OnDragStop()
_ScriptErrorsFrame.moving = nil;
_ScriptErrorsFrame:StopMovingOrSizing();
end
function ScriptErrorsFrame_HandleSlashCmd(msg)
if (msg == nil) then msg = ""; end
msg = string.lower(msg);
if (msg == "on" or msg == "install") then
_previousErrorHandler = geterrorhandler();
seterrorhandler(ScriptErrorsFrame_OnError);
_ChatPrint("Lua error capture installed.");
elseif (msg == "off" or msg == "uninstall") then
if (_previousErrorHandler) then
seterrorhandler(_previousErrorHandler);
end
_ChatPrint("Lua error capture uninstalled.");
elseif (msg == "test") then
ScriptErrorsFrame_OnError("test error from /luaerrors test");
elseif (msg == "clear") then
_ScriptErrorsFrame.seen = {};
_ScriptErrorsFrame.order = {};
_ScriptErrorsFrame.count = {};
_ScriptErrorsFrame.messages = {};
_ScriptErrorsFrame.times = {};
_ScriptErrorsFrame.index = 0;
ScriptErrorsFrame_Update();
else
if (_ScriptErrorsFrame:IsShown()) then
_ScriptErrorsFrame:Hide();
else
_ScriptErrorsFrame:Show();
end
end
end
------------------------------------------------------------------------------
-- FrameStackTooltip
------------------------------------------------------------------------------
function DebugTooltip_OnLoad(self)
self:SetFrameLevel(self:GetFrameLevel() + 2);
if (TOOLTIP_DEFAULT_COLOR and self.SetBackdropBorderColor) then
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r,
TOOLTIP_DEFAULT_COLOR.g,
TOOLTIP_DEFAULT_COLOR.b);
end
if (TOOLTIP_DEFAULT_BACKGROUND_COLOR and self.SetBackdropColor) then
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r,
TOOLTIP_DEFAULT_BACKGROUND_COLOR.g,
TOOLTIP_DEFAULT_BACKGROUND_COLOR.b);
end
end
local _STRATA_RANK = {
BACKGROUND = 1, LOW = 2, MEDIUM = 3, HIGH = 4,
DIALOG = 5, FULLSCREEN = 6, FULLSCREEN_DIALOG = 7, TOOLTIP = 8,
};
-- Walks every frame the engine knows about via EnumerateFrames (the
-- engine-internal frame iterator). Beats a getfenv(0) walk on two
-- counts: faster (the engine list is much shorter than _G), and
-- includes anonymous frames that never appear in globals (parented
-- templates, runtime `CreateFrame` with no `name` arg, etc.). The
-- iteration order itself is engine-implementation detail; we re-
-- sort by strata + level below either way.
local function _CollectFramesUnderMouse(showHidden)
local matches = {};
local frame = EnumerateFrames(nil);
while (frame) do
local visOK, visible = pcall(frame.IsVisible, frame);
local overOK, mouseOver = pcall(frame.IsMouseOver, frame);
if (overOK and mouseOver and (showHidden or (visOK and visible))) then
local nameOK, name = pcall(frame.GetName, frame);
if (not nameOK or not name) then name = "<anonymous>"; end
table.insert(matches, { name = name, frame = frame });
end
frame = EnumerateFrames(frame);
end
table.sort(matches, function(a, b)
local sa = _STRATA_RANK[a.frame:GetFrameStrata()] or 0;
local sb = _STRATA_RANK[b.frame:GetFrameStrata()] or 0;
if (sa ~= sb) then return sa > sb; end
local la = a.frame:GetFrameLevel() or 0;
local lb = b.frame:GetFrameLevel() or 0;
return la > lb;
end);
return matches;
end
local function _UpdateFrameStackTooltip(tooltip, showHidden)
tooltip:ClearLines();
tooltip:AddLine(DEBUG_FRAMESTACK or "Frame Stack", 1, 1, 1);
local matches = _CollectFramesUnderMouse(showHidden);
local count = table.getn(matches);
if (count == 0) then
tooltip:AddLine("(no frames under cursor)", .7, .7, .7);
else
local shown = 0;
for i = 1, count do
local m = matches[i];
local strata = m.frame:GetFrameStrata() or "?";
local level = m.frame:GetFrameLevel() or 0;
local label = string.format("%s |cff888888[%s:%d]|r",
m.name, strata, level);
tooltip:AddLine(label, 1, .82, 0);
shown = shown + 1;
if (shown >= 30) then
tooltip:AddLine(string.format("|cffff8888...%d more|r",
count - shown),
1, 1, 1);
break;
end
end
end
tooltip:Show();
end
function FrameStackTooltip_Toggle(showHidden)
local tooltip = FrameStackTooltip;
if (tooltip:IsVisible()) then
tooltip:Hide();
else
tooltip:SetOwner(UIParent, "ANCHOR_NONE");
tooltip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -32, 96);
tooltip.default = 1;
tooltip.showHidden = showHidden;
_UpdateFrameStackTooltip(tooltip, showHidden);
end
end
FRAMESTACK_UPDATE_TIME = .1;
local _frameStackAccum = 0;
function FrameStackTooltip_OnUpdate(self, elapsed)
_frameStackAccum = _frameStackAccum + elapsed;
if (_frameStackAccum >= FRAMESTACK_UPDATE_TIME) then
_frameStackAccum = 0;
_UpdateFrameStackTooltip(self, self.showHidden);
end
end
function FrameStackTooltip_OnShow(self)
local parent = self:GetParent() or UIParent;
local ps = parent:GetEffectiveScale();
local px, py = parent:GetCenter();
if (not px or not py) then return; end
px, py = px * ps, py * ps;
local x, y = GetCursorPosition();
self:ClearAllPoints();
if (x > px) then
if (y > py) then
self:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 20, 20);
else
self:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -20);
end
else
if (y > py) then
self:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -20, 20);
else
self:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -20, -20);
end
end
end
FrameStackTooltip_OnEnter = FrameStackTooltip_OnShow;
function FrameStackTooltip_HandleSlashCmd(msg)
if (msg == nil) then msg = ""; end
local showHidden = (string.find(msg, "hidden") ~= nil);
FrameStackTooltip_Toggle(showHidden);
end
------------------------------------------------------------------------------
-- Slash command registration
------------------------------------------------------------------------------
SLASH_ETRACE1 = "/etrace";
SlashCmdList["ETRACE"] = EventTraceFrame_HandleSlashCmd;
SLASH_FRAMESTACK1 = "/framestack";
SLASH_FRAMESTACK2 = "/fstack";
SlashCmdList["FRAMESTACK"] = FrameStackTooltip_HandleSlashCmd;
SLASH_LUAERRORS1 = "/luaerrors";
SLASH_LUAERRORS2 = "/scripterrors";
SlashCmdList["LUAERRORS"] = ScriptErrorsFrame_HandleSlashCmd;