TOGTools-v0.13.0
What's new
Changelog
[v0.13.0] (2026-11-12) - Diagnostics shows the full error detail BugSack shows
New Features
Diagnostics keeps the call stack of a blocked / forbidden action and shows it in Explain. A player on WoW Forever got
[ADDON_ACTION_BLOCKED] AddOn 'Ultimate Guild Roster' tried to call the protected function 'UNKNOWN()'.and asked "Why doesn't Ultimate Tools show the full text?" (Discord, via Ultimate Guild Roster, inbox threadb01704dd). Without the stack, UGR could not tell which of its code paths made the call._recordtakes a newstackargument, kept from the first sighting the waymsgis and capped at 3000 characters (MAX_STACK). A repeat fills it in only when the first sighting had none, which happens with an older !!TOGT._ingestRawpassesrec.stackthrough from !!TOGT v0.3.0 (worked on as v0.2.5, which was never released). The fallback capture takes it itself through a newactionStack():debugstack(3)in apcall, nothing stored on a throw, a non-string or a Retail secret value (issecretvaluefeature-detected). That is BugGrabber's fallback level for these events (!BugGrabber/BugGrabber.lua:288), anddebugstackexists in the Forever tree (Blizzard_ScriptErrors.lua:56). The tab'srowRawTextadds the stack under the rebuilt one-line message, so clicking the row shows it in the Explain window's multi-line box, where it can be selected and copied.Interpretstill takes the culprit from "AddOn '...'" first, so the stack does not change who Explain blames. Lua errors still keep only the one-line summary, and nothing keeps locals. The help text and the module header now say that. The row-menu Copy is still the one-line row. Seen in game on WoW Forever (operator, 2026-11-04: "confirmed it's all there"): a forbiddenInvite()showed the stack down to the calling addon's own code,fastguildinvite/FGI_APICompat.lua:45inside itsGuildInviteat line 27, under the companion's handler frame. That needed !!TOGT's_forever_replication fix first: the first test showed only the one-liner because Forever still had the old!TOGT.lua. Not yet seen: anUNKNOWN()block. Four new specs.debugstackwas added to.luacheckrcand.luarc.json. Locations:Modules/Diagnostics/Diagnostics.lua,GUI/DiagnosticsTab.lua,Tests/diagnostics_spec.lua,.luacheckrc,.luarc.json.Diagnostics keeps the full detail BugSack shows, for every error: the stack AND the locals, Lua errors included. The operator, 2026-11-04, after the Discord bot told the player that BugSack "records a little extra detail that Ultimate Tools won't": "why don't we have the full detail?". The entry above covered only blocked actions and only their stack. Now
_recordalso keepslocals, under the same first-sighting and fill-on-repeat rule. Stack and locals are each capped at 4000 characters (MAX_DETAIL, wasMAX_STACKat 3000)._ingestRawpassesrec.localsthrough. The fallback capture's newerrorDetail()takes both from Lua errors as well as from blocked actions. The level is Blizzard's ownGetErrorDataarithmetic (Blizzard_ScriptErrors.lua:48-64, the same in the Classic Era and Forever trees):GetCallstackHeight() - (GetErrorCallstackHeight() - 1)inside an error handler, and 3 outside one.debuglocals(level, true)skips functions and userdata as Blizzard's call does, and|k/|Kbecome plain letters (Blizzard'sRemoveKStrings). A defect in the first cut:actionStackwrappeddebugstackinpcall, which is a frame of its own, so every stack started one level too high, on the capture handler itself. That is the!TOGT.lua:155line at the top of the operator's in-game stack.errorDetailcallsdebugstack/debuglocalsdirectly. Explain now shows the message, then the stack, thenLocals:and the locals, in BugSack's layout (rowRawText, split from the newrowMessage). The interpreter already stops reading callers atLocals:. The tab help no longer says Lua errors keep only the summary. Specs: locals kept and capped, locals on a Lua error, fill on repeat, the fallback handler's level arithmetic and|Kstripping, and missing, empty or secret answers; the global swaps are put back after each spec.debuglocals,GetCallstackHeightandGetErrorCallstackHeightwere added to.luacheckrcand.luarc.json. A repeat by another call path is kept too. The operator compared one forbiddenInvite()row with BugSack in game on Forever (2026-11-04). Ultimate Tools held the first path, the addon's own Scan-tab button. BugSack (5x) held the latest, Blizzard's invite popupOnAccept. Both were real, and keeping only the first sighting hid the second._recordnow keeps every further distinct call path ine.more, each with its own locals and a count, told apart by stack only, since locals change on every call. For the session nothing is capped (operator, 2026-11-04: "we can leave all that data in the live version, it's just whats saved and lives through a /reload that changes"). Only the saved copy is trimmed, byTrimForSaveonPLAYER_LOGOUT. The first two paths keep their locals (FULL_PATHS), every later path keeps only its stack and count, and later stacks share a 16,000-character budget (SAVED_MORE_STACKS). Paths past that budget are counted indroppedPaths, and Explain says how many were not kept rather than hiding them. The newest path leads, as in BugSack (operator, 2026-11-04: "when the stack changes, we keep the 'newest' one like bugsack does ... so when we fix errors, the updated info is what shows"). Every path, the entry's own included, now carries a count and a last-seen time (stackCount/stackLast, andcount/laston each ofmore). The newDiagnostics.Paths(e)returns them newest first. Explain shows the newest path first ("Last seen from (Nx, last time):"), so its caller attribution follows the latest occurrence, and then the older ones ("Earlier, from ...").TrimForSavereorders the entry newest first before trimming, so the newest two paths keep their locals across a reload. An open Explain window now follows its row. The operator, in game on Forever: "i had to close/open the window to get the udpates". The table did redraw on every capture throughonUpdate, but Explain was filled once, from a snapshot of the row's text.openExplainnow takes the entry and keeps it as_explain.entry._recordcalls the newDiagnostics.onEntryChanged(e)on every occurrence, and when the window follows that entry it redraws from the entry as it now is. Redraws are coalesced to one per 0.25 s (C_Timer.After, internal: it repaints only our own window), so a burst of one error redraws once. Typing or pasting into the box drops the link, so the player's own text is never overwritten. One new spec pins the hook; the window itself needs the client. The table's own redraw (onUpdate) is coalesced the same way now, so an error storm with the tab open rebuilds the table once per 0.25 s rather than once per error. Self-audit 050586d2: Explain no longer reads callers from older paths (ErrorInterpreterstackAddonsstops at "Earlier, from"; new spec), and!!TOGT'searlydata_specputs back the eleven globals it stubs once its specs have run. This also covers fastguildinvite's report (inbox 124c584c): a row with a current count and Last seen showed only a stack from code since rewritten, and BugSack showed the path that had actually fired. An interim cut capped paths at three and dropped the rest silently. That cap was never measured; the operator asked why every path would not be listed, and the cap was replaced. Seen in game in that same comparison: locals on a forbidden action (the invited player's name, where BugSack's locals are BugGrabber's own handler variables). That row's stack still began on the old!TOGT.lua:155capture line, because it was first recorded before the fixed !!TOGT reached Forever, and the first sighting is kept. Not yet seen in game: the stack of a Lua error, the stack starting at the caller, and the extra paths. Locations:Modules/Diagnostics/Diagnostics.lua,GUI/DiagnosticsTab.lua,Tests/diagnostics_spec.lua,.luacheckrc,.luarc.json.
Improvements
CHANGELOG.mdarchived under the release-body ceiling. At release prep it measured 122,635 bytes, over the 120,000 working ceiling (GitHub refuses a release body over 125,000). The two oldest sections, v0.8.8 and v0.8.7, moved whole to the top ofCHANGELOG_ARCHIVE.md, ahead of v0.8.6, with the editing tools; the trailing pointer now says "v0.8.8 and earlier".- Every list's numbers now sit under their header. The operator, in game, 2026-11-04: "the error numbers don't line up with the column header like the other columns do".
RowList.TranslateColumnmapped a tab'sjustify = "RIGHT"onto the library'salign, which right-aligns the cells, but LibAceGUIWidgets left-justifies every header label so the sort arrow follows the text (LibAceGUIWidgets-RowList.lua:824-826). Thirteen columns in nine tabs (Diagnostics #, Addon Load Memory and Load Time, Gathering and Guild Bank Qty, Gratz and Smack Count, Smack %, Item DB and Quest DB ID, Quest DB Level and Objectives, Mail Money) had their figures at the far edge of a left-aligned header.justifyis now dropped, so every cell is left-aligned under its header.Tests/rowlist_spec.luapins it. Location:GUI/RowList.lua. wow-version-replication.ps1now replicates into WoW Forever's new_forever_install folder (operator, 2026-11-04: "add the new forever folder to your ps1 replication script, so the updates get to that folder")._forever_has its ownWow.exenext to the older_classic_beta_, which stays in$WowVersionswhile it is installed. The dry run lists_forever_\Interface\AddOns\TOGToolsas a target, and after a restart the watcher copied all seven TOCs there. Dev-only; the script does not ship. Location:wow-version-replication.ps1.
Bug Fixes
addon.AceGUIFrameScriptsleft a script on a pooled widget whenever that event had no script before. It saved each prior script insaved[evt]and restored by iteratingsaved, but a nil prior is no entry in a Lua table, so every event that started empty was never put back and the next owner of the recycled frame inherited our handler, the leak the helper exists to prevent. It reaches every Dropdown and EditBox tooltipGUI/UI.luaattaches. The event names are now kept in their own list and every one is restored. Found by the helper's first spec, written for the Compat coverage gap. Also:addon.GUI.MakeColumnHeaderdeleted, with no caller since the lists moved to LibAceGUIWidgets' RowList;GuildFilterDropdown's entry / order / selection logic split intoaddon.GUI.GuildFilterChoices; Compat.lua is now at 247/247 lines covered (was 212/259). The dropdown's spec swapsAceGUI.Createfor a stand-in, because the harness cannot yet build AceGUI's real Dropdown (it readsUIDropDownMenuTemplate's named child textures); requested from WoWAPITesting (inbox 62210fcd). Locations:Compat.lua,Tests/guildkey_spec.lua.- A Gratz guild ding was blocked on WoW Forever with
ADDON_ACTION_BLOCKED"UNKNOWN()". Seen by the operator 2026-11-12: the stack ran LibGuildRosterOnLevelComm-> GratzonMemberLevelChanged->tryFire->sendToChannels->queuedOrSend->addon.Chat.Send(Compat.lua:421) ->C_ChatInfo.SendChatMessage. On Forever / Retail the client refuses addon chat whileC_ChatInfo.InChatMessagingLockdown()is true ("API security restrictions regarding chat messaging are in effect",ChatInfoDocumentation.lua; the wiki names encounters, keystone runs, PvP matches and dungeon / raid maps), and nothing checked it. Newaddon.Chat.MessagingLocked()(feature-detected, pcall'd).addon.Chat.Sendno longer attempts a send while locked and returns false, so Smack and every other caller stop provoking the block and do not count a fire. Gratz'squeuedOrSendholds the line in the combat queue while locked,flushCombatQueuekeeps holding while still locked (combat ending inside a dungeon), and the newADDON_RESTRICTION_STATE_CHANGEDhandler flushes when its state is 0 (Inactive,RestrictedActionsConstantsDocumentation.lua); the event also fires before a restriction activates, which is why only a lift releases. Which restriction was active when the operator saw it is not verified. Four new specs. Locations:Compat.lua,Modules/Gratz/Gratz.lua,Tests/gratz_spec.lua. - Gratz threw "attempt to index local 'name' (a secret string value)" on WoW Forever. Reported on Discord by a Forever player (inbox ff9f7597, 2026-11-10), stack
Gratz.lua:104inshortName<-isSelf<- theUNIT_LEVELhandler. On Retail-engine clients (Retail, Forever;issecretvalueis in the Forever tree)UnitName/UnitLeveland chat payloads can be secret while execution is tainted in combat, andname:matchon one throws. Gratz had no secret guard at all, unlike Smack, Whisper Log and Gathering Log. NewisSecret(feature-detectedissecretvalue, so Classic is unchanged):shortNamereturns nil andisSelffalse for a secret; theUNIT_LEVELhandler, its deferred re-read andrefreshPartyTrackerskip a secret name or level; the achievement handler returns early on a secretarg1/arg2, before thesenderName == playerGuildcompare andarg1:match. A secret is skipped, never guessed at. A skipped party ding is retried when combat ends (Vishiswaz on Discord, inbox 7cf1a18d: "if it can't read the names, shouldn't it try again later"):PLAYER_REGEN_ENABLEDnow also runsrefreshPartyTracker, and since the tracker still holds the pre-combat level, a higher readable level fires the ding then, once. That relies on the name being readable outside combat, which is assumed from the error text ("while execution tainted") and is not verified. An achievement line cannot be retried: the event's text is the only copy, so a secret one is lost. Five new specs inTests/gratz_spec.lua(a paired readable baseline, secret name, secret level, secret achievement text / sender). Not mutation-tested against the old code; not yet seen in game. Location:Modules/Gratz/Gratz.lua. - The two copies of the stack-and-locals capture are now pinned to one behaviour. Self-audit 050586d2 finding 5.
errorDetailexists twice, in!!TOGT/!TOGT.luaand in TOGTools' fallback capture, because the two addons share no code, and nothing asserted they agree. The newTests/errordetail_parity_spec.lualoads the real!TOGT.luafrom the sibling folder beside the real Diagnostics module and drives both through the same API stubs, by the error handler and by a blocked-action event, in nine cases: Blizzard's level arithmetic inside an error, level 3 outside one or without the height API,|k/|Kstripping, empty, secret and non-string answers, nodebuglocals, nodebugstack. Each answer is checked against the expected value and against the other addon's. Changing TOGTools' level by one turned eight of the nine red, then it was put back. Locations:Tests/errordetail_parity_spec.lua,Modules/Diagnostics/Diagnostics.lua(comment). - Diagnostics could grow memory without end from one error whose stack differs on every call. Self-audit 050586d2 finding 2.
addPathkept every distinct call path for the session, so an error raised from a runtime chunk with a generated name added a new path per occurrence. A new flood guard,MAX_LIVE_PATHS = 100extra paths per entry, drops the least-recently-seen extra path when exceeded (dropStalestPath) and counts it indroppedPaths, the counter the saved trim already uses; the entry's own path and every occurrence's count are kept. The operator's "leave all that data in the live version" still holds for any ordinary error: 100 distinct paths for one message is a flood, not a bug with several callers. Explain's dropped-path line now reads "were seen but not kept", since a drop can now happen without a reload. New spec. Locations:Modules/Diagnostics/Diagnostics.lua,GUI/DiagnosticsTab.lua,Tests/diagnostics_spec.lua. - An out-of-order Guild Bank bucket would never be put back in order on Classic Era. Peer Review F1 on the v0.12.0 self-audit (inbox thread 2067f94e). v0.12.0's
InjectEntrybinary-inserts each row and runs the retention prune only when the bucket's last row has expired, so both assume the bucket is newest-first, whileLogs:PruneEntrieswas deliberately made order-agnostic. On Era nothing ingests, so if a bucket ever got out of order no path there sorted it again: rows would go in at the wrong place and an expired row in the middle would never be pruned. Peer Review traced every released write path and found they all sort, so no stored bucket is known to be out of order; this is a safeguard.injectIndexnow checks the order in the pass it already makes over the bucket and sorts it newest-first when a row is out of place, at no extra per-row cost. New spec: a stored bucket that is out of order and has an expired middle row loses that row and is newest-first after oneInjectEntry. Location:Modules/GuildBankLog/GuildBankLog.lua,Tests/guildbanklog_spec.lua.
[v0.12.0] (2026-10-26) - Shows as Ultimate Tools in game; "script ran too long" when TOGBankClassic reports bank movements
New Features
- Shows as Ultimate Tools in game (operator, 2026-10-26: "lets do what we did with TOGPM and FGI to make it show as Ultimate Tools in game. we don't change the file structure or folders, we just change how it looks in game for now"). Same pattern as Ultimate Profession Master v1.7.0 and Ultimate Guild Invite v2.20.0. New
addon.DisplayName = "Ultimate Tools"andaddon.ShortName = "UT"inTOGTools.lua, used by: the## Titleof all seven TOCs; the main window title and its Help / Settings icon tooltips; the ESC > Options > AddOns entry and the options page title (AddToBlizOptions' display argument; the AceConfig app name staysTOGTools); the minimap button's LibDataBroker label and tooltip; the Explain window title, the Diagnostics row menu title and the scriptErrors tooltip; the Diagnostics error toast, Titan/LDB label and tooltip; the Item DB and Quest DB wipe dialogs; the Event Trace "no stream" line; the Smack key-binding warning; every "ESC > Options > AddOns > ..." hint in the Logs, Graphs, Gathering and Whisper help. Chat lines start with an orangeUT:instead ofTOGTools::Ace:Print/PrintfinTOGTools.luareplace AceConsole's, which prefix with the AceAddon name (TOGPM's override, copied). The[TOGTools]tags on the Login Digest and Mailbox login lines are now[UT], and the Debug, Retrack, Name Prefix, Profession Capture, LibAceGUIWidgets-outdated and UI warnings printUT./utis registered beside/togt, which keeps working;/ut helplists every command as/utwith one line saying/togtstill works, and the in-game hints that named a command (/ut mail,/ut glcheck,/ut gbcheck,/ut settings) now say/ut. Deliberately unchanged, because changing them would lose data or settings: the folder and file names,TOGTools_DB, the AceAddon / AceConfig / LibDataBroker / LibDBIcon / VersionCheck names (the saved minimap position hangs off the LibDBIcon name), frame global names, the!!TOGTdependency and the CurseForge project. The minimap button, the AddOns-list icon (new## IconTexturein all seven TOCs), the Diagnostics error toast and its Titan/LDB entry use the operator's newtextures/UT_MMB_Icon.tga(operator, 2026-10-26: "change that to be the image used in game") instead ofToGTools_PH_MMB. The first cut copied TOGPM's square crop, and the operator saw the coin stretched in game ("the icon seems streched?"). The newTests/icon_spec.luareads the TGA and measures the coin: that art had it squeezed to an oval (38 wide by 59 tall), so the operator re-exported it ("the image was bad, i replaced the image"). The oval-fitted crop then squashed the round coin ("the icon is now smooshed"). The final art has a round coin at x 13..50, y 12..50 of 64x64. UPM's and UGI's crop,{ 0.135, 0.825, 0.105, 0.795 }, was tried next at the operator's request ("do the slight adjustment UPM and UGI did"), but it is wider than this coin and left about 3 pixels of the art's white background showing ("it seems like it's smaller?"). So the minimap button'siconCoordsand the toast'sSetTexCoordare a square on the coin's own edge,{ 12.5/64, 51.5/64, 12/64, 51/64 }, then shifted 0.02 up and left, the same nudge UPM got in game (operator: "ours is offset like UPM was before the moved it slightly"), which draws the coin down and right in the slot. The spec pins that crop and the nudge, that the window is square and the coin round, that the coin stays inside the window (the nudge trims at most a pixel and a half) with at most two pixels of background showing, and that the coin is round in the file, since the AddOns-list icon (## IconTexture) cannot be cropped. It prints the measured bounds when they disagree, so a future image change shows the new numbers. The oldToGTools_PH_MMB.tgais no longer referenced by any code. README and the CurseForge page are not changed in this pass. New specTests/brand_spec.lualoads the realTOGTools.luaandSlashCommands.luaover the installed Ace3 and pins the prefix, both slash commands and the help text;Tests/toc_spec.luaandTests/mailbox_spec.luanow expect the new names and that the old title is gone. Locations: every.toc,TOGTools.lua,SlashCommands.lua,GUI/MainWindow.lua,GUI/Settings.lua,GUI/MinimapButton.lua,GUI/DiagnosticsTab.lua,GUI/ItemDBTab.lua,GUI/QuestDBTab.lua,GUI/EventTraceTab.lua,GUI/LogsTab.lua,GUI/LogGraphsSubTab.lua,GUI/ProfCaptureTab.lua,GUI/RowList.lua,GUI/SmackTab.lua,GUI/UI.lua,Modules/Diagnostics/Diagnostics.lua,Modules/LoginDigest/LoginDigest.lua,Modules/Mailbox/Mailbox.lua,Modules/NamePrefix/NamePrefix.lua,Modules/Retrack/Retrack.lua,Modules/GatheringLog/GatheringLog.lua,Modules/WhisperLog/WhisperLog.lua,Modules/GuildLog/GuildLog.lua,Modules/GuildBankLog/GuildBankLog.lua.
Bug Fixes
GuildBankLog.lua:198: script ran too longon Classic Era, raised inside TOGBankClassic'sLog:Deliver. User-reported 2026-10-26.GuildBankLog:InjectEntryrebuilt its dedupe set by hashing every stored row, full-sorted the bucket, ran the retention prune over it and redrew the Guild Bank sub-tab, once per row. TOGBankClassic pushes a whole batch in one timer callback, so a batch of k rows into a bucket of n rows cost k x n key hashes plus k sorts, all in one frame, and a long-lived Era log (where every row arrives this way) tripped the client's watchdog. Line 198 was just where the counter ran out, insidebaseKey. Now: the dedupe set and per-tuple highest numeric ordinal are cached per bucket (injectIndex, weak-keyed, valid only for the sameentriestable at the same length, and dropped explicitly by ingest, the three one-shot repairs, a prune that removed rows, andClearAll); the row goes in at its place in the newest-first order by binary search instead of a re-sort; the prune pass runs only when the bucket's oldest row is past retention (newLogs:RetentionCutoff(), whichPruneEntriesnow uses too, so the two cannot disagree); and the redraw is coalesced to one per batch (C_Timer.After(0), synchronous with noC_Timer). Stored rows, ids and dedupe results are unchanged. 10 new specs, including a 300-row batch into a 3000-row bucket under a fixed VM-instruction budget. Not yet verified in game. Locations:Modules/GuildBankLog/GuildBankLog.lua,Modules/Logs/Logs.lua,Tests/guildbanklog_spec.lua,Tests/logs_spec.lua.
[v0.11.0] (2026-10-16) - Tracking comes back after you die
New Features
Retrack: after you die and come back, your next mouse click turns your last tracking back on. Feature request from Discord (Rakish, 2026-10-11: "After dying and resurrecting, automatically activate the last tracking spell that was active"). Tracking is remembered per character (
db.char.retrack={ spellID, name, owed }) on everyMINIMAP_UPDATE_TRACKING. Whether a death owes a restore is decided atPLAYER_DEAD, because the client's order of "tracking off" against the unit reading dead is not known: an off report while alive only notes when it went off, and if it had been off for 2 seconds or more when the death arrives, you turned it off yourself, so it is forgotten and stays off. OtherwisePLAYER_DEADsetsowed, cleared only when the tracking is seen on again or the restore gives up, and only an owed restore is ever armed.PLAYER_ENTERING_WORLDre-arms an owed restore, so a/reloadbetween coming back to life and the click does not lose it. If the remembered tracking is not in the list yet at login, the restore waits rather than giving up, and the nextMINIMAP_UPDATE_TRACKINGarms it (never betweenPLAYER_DEADand coming back to life, when the unit may not read dead yet). Re-entering the arm while already armed (zoning) keeps the tries already spent. Classic Era reads and restores tracking differently, and the first draft got it wrong. The first in-game test on Era did nothing (operator, 2026-10-14: "had herbing on, died to test it, went to my corpse, got back into the game, and nothing"), and nothing was ever saved. The cause:Blizzard_Minimap_Classic.tocloads onlyMinimapTracking_Simpleon vanilla ([AllowLoadGameType vanilla]; the dropdown is[ExcludeLoadGameType vanilla]). It reads the active tracking withGetTrackingTexture()and names it withGameTooltip:SetTrackingSpell(). I only read theC_Minimaplist, which never reported Find Herbs on Era. I first blamed an "always-on non-spell entry"; that was invented, there is no such tracking on Era, and it is withdrawn. Now, when the list shows nothing active andGetTrackingTexture()returns an icon, the name is read from a hiddenGameTooltipTemplatetooltip (TOGTools_RetrackTip). It is read from the game rather than mapped from the icon, because Track Beasts and Track Humanoids share one. The memory getscast = true. Turning it back on there means casting the spell, which an addon can only do through a secure button. So that path's click-through sheet is aSecureActionButtonTemplatebutton (TOGTools_RetrackCaster,RegisterForClicks("AnyUp"):SecureActionButton_OnClickacts on the release for a secure mouse press). Its restingtypeattribute is nil, so it casts nothing.PreClicksetstype = "spell"/spell = <name>only for a click that passes every gate below, andPostClickclears it. Both skip in combat lockdown, so a sheet that cannot be hidden in combat still casts nothing.PLAYER_REGEN_DISABLEDtakes it down andPLAYER_REGEN_ENABLEDputs it back, or hides one a disarm left up in combat. The give-up message on this path says to cast it from the spellbook, since Era has no tracking menu. The operator re-tested this path in game on Classic Era on 2026-10-16 ("seems to work"). The list path keeps onlytype == "spell"entries as the remembered tracking, the split Blizzard's ownMinimapTracking_Updatemakes (Blizzard_Minimap/Classic/MinimapTracking_Dropdown.lua). Every decision is traced throughaddon:Debug(Settings' debug switch) so the next test shows where a restore stops. A click within 1.5 s of the lastSetTrackingis ignored, so a burst of clicks spends one try on a cast still in flight instead of reaching the give-up message while the tracking is coming on. (Earlier drafts forgot the memory the moment tracking went off while alive, with a 5-second grace afterPLAYER_DEAD; the self-audit and Peer Review caught that a corpse run outlasts the grace, and an off report racing ahead of the death beats it, so either order wiped the memory and nothing was restored.)GetTrackingInforeturns the sameMinimapScriptTrackingInfotable (name,active,spellID) in the Era, Anniversary, Classic, Forever and live API docs. OnPLAYER_UNGHOST/PLAYER_ALIVE(alive, not a ghost), the module arms a full-screen, click-through overlay, which is FastGuildInvite's Wingman pattern (operator, 2026-10-13: "should we use an overlay like we do for FGI, so any mouse click turns them on?"). The overlay'sOnMouseDowncallsC_Minimap.SetTracking(index, true)inside the player's own click, the same call Blizzard's tracking menu makes from a click (Blizzard_Minimap/Classic/MinimapTracking_Dropdown.lua). The click still passes through to whatever was under the cursor. Other Classic tracking addons only manage this "best effort" from an event, so this module never calls it from an event or a timer. The file has no timers at all. Each step is guarded:- The overlay is never shown without verified click pass-through (
SetPropagateMouseClicks, protected in combat, checked withCanPropagateMouseClicks). On a client without pass-through it prints the tracking-menu hint instead. - In combat it waits for
PLAYER_REGEN_ENABLED. - A click within 0.3s of arming is ignored, so the click that accepted a resurrection does not count.
- The overlay stands down when
MINIMAP_UPDATE_TRACKINGreports the tracking on. - After three clicks that do not take, it prints "pick it from the minimap tracking menu" and stops.
- The tracking is matched by spell id first, then by name, because the list's order changes as spells are learned.
- Feature-detected on
C_Minimap.GetNumTrackingTypes/GetTrackingInfo/SetTracking, so a client without them gets an inert module.
No tab (operator, 2026-10-13: "i don't think this needs it's own tab ... lets just add it to general settings"). There is one toggle, "Turn tracking back on after you die", in Settings > General, on by default ("have it turned on by default"), stored in
db.global.retrack.enabled. Because it has no tab it is NOT inaddon.modules:IsModuleEnabledfails open for a key it does not know.Ace:OnEnablestarts it directly, and turning the toggle off stands down a pending restore.Tests/retrack_spec.luacovers both paths;Modules/Retrack/Retrack.luais at 100% line coverage. Tested in game on Classic Era only; not verified in game: whetherSetTrackingtakes from the overlay click on the list flavours. Locations:Modules/Retrack/Retrack.lua,TOGTools.lua,GUI/Settings.lua, every.toc,.luarc.json,.luacheckrc,Tests/retrack_spec.lua.- The overlay is never shown without verified click pass-through (
[v0.10.2] (2026-10-07) - CurseForge installs every dependency; Profession Capture fixes on Forever
Bug Fixes
- The CurseForge upload declared LibDBIcon-1.0 as EMBEDDED, so the CurseForge app would not install it, and never declared LibAceGUIWidgets or !!TOGT at all. All three are on every TOC's
## Dependenciesline, so a player who got TOGTools through the app without already having them got an addon that does not load. Found by the harness's.pkgmetachecker (Tests/wowapi/pkgmeta.lua, run for the first time on this repo). Root causes: (1) the Forever-only embedded copylibs/LibDBIcon-1.0.luakept LibDBIcon's first line,--@curseforge-project-slug: libdbicon-1-0@; the packager greps every shipped.luafor that marker and it overrides.pkgmeta'srequired-dependenciesentry, so the v0.9.1 switch to a required dependency never took effect on CurseForge. The marker line is deleted; no code changed. (2)required-dependencieslackedlibaceguiwidgets(the slug every other fleet addon uses) andtogt(!!TOGT, project 1533953, slug read off its CurseForge page). Both added. The checker now reports no FAIL; it is declared as the writ suitepkgmeta(lua Tests/wowapi/pkgmeta.lua --slug !!TOGT=togt). Not verified on CurseForge until the next upload. Locations:libs/LibDBIcon-1.0.lua,.pkgmeta. - Profession Capture on WoW Forever filed every profession's recipes under each profession, and never recorded an unlearned recipe. Reported by ProfessionDB (inbox
c934e6aa) from the operator's 2026-10-07 Forever capture: Mining, Fishing, Blacksmithing, First Aid and Cooking each held the same mixed list, alllearned = true. Two causes, both read inwow-ui-source-forever. (1)C_TradeSkillUI.GetFilteredRecipeIDs()answers for every profession the character has; Blizzard's own list keeps a recipe only whereC_TradeSkillUI.IsRecipeInSkillLine(recipeID, professionID)is true (Blizzard_ProfessionsTemplates/Blizzard_Professions.lua:847-849). The read now applies the same test, against the base or the child skill line (newProfCapture.SkillLineTest). The merge that keeps an earlier read's rows applies it too, so the mixed rows already saved are dropped on the next read. (2) Forever's Professions UI forcesSetShowUnlearned(false)after every list build and offers no menu entry for it (Camelot/Blizzard_Professions.lua:16-25), so the window never lists an unlearned recipe, andGetRecipeSourceText(called only for unlearned rows) never ran. The read now also takes LibProfessionDB's recipes for the open profession and reads each one while the window is open. It asks byparentProfessionIDwhen the client sets one, elseprofessionID, because LibProfessionDB files every recipe under the base id and answers nil for any other. Each known record now carrieslibProfessionIDandlibRecipes(the count LibProfessionDB gave, 0 when it found nothing, unset when the library is absent). The filters are left alone, so the player's list does not change. Every unlearned row the read asks about now carriessourceChecked = trueandsourceTextType(the raw type returned), so a missingsourcereads as "the client answered nothing", never as "not asked"; an empty or secret string is not stored as a source. The operator's v0.10.2 Forever run (2026-10-07) read 662 unlearned rows across five professions andGetRecipeSourceTextgave none of them text (ProfessionDB's report). 9 new specs inTests/profcapture_spec.lua. Location:Modules/ProfCapture/ProfCapture.lua(retailKnown,CaptureKnown). - "Walk recipe sources" told the player Forever's gap was covered when it was not. On a walk that found no source it printed that it had "Saved the game's recipe link ... so ProfessionDB can look sources up offline". ProfessionDB cannot: the link is
skillLineAbilityID, and Forever's SkillLineAbility table has no vendor, drop or quest column (ProfessionDB, inboxc934e6aa). The message now says the walk gets no sources there and to open each profession window instead; the button's tooltip and two engine comments say the same. Locations:GUI/ProfCaptureTab.lua,Modules/ProfCapture/ProfCapture.lua.
Process
.pkgmetahas no comments any more (operator's standing rule, 2026-10-03: comments in.pkgmetacan break packaging). Its notes moved toCLAUDE.mdunder ".pkgmeta". The commented-outcurseforge-project-idline went with them; the id (1533830) is recorded there.Test harness pin moved
3d492c7->b20754a, with its adoption steps.Tests/gatheringlog_spec.luano longer installs its ownGetSubZoneText; the harness now does, returningwow.subZone(harness inbox280f62ed).Tests/smack_spec.luano longer stubsSetOverrideBindingClick/ClearOverrideBindings: the bindings and clear count it asserts now come from the harness's recordedwow.overrideBindingsandwow.clearedBindings(harness61c09b0, inbox366486c9).Tests/gratz_spec.lua's GreenWall stand-in check went red on the move, correctly: the harness (4fd0cbf, measured on Classic Era by GuildRoster) now reports an automated CHANNEL send on Classic as BLOCKED (wow.blocked,ADDON_ACTION_BLOCKED) instead of a silent drop, so the example assertswow.blockedand is renamed "...is blocked on Classic". No addon code changed by the move.
[v0.10.1] (2026-10-04) - WoW Forever recognised again
Bug Fixes
- WoW Forever was treated as Classic Era, so opening a profession window threw
ProfCapture.lua:412: attempt to call a nil value(GetTradeSkillLine). Reported by the operator from a Forever client, opening TOGProfessionMaster's Crafting tab. Root cause:addon.DeriveGameVersiononly recognised Forever whenWOW_PROJECT_ID == WOW_PROJECT_MAINLINE, but Forever reports project id 18 (FastGuildInvite observed "Project ID: 18, Build: 16001" in-client on build 1.60.1.70124,FGI_Compatibility.lua:22-38; the forever tree'sProjectConstants.luanames only 1 and 2). So Forever at Interface 16001 was derived as Vanilla,isRetailwas false, and every Forever/Retail branch in the addon was dead there -- ProfCapture took the ClassicGetTradeSkillLinepath, which Forever does not have. Other consumers that were wrong on Forever for the same reason, among them: Quest DB refused to run at all (QuestDB.lua:71gates onisForever, "only runs on the WoW Forever client");ProfCapture:ReadTrainerreadGetTrainerServiceInfoin Classic return order (ProfCapture.lua:182-198); I'd Hit That skipped Forever's ranged-hit modifier (IdHitThat.lua:275); Smack offered its six combat-log triggers, which it hides onisRetail120Plus(Smack.lua:142-147); and/togt vctried VersionCheck instead of saying it is not available. The load-time call now counts project id 18 as a retail-codebase client too. New spec: "is derived from Forever's own project id, 18" (Tests/version_spec.lua). Not verified in game. Location:Version.lua.
Process
CHANGELOG.mdarchived under the release-body ceiling. With the entry above it measured 121,457 characters, over the 120,000 working ceiling (GitHub refuses a release body over 125,000). The oldest section, v0.8.6, moved whole to the top ofCHANGELOG_ARCHIVE.md, ahead of v0.8.5, with the editing tools. The trailing pointer now says "v0.8.6 and earlier".
[v0.10.0] (2026-09-30) - Event Trace developer tab
New Features
- Event Trace: a live stream of every event the WoW client fires. New developer tab (shown with Developer Tools on) over the stream !!TOGT v0.2.5 captures from the first line of addon code (
!!TOGT/EventStream.lua). Lines arrive as they happen, like a chat window: sequence number, local time with milliseconds, seconds from login (negative before it), event name, arguments. The view is aScrollingMessageFrame(the widget chat windows use; AceGUI has none) keeping the newest 5,000 lines on screen, with the mouse wheel scrolling 3 lines, or 20 with Shift. Filter narrows the view and the copy to lines containing the text. Copy opens every captured line, not only the on-screen ones, in a read-only box for Ctrl+A / Ctrl+C: an AceGUIMultiLineEditBoxwhoseOnTextChanged(which AceGUI fires only for the user's own typing) puts the text back, so nothing can be deleted by accident. Counts lists every event fired since load and how often, excluded ones included. Pause, a confirmed Clear, a Feed on tick box, and an editable exclude list round it out. Feed on is OFF by default and saved, so it stays as left through reloads (operator: "we don't want a memory leak, this should only work when i want it to"); unticking it stops the stream at once and frees every line. The Copy box shows the whole stream 1,000 lines to a page, with Previous / Next, because one edit box cannot hold it. That was measured in game on 2026-09-30. The whole stream in one box showed BLANK at 9,511 lines. It was blank again at 5,099 lines after the box's byte cap was lifted (SetMaxBytes(0), as the AceGUI report in WoWInterface thread 55511 did) and !!TOGT began escaping control bytes. 1,000-line pages showed and copied. Later the same day a single box showed at 1,691 lines and was blank at 1,992. AddOn Studio'sEditBox:SetTextpage documents one cause: "SetText fails when the text contains characters that aren't renderable (e.g. control chars, invalid UTF8). In that case the EditBox is cleared". So !!TOGT v0.2.5 now also escapes every byte that is not valid UTF-8, and never cuts an argument mid-character. Whether that alone lets one box hold a long stream is not tested in game, so Copy stays paged. Needs !!TOGT v0.2.5; with an older !!TOGT the tab says there is no event stream. Memory only: nothing is saved, so a/reloadstarts a fresh stream. The operator asked for it on 2026-09-30 ("an in game log capture ... capture them into a 'memory' log and display them in TOGTools to help analysis"; "it's just a livestream we capture"; "this is to capture the wow api, so we can see what's going on in it"). It replaces a GUILD-chat login probe drafted earlier the same day for FastGuildInvite's question (inbox thread15b4d7a3), which never shipped. - Settings in
db.global.eventTrace(enabled,capture,exclude), which !!TOGT reads fromTOGTools_DBatPLAYER_LOGIN.EventTrace:SetExcludeapplies a new list to the live stream at once, and empty text restores !!TOGT's default.SetCapture(true)restarts a stopped stream andSetCapture(false)calls !!TOGT'sStop(). The module is inmoduleregistry_spec'sGATE_EXEMPT, with the reason: the gate is !!TOGT's. 15 specs inTests/eventtrace_spec.luaload the REAL!!TOGTfiles from the sibling folder, not a stand-in;Modules/EventTrace/EventTrace.luais at 100% line coverage. Locations:Modules/EventTrace/EventTrace.lua,GUI/EventTraceTab.lua,TOGTools.lua, every.toc,Tests/eventtrace_spec.lua,Tests/moduleregistry_spec.lua.
Bug Fixes
- Quest DB threw a load error on the Classic client:
Attempt to register unknown event "QUEST_DATA_LOAD_RESULT"(24x). Reported by QuestDB (inbox threadb0734afe) from the user's_classic_client. The event exists only in the live and Forever trees (QuestLogDocumentation.lua); it is not in the classic or classic_era docs.Modules/QuestDB/QuestDB.lualoads on every flavour and registered it unguarded at file load, andRegisterEventthrows on an event the client does not have. The registration is nowpcall'd; the walk only runs on Forever (UnavailableReason), so nothing is lost elsewhere. New spec: "loads on a client that does not have the reply event" (Tests/questdb_spec.lua), whose frame stub throws on anyRegisterEventas the client does. Not verified in game. Location:Modules/QuestDB/QuestDB.lua.
[v0.9.6] (2026-09-29) - Diagnostics explains Lua errors
New Features
- Explain a Lua error. Players bring errors to whoever they know, whether or not the addon is
theirs: the prompt for this was a Retail
LibOpenRaid.lua:1222: attempt to compare a secret number value (execution tainted by 'Details')brought to the TOG author. New pure-logicaddon.ErrorInterpreter(Interpretreturns a result table,Explainreturns the display text) reads a singlepath:line: reasonline or a whole BugSack / Blizzard error-window paste. It works out what the error means (16 rules: secret value, blocked / forbidden action, attempt to call / index / compare / concatenate / arithmetic, bad argument, stack overflow, script ran too long, SavedVariables too large, missing library, XML from another game version, and more), which addon folder owns the file (it also handles a cut-off...face/AddOns/path, a path pasted without itsInterface/AddOns/prefix, and[string "@..."]), and whether the file is inside a library bundled with another addon (Foo/Libs/LibBar/). It then says who to report it to. For Blizzard code or a standalone library addon (Lib*,Ace*,Ace3), the report goes to the game'stainted by 'X'marker first, otherwise the first real addon in the stack (skipping BugGrabber/BugSack). The stack scan stops atLocals:. In the Diagnostics tab: a new Explain entry in the row right-click menu, and a plain left-click on a row does the same (added after the user could not find how to get a row into the window). There is also an Explain an error button that opens a paste window updated as you type. The window shows the addon's installed title, version and author throughGetAddOnMetadata, and says whether it is a Pimptasty addon (authorPimptastyor aTOG*folder). The label is "Pimptasty", not "TOG", because not all of the user's addons are TOG ones. For a Pimptasty addon, the Discord invite (EI.DISCORD, the same link the README uses) appears in a read-only copy field right under the reading: click it and press Ctrl+C, since addons cannot write the clipboard. It was briefly a separate button; the user asked "why can't we just have the copy box appear in the text". The field is the newaddon.UI.CopyField, which selects all on click and puts the text back if typed over. Its select-all hook on AceGUI's pooled edit box acts only while TOGTools holds the widget. The field is shown only for Pimptasty addons, so the explanation pane is rebuilt on each render (AceGUI's List layout leaves room for a hidden child). The Explain window snaps to the main window's right edge with LibAceGUIWidgets'DockWindow(user request). It is docked by default; dragging it away undocks it, dropping it near the edge re-docks it, and the choice is kept per character inchar.frames.explainDock. An AceGUI Frame moves from its title bar's mouse handlers and never fires theOnDragStopthat DockWindow listens to, so the title bar'sOnMouseUpcallsh:Moved(). The main window's frame goes back to AceGUI's shared pool on close, and the Explain window must let go of it first, or it would dock to whichever addon's window reused that frame. A newMainWindow.beforeReleasehook calls the library'sW:UndockWindow(MINOR 38, requested on thread2472500eacf8; that MINOR also lets go of a released AceGUI frame by itself). On an older library, it pins the Explain window and re-points its dock at a private hidden frame. Output is ASCII only. Location:Modules/Diagnostics/ErrorInterpreter.lua(new, added to all seven TOCs),GUI/DiagnosticsTab.lua,GUI/MainWindow.lua. Spec:Tests/errorinterpreter_spec.lua, which uses the Details error verbatim; line coverage is 100%. After Peer Review's reply to this session's self-audit (thread4e63f914), there were three fixes, each with a spec. The stack scan skips the error's own message line, so a reason quoting a file ("... in Foo/Bar.lua") no longer names Foo as the caller; that spec was written first and failed first. The Explain window interprets once per keystroke and passes the result toExplainandWantsAddonLoad, which now accept either text or a result. It used to interpret the same text three times. And the "Pimptasty addon" test isEI.IsOurs, a substring match on the TOC Author line, because co-authored addons carry the name inside a longer line. - Diagnostics keeps each error's original message (
msg, new entries only, capped at 500 characters, not part of the dedup key), because the compactsourcedrops the folders where an embedded library shows. Rows saved before this, and blocked-action rows, are rebuilt into the game's own wording for Explain. Location:Modules/Diagnostics/Diagnostics.lua. Spec:Tests/diagnostics_spec.lua.
Improvements
- One copy box, from the widget library. The user asked for the "copy" popup to live in
LibAceGUIWidgets so every addon can use it. That library method,
W:ShowCopyBox(text, opts)(one button, text selected, read-only), was requested on the library's inbox (thread737274f20c2f) and built there at MINOR 38 (library v0.2.8). TOGTools now copies through a single helper,addon.UI.ShowCopy(text, prompt, parent), which callsShowCopyBoxwhen the installed library has it and otherwise builds the same box from the library's existingShowDialog(one button, pre-filled, selected). So nothing in TOGTools has to change when the library ships it. The hand-rolledStaticPopupDialogs["TOGTOOLS_DIAG_COPY"]is gone; the Diagnostics row Copy uses the helper.parentis passed on purpose: the library dialog takes its parent's strata, so parenting to UIParent would open the box behind the FULLSCREEN_DIALOG windows. Location:GUI/UI.lua,GUI/DiagnosticsTab.lua. Spec:Tests/uishowcopy_spec.lua(both paths). - Diagnostics is its own main tab, for every player, not a developer-only Logs sub-tab. User
report, 2026-09-29, with a screenshot of Settings > Modules: the Diagnostics tab did not appear
and there was no toggle for it. I had made it visible only when Settings > General > Developer
Tools was on, so no player could reach it, and neither could the user with Developer Tools off.
It briefly became a Log category, and then the user asked: "it needs to be it's own main tab, and
not a subtab". It now registers in
addon.modules["diagnostics"](no longeraddon.logCategories), withtabKey/configKey/label/WINDOW_SIZEinGUI/DiagnosticsTab.lua, the usual automatic Settings > Modules toggle backed bydiagnostics.enabled(default on), andshowWhenDevso it stays visible under Developer Tools with the toggle off. The gate (Diagnostics.IsWanted, which is alsorecordingEnabled) isIsModuleEnabled("diagnostics")OR Developer Tools. Switching it on runsDiagnostics:Wake()(broker, fallback capture, draining the companion's early buffer), from the module toggle'sOnModuleToggledand from the Developer Tools setting, replacing the startup code the Settings file used to carry. The toast, the Titan broker and/togt diagopen the main tab./togt diagno longer insists on Developer Tools: it refused players even after the tab was surfaced, which I missed when I changed the gate./togt logs diagnosticsstill gets there, and/togt diagis now in/togt help. Known cost: with it on by default, every player records errors (capped at 500 distinct entries) and sees the new-error pop-up. Location:Modules/Diagnostics/Diagnostics.lua,GUI/DiagnosticsTab.lua,GUI/Settings.lua,GUI/LogsTab.lua,SlashCommands.lua,TOGTools.lua(default). Specs:Tests/diagnostics_spec.lua(the recording gate, the main-tab registration, Open, Wake) andTests/logregistry_spec.lua(Diagnostics exemptions removed). - Explain sends a missing-dependency error to the Addon Load tab. User request: "if the error
is the missing dep error, it needs to have some way to click to open up the addon load tab".
New
AddonLoad:GetMissingDeps(name)answers one addon's missing required dependencies, the same way the tab works them out. Explain asks it for the culprit and every addon in the stack, names what each is missing under a new "Missing addons" heading, and shows an Open Addon Load button (EI.WantsAddonLoad). It does the same for an error that is itself about a dependency (a newdepsrule) or a missing library. Location:Modules/AddonLoad/AddonLoad.lua,Modules/Diagnostics/ErrorInterpreter.lua,GUI/DiagnosticsTab.lua. Specs:Tests/addonload_spec.lua,Tests/errorinterpreter_spec.lua. - The Discord copy field is the library's. The user asked for the library's copy box with its
copy icon. LibAceGUIWidgets built an inline widget,
LAGW-CopyField(MINOR 38, inbox thread513d5779bb63), andaddon.UI.CopyFieldcreates it whenAceGUI:GetWidgetVersion("LAGW-CopyField") >= 2. Version 1 errored on every Create ("FontString:SetText(): Font not set", its label had no font); the user hit it in game, it was reported (thread63629c29099f) and fixed as Version 2. On an older library,UI.CopyFielddraws the same box itself: the library'sCOPY_ICONwith its numbers (LEFT +1,-1, 14px, inset 20, the 1.0/0.6 tint), and on release it hides the icon and puts AceGUI's insets (0, 0, 3, 3) back on the pooled edit box. Location:GUI/UI.lua.
Documentation
README and the CurseForge page checked claim by claim against the code (user request: "ensure we have addon functionality parity with the documents"). Corrected where the documents said something the code does not do:
- Both promised a history-retention setting.
retentionDaysis only a default (TOGTools.lua), andGUI/Settings.luahas no control for it, so both now say "older than 90 days". - The Gratz achievement triggers were described as Retail-only.
Modules/Gratz/Gratz.luaregisters the achievement events on every flavour, so both now say "a version of WoW that has achievements". - The README's Gratz and Smack Say/Yell rule, and the page's Smack Emote note, left out that
Midnight and Forever block automatic Say/Yell/Emote everywhere (
Compat.lua, viaVersion.luaisRetail120Plus). - Bloodlust was listed as working on Classic Era.
Modules/Smack/Smack.luasays it never lands there. - The gathering summary's sources were described as "the best". They are the three most gathered
by item count (
GatheringLog.lua). - The custom date filter was described as a single day.
GUI/DateRangePicker.luapicks a day or a range, labelled "Custom...". - Diagnostics "records nothing" when switched off now adds "unless Developer Tools is on".
- Quest DB was described as a Forever-only tab. It shows on every flavour behind Developer Tools and only works on Forever.
- Walk recipe sources was described as covering "every known recipe". It covers the TOG profession database and needs ProfessionDB, which both Requirements sections now list as optional.
- Switching Login Digest off from its tab hides the tab. Both documents now say so.
- The Addon Load status list now says "include", since unlisted client reasons show raw.
Added what the code has and neither document mentioned: Settings > Clear Data, the verbose debug switch, the minimap right-click to settings and its per-character hide, the gear icon, Login Digest's Print test digest, Smack's Manual trigger, and the missing slash aliases (
versioncheck,db,logs bank/gbank/whisper, plus the README's aliases now copied into the page).- Both promised a history-retention setting.
Stale in-game help text. The Mailbox help said the threshold "lives in the addon settings panel", but it is the slider on the tab (
GUI/MailboxTab.lua). The Logs help still called Trade "coming in a follow-on patch" (GUI/LogsTab.lua). Both Gathering descriptions in Settings left out gas and pickpocketing (GUI/Settings.lua). Two code comments were also out of date.
[v0.9.5] (2026-09-28) - I'd Hit That on WoW Forever; Name Prefix matches a first or last name
Bug Fixes
- I'd Hit That said "not applicable" on WoW Forever. Player report, 2026-09-28 (screenshot of
the tab: "Hit chance was removed as a stat in Warlords of Draenor"). I gated the whole module on
gv.isRetail, which is true on Forever because it is the Retail client, and never checked whether Forever has hit. It does: its own character sheet computes melee, ranged and spell hit asGetCombatRatingBonus(CR_HIT_*) + Get*HitModifier()(wow-ui-source-foreverBlizzard_UIPanels_Game/Camelot/PaperDollFrameStats.lua:489-496). The engine now asksNO_HIT_STAT = gv.isRetail and not gv.isForevereverywhere it used to askisRetail, and the tab shows its settings on Forever. Forever has no weapon-skill API in its generated docs, so the existing no-skill path is used (Blizzard's miss table, glancing "not known"); ranged hit is read the way Forever's sheet reads it (GetRangedHitModifier+CR_HIT_RANGED, no scope tooltip scan), Forever only. Every one of those calls isSecretWhenUnitStatsRestricted(PlayerScriptDocumentation.lua), so each read goes throughstat(): a secret value marks the rebuild unreadable, the previous numbers are put back and the player half stays dirty so the next refresh retries. Retail proper is unchanged (still always-hits, no stat reads). Not verified in a Forever client, including the HUD's placement on Forever's target frame. Location:Modules/IdHitThat/IdHitThat.lua,GUI/IdHitThatTab.lua. Spec:Tests/idhitthat_spec.lua("WoW Forever", three cases). - Name Prefix's "Hide prefix if nickname matches character name" never matched on Forever.
Player report, 2026-09-28: character "Vishi Swaz", nickname "Vishi". Forever allows a space in a
character name (a first and a last name), and the test was an exact, case-sensitive
nick == UnitName("player"). NewnickMatchesNamematches the whole name or any one word of it (first or last name, operator 2026-09-28), ignoring case; a partial word does not match. The player also suggested per-character checkboxes; not built, as the name match covers the report. Location:Modules/NamePrefix/NamePrefix.lua,GUI/NamePrefixTab.lua(checkbox description). Spec:Tests/nameprefix_spec.lua.
[v0.9.4] (2026-09-28) - Quest DB developer tool; Item DB opens on WoW Forever and walks its whole item range; Profession Capture on Forever
New Features
- Quest DB developer tool (WoW Forever only). QuestDB inbox thread 76745964: LibQuestDB has no
offline source for Forever's own quests (QuestieDB's Forever TOC carries the Vanilla set, and
Forever's QuestV2 DB2 has no titles), so this asks the live client. It walks the ids
LibQuestDB:GetCoverageanswers"uncovered"for, not every non-"shipped"id, because"unknown"spans the whole range. Each quest is requested withC_QuestLog.RequestLoadQuestByIDand read onQUEST_DATA_LOAD_RESULT(both confirmed inwow-ui-source-foreverQuestLogDocumentation.lua:1219,:1336). The walk is paced by replies, not a ticker: at most 10 requests outstanding, and each reply sends the next. A_fillingflag guards re-entry, because the event is declaredSynchronousEvent. The cost: a request the server never answers holds its slot. When all ten are held, the tab says "Waiting", and Pause then Fill gaps re-sends them. Saved asdb.global.questDB.quests[id] = "title\31level\31groupSize\31elite\31questType\31obj\30obj...\31tip\30tip...", the format QuestDB's generator reads. A field the client does not return is stored empty. Separators are stripped from client text. Off Forever, or without LibQuestDB loaded, the tab says why and nothing runs. The walk's ceiling is the end of LibQuestDB's last coverage run (QuestDB:TopID). I first shipped a fixed 65000 with a comment claiming the ids sat below it, without checking. Forever's coverage runs to 99234 and most of its ids are above 73000. The first in-game walk therefore stopped at cursor 65001, marked itself complete, and saved 64 quests. A store in that state resumes from its cursor on the next Build / Resume; the operator's resumed walk finished at cursor 99235 with 833 quests (QuestDB, thread 76745964). That first walk did show that objectives and tooltip text come back for a quest the player does not hold. Location:Modules/QuestDB/QuestDB.lua,GUI/QuestDBTab.lua, all seven TOCs (QuestDBadded toTOGTools_Camelot.tocOptionalDeps), thequestDBdefaults inTOGTools.lua, andC_QuestLogin.luacheckrc/.luarc.json. Spec:Tests/questdb_spec.lua; ungated exemption recorded inTests/moduleregistry_spec.lua.
Bug Fixes
Item DB tab threw "SetPoint would result in anchor family connection" on open (WoW Forever). ItemDB inbox thread a2110cf1; the operator saw it 3x opening the window from the minimap button. The failing call is AceGUI's Flow layout anchoring the tab's first group to the TabGroup content (
AceGUI-3.0.lua:717); the client refuses that when the content already depends on the group. AceGUI pools widget frames for every addon andReleaseclears only the released frame's own points, so a group another frame still anchors onto can come back fromAceGUI:Create. I did not find the stray anchor in TOGTools' own code (everySetPointonto a widget frame inGUI/was read), so which addon leaves it is not verified. NewUI.AnchorPath(from, target)walks a region's points breadth-first and returns the chain, andUI.CreateUntangled(AceGUI, type, container)refuses a pooled widget the container already depends on: it is hidden, kept out of the pool for the session, and a fresh one is taken; the first time, the chain is printed once in chat so the frame holding the stray anchor is named. The Item DB tab's four groups use it. Not verified in game. Location:GUI/UI.lua,GUI/ItemDBTab.lua. Spec:Tests/uianchor_spec.lua.Profession Capture's "Walk recipe sources" looked like it did nothing. On Forever it walked all 2,511 ProfessionDB recipes, got 0 answers, and said nothing (operator's screenshot, 2026-09-27). Blizzard only calls
C_TradeSkillUI.GetRecipeSourceTextfor a recipe shown in the open profession window, and only for an unlearned one (wow-ui-source-foreverBlizzard_ProfessionsRecipeSchematicForm.lua:633-634). A second walk with the trainer and the Blacksmithing window open also answered 0. The operator's screenshot shows why: the window listed only learned recipes, because its "Show unlearned" filter (Blizzard_Professions.lua:1222) was off.retailKnownasks for a source only for an unlearned recipe, so it had nothing to ask about. Changes:- Capture never writes the profession window's filters. I first forced "Show learned" and
"Show unlearned" on, the way the trainer's are, and that locked the WoW Forever profession
window (K) so nothing in it could be clicked (operator, 2026-09-28; it cleared with TOGTools
disabled). Cause: Forever's
Professions.GenerateCraftingDataProviderforces "Show unlearned" back off every time it rebuilds the list (wow-ui-source-foreverBlizzard_ProfessionsTemplates/Camelot/Blizzard_Professions.lua:17-20), each change re-lists and firesTRADE_SKILL_LIST_UPDATE, and my handler turned it on again, so the two fought without end.OpenTradeSkillFilters,RestoreTradeSkillFilters,TradeSkillFiltersOnand theTRADE_SKILL_CLOSEregistration are gone; the list the window shows is read as it is. CaptureKnownmerges a Retail read into the stored profession, so a narrower list (learned only) keeps the recipes an earlier read listed instead of overwriting them.WalkRecipeSourcesalso counts every source the window capture read. These are keyed by the client's own recipe id, which covers ids ProfessionDB lacks.- A walk keeps every source an earlier walk found, and reports
answeredNow(what this pass added). - The button prints the result in chat.
- The walk now also records
sourceTypefromC_TradeSkillUI.GetRecipeInfo, labelled withBATTLE_PET_SOURCE_<n>, the same numbers and labels Blizzard's own Sources filter uses (Blizzard_Professions.lua:1255-1272). The operator's SavedVariables showGetRecipeInfoanswered for all 2,496 unlearned recipes with no window open, whileGetRecipeSourceTextanswered none. The walk was already calling it and threw that field away.retailKnownkeepssourceTypetoo. A recipe counts as answered when it has either a source type or source text, and is never counted twice. - Forever gives no source at all -- measured. The operator's field dump for unlearned
recipe 7928 (2026-09-28) lists every
GetRecipeInfokey: nosourceTypeor anything like it, andGetRecipeSourceTextreturned nil. The forever tree'sTradeSkillUIDocumentation.luahas no other source call, only the window's source-type filter, which would mean writing the window's filters (see above). The walk now keepsskillLineAbilityIDper recipe (4414 for 7928), the client's SkillLineAbility row, so an offline build can join it to how the recipe is learned, and counts those aswalk.linked. A link is not counted as an answer. When nothing answers, the chat line now says the client gives no recipe sources and how many links were saved, instead of printing the sample dump.
Not verified in game, apart from the Forever field dump above. Location:
Modules/ProfCapture/ProfCapture.lua(CaptureKnown,OnEvent,WalkRecipeSources),GUI/ProfCaptureTab.lua. Spec:Tests/profcapture_spec.lua.- Capture never writes the profession window's filters. I first forced "Show learned" and
"Show unlearned" on, the way the trainer's are, and that locked the WoW Forever profession
window (K) so nothing in it could be clicked (operator, 2026-09-28; it cleared with TOGTools
disabled). Cause: Forever's
Item DB walk stopped at ID 240,000, below WoW Forever's items. ItemDB inbox thread 500e4ce2: Forever (build 1.60.1.69913) has 4,862 item IDs above 240,000, up to 286,427.
DEFAULT_TOP_IDis now 320,000, andraiseTopIDlifts a storedtopIDbelow it on everyStartandFillGaps, so an existing walk (stored 240,000, cursor 240,001, even marked complete) resumes from its cursor into the new range instead of keeping the old ceiling.FillGapsraises before placing the cursor past the ceiling, so it still skips the walk. The comment claiming the ceiling was "editable via the tab" was wrong (the tab only displays it) and is replaced. Location:Modules/ItemDB/ItemDB.lua. Spec:Tests/itemdb_spec.lua("walk ceiling").wow-version-replication.ps1could delete a live file from every replica install. Peer review259f99e3(TOGProfessionMaster): a save-by-replace raises Deleted and Created/Changed for one save, in either order, and when Deleted ran lastSync-Fileremoved the file from every other WoW install while the source still had it. A delete is now honoured only when the source file is really gone; otherwise it is copied as a change. Dev-only; the script does not ship. Location:wow-version-replication.ps1(Sync-File).
[v0.9.3] (2026-09-27) - Gratz: sister-guild level-ups, and no more roster polling
New Features
- Guild level-up profiles also fire for sister-guild members. Operator request, 2026-09-27.
LibGuildRoster (already a required dependency in all seven TOCs) reports sister members' level-ups
from its periodic
/whosightings, sister roster copies and groupedUNIT_LEVELasOnSisterMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source). Gratz routes it throughfireGuildLevelUp, so the level band, every-Nth-level interval, cooldown, skip-self and channel rules apply unchanged;[guild]is the sister guild's name (the key'sFaction-prefix stripped). The line goes to our guild chat and crosses to the sister guilds through the existing GreenWall relay. Location:Modules/Gratz/Gratz.lua("Guild level-ups",onMemberLevelChanged,guildNameFromKey). - "Sister guilds" checkbox on Level up (guild) profiles. Stored as
profile.sisterGuilds;nilmeans on, so profiles saved before this release get it, and new profiles default to on. Unticked keeps a profile to the home guild. Location:GUI/GratzTab.lua,Gratz:NewProfileDefaults.
Improvements
- The 60 s guild roster poll is gone. Gratz asked the server for the whole roster every 60 s
(
C_GuildInfo.GuildRoster) and compared every row while a guild level-up profile was enabled; the operator called that too expensive for one addon. Home-guild dings now come from LibGuildRoster'sOnHomeMemberLevelChanged(same signature), which is free: every library user's ownPLAYER_LEVEL_UPannounced on the guild addon channel ('comm'), plusUNIT_LEVELon a grouped guildmate ('unit'). GuildRoster searched every flavour tree and the web and found no server message announcing another player's level. Removed:scanGuildLevels,guildLevelUpQualifies,requestGuildRoster,guildTrackerArmed, theGUILD_ROSTER_UPDATEregistration and the login ticker. Baseline, online-at-both-readings and cross-source dedupe are now the library's rules (itsTests/sisterlevel_spec.lua); the Gratz specs that pinned them for the old scan went with it. Contracts2f198123and9c178c96. - A second guard behind the library's: a decrease, no change or a malformed call says nothing, and a home-guild key arriving on the sister callback is refused, so the two can never gratz one person twice.
wow-version-replication.ps1now replicates into WoW Forever._classic_beta_(Battle.net productwow_classic_beta, which readsTOGTools_Camelot.toc) added to the version list, and a destination is kept when the version folder exists rather than itsInterface\AddOnsfolder: a fresh client has none yet, so the old test skipped Forever silently. The AddOns folder is created with the addon folder. Verified with-DryRun(Forever listed as a target) and by the live watcher filling_classic_beta_\Interface\AddOns\TOGTools. Dev-only; the script does not ship.- Addon-list title in the brand colours, on all seven TOCs. Fleet request
ff41f4c4(DeltaSync, at the operator's direction). The version tag used|c999999FF, which is alpha-first (a pale blue, not grey), and neither colour code was closed. Now|cffFF8000TOG Tools|r |cff999999<Version>|r. A newTests/toc_spec.luacase pins the exact title per flavour. Not yet seen in the client's AddOns list. Locations: every.toc,Tests/toc_spec.lua.
Bug Fixes
- Guild Log read Blizzard's event buffer twice per query on Classic.
requestQueryarms a 1.5 s fallback ingest for Retail, whereGUILD_EVENT_LOG_UPDATEno longer fires; on Classic the event arrives too, and both ran, so every query re-hashed all 100 buffer entries twice (the identical pairs every ~30 s in the operator's 2026-09-27 debug trace). The event handler now cancels the pending fallback before ingesting. Location:Modules/GuildLog/GuildLog.lua. Specs:Tests/guildlog_spec.luacounts ingest passes on both paths; the Classic spec was run red with the cancel line removed (1 failed) and green with it restored. /togt glcheckwrote to the saved Guild Log.GuildLog:Diagnose's instrumented ingest walktinserted every new buffer row into the bucket, with noid, unsorted and unpruned, and the next real ingest would not re-sort them (it sorts only when it appends). The walk now counts what the real ingest would append ("would append / deduped") and writes nothing. Location:Modules/GuildLog/GuildLog.lua. Found by the self-audit's coverage pass.- One level-up could post twice in the same channel. Since LibGuildRoster MINOR 27 a grouped
guildmate's ding reaches the party trigger (Gratz's own
UNIT_LEVEL) and the guild trigger (the library's'unit'source) at once, so a party profile and a guild profile both set to Guild chat said it twice.sendToChannelsnow keeps one line per (player, level, channel) for the session, first profile wins; profiles aimed at different channels still each post. A repeated identical ding is therefore no longer re-gratzed after its cooldown. Peer review, inbox thread0f319d11. Location:Modules/Gratz/Gratz.lua(_dingSent,tryFire'sdingKey). The spec for profile order and two cooldown specs moved to the achievement trigger, which the rule does not touch. - Guild Log line coverage 53% -> 100% (437/437): specs for
/togt gldump,/togt glcheck, the default-rank lookup, the login query,Requery, theC_Timer.Afterfallback and the sub-tab refresh.Gratz.lua404/404. Full suite 1126 passed, 0 failed (desk, 2026-09-27, after the TOC title spec).
Known limits
- Needs LibGuildRoster MINOR 27 (GuildRoster 1.1.0), released before this. On an older
library neither callback fires and guild level-up gratz go quiet; registering an unfired name is
harmless under CallbackHandler. Seen in game 2026-09-27 (Classic Era): a home guildmate's ding
to 15 was gratzed in guild chat. Which library source fired it was not captured (the order
against GRM's own announcement proves nothing: two pollers on different schedules); with debug on,
every callback now prints
[gratz] home|sister level-up: ... (source=...)before any filter. Sister-guild dings, the grouped path, the "Sister guilds" checkbox and the no-library-addon re-check are not yet seen in game. - The 60 s re-check moved INTO LibGuildRoster, shared by every addon (operator, 2026-09-27,
contract
7e447fb6). The library re-reads the home roster about once a minute while any addon holdsRequestLevelRefresh, keepsmember.levelcurrent for every member, and firesOnHomeMemberLevelChangedwith source'roster'under the same rules. Gratz holds it exactly while the module is on and a guild level-up profile is enabled:Gratz:SyncLevelRefresh, called fromOnEnable,OnModuleToggled,SaveProfileandDeleteProfile(the tab's form and "On" checkbox both go throughSaveProfile). Feature-detected, so an older library is a no-op. So a guildmate who runs no library addon is still seen, as the old poller saw them, at one request per client however many addons ask. - Sister dings can be minutes late (the library's
/whocadence), or missed if the member logs off first. Seen in game 2026-09-27: a sister member's ding to 40 was never reported, because no reading of them preceded it (a first reading is a baseline). LibGuildRoster MINOR 27 now forwards a player's own ding to the sister guilds throughGreenWallAPI.SendMessage(version 2) and firesOnSisterMemberLevelChangedsource'comm'on receipt (contract58cbd95c), so a sister member running the standalone GuildRoster addon plus GreenWall 1.14.0+ is gratzed at once. Not verified in a client. - Sister gratz volume is untested. Every TOGTools user with a guild level-up profile gratzes a
sister-guild ding, and existing profiles have "Sister guilds" on. Peer review (thread
0f319d11) asks for one in-game check with two TOGTools users online; the player notes say it can be switched off.
[v0.9.2] (2026-09-25) - Addon Load names the missing dependency and shows each addon's .toc
New Features
- "Dep Missing" now names the dependency. A row the client refuses with a
DEP_*reason readsDep Missing: LibX, LibY, listing every required dependency that is not installed or will not load. Until now the tab only said that something was missing.AddonLoad:GetDatareads each addon's## DependenciesthroughC_AddOns.GetAddOnDependencies(the call Blizzard's own AddOn list tooltip uses,Blizzard_AddOnList/AddonList.lua:849in the Era tree) and resolves each one:C_AddOns.DoesAddOnExistfalse -> "Not installed";IsAddOnLoaded-> "Loaded"; otherwise the client's reason fromGetAddOnInfo(dep)(Disabled, Wrong Version, ...). A dependency that is installed, loadable and not yet loaded (load-on-demand) counts as fine. Location:Modules/AddonLoad/AddonLoad.lua(depState,collectDeps). - Hover a row to see its .toc. An addon cannot open a
.tocfile, so the tooltip reads it back through the client: title, folder name,## Notes, status,## Versionand## Author(C_AddOns.GetAddOnMetadata), the interface number (C_AddOns.GetAddOnInterfaceVersion), and the required and optional dependency lists (GetAddOnOptionalDependencies), each dependency coloured green or red with its state. Wired through RowList'sonRowEnter/onRowLeave, which the translator passes to LibAceGUIWidgets unchanged. Location:GUI/AddonLoadTab.lua(showRowTooltip).
Improvements
- Every new API is feature-detected on
C_AddOns. A flavour without the dependency calls shows the bare "Dep Missing" label and an empty list, never an error.GetAddOnInfoon a dependency name ispcall'd: whether it raises or returns for an uninstalled name is not verified on every flavour, so a raise, or aMISSINGreason, reads as "Not installed". - Sort priority keys on the bare label, so
Dep Missing: LibXstill sorts with the failures. Tests/addonload_spec.luacovers the module at 149/149 lines. Eleven dependency specs, plus three for paths no spec had run before: the module's ownADDON_LOADED/PLAYER_LOGINtiming capture used when!TOGTis absent, andRefreshMemory. Not verified in a client.
[v0.9.1] (2026-09-25) - LibGraph-2.0 and LibDBIcon-1.0 become required dependencies instead of embedded copies
Improvements
- LibGraph-2.0 is now the standalone "LibGraph-2.0 Revived" addon, not a vendored file. All
seven TOCs add
LibGraph-2.0to## Dependenciesand stop loadinglibs\LibGraph-2.0.lua, which is deleted..pkgmetaaddslibgraph-2-0-revivedtorequired-dependencies, so CurseForge installs it alongside TOGTools; that slug is the one FastGuildInvite already ships with. The TOC line decides whether TOGTools loads and the.pkgmetaline decides what gets installed, so both are needed. - Why the embedded copy was wrong, not just duplicated. LibGraph finds its line and bar
textures by parsing
debugstackfor the directory its.luasits in, and expects the.tgafiles beside it (LibGraph-2.0/LibGraph-2.0/*.tgain the standalone addon). TOGTools shipped the.luaflat inlibs\with no textures at all. The standalone copy also carries the maintained fixes and a minor (92069) that out-ranks older embedded copies.GUI/LogGraph.luareads the library throughLibStub("LibGraph-2.0")at file scope and needs no code change. - LibDBIcon-1.0 (and the LibDataBroker-1.1 it carries) is a required dependency on six of seven
flavours. The Era, TBC, Wrath, Cata, Mists and Mainline TOCs add
LibDBIcon-1.0to## Dependencies, drop the## OptionalDeps: LibDataBroker-1.1, LibDBIcon-1.0line and stop loadinglibs\LibDataBroker-1.1.lua/libs\LibDBIcon-1.0.lua;.pkgmetaaddslibdbicon-1-0torequired-dependencies. The standalone addon (Funkeh, v12.0.3) loads LibStub, CallbackHandler-1.0 and LibDataBroker-1.1 from its ownembeds.xml, so one dependency covers both librariesGUI/MinimapButton.luaasks LibStub for. Why: an embedded copy only reaches the players of the addon that shipped it, so every LibDBIcon update meant a release of every addon with a minimap button; as a CurseForge dependency it updates without one. Kept against: the embedded copy's zero install cost.TOGTools_Camelot.tocstill embeds both files, because the standalone's TOC lists no16001and an out-of-date required dependency would keep TOGTools from loading on WoW Forever (inferred from how the client treats out-of-date addons, not observed on a Forever client). The embedded copy was already MINOR 56, the same as upstream v12.0.3, so it was not refreshed. .luarc.json(TOGTools and !!TOGT) setsdiagnostics.libraryFilesanddiagnostics.ignoredFilestoDisable, so vendored code inlibs\stops filling the Problems panel whenever one of its files is open in a tab. TOGTools'workspace.libraryalso points at the sibling../LibGraph-2.0/LibGraph-2.0folder soLogGraph.luakeeps its type information.GUI/MinimapButton.lua: the unused_selfargument of the LDBOnClickis now_.Tests/toc_spec.lua:INTENTIONAL_OMISSIONSlistslibs\LibDataBroker-1.1.luaandlibs\LibDBIcon-1.0.luafor every flavour except Forever, so the cross-flavour parity check pins the Forever-only embed instead of failing on it.- Docs:
README.mdanddocs/Curseforge_Description.htmllist LibGraph-2.0 and LibDBIcon-1.0 under Requirements (LibDBIcon auto-installed everywhere but Forever, which keeps a built-in copy). They no longer call LibDataBroker/LibDBIcon optional. Smack's big-hit trigger now notes environmental damage, a detail kept from the v0.8.6 notes that roll off Recent Updates.
[v0.9.0] (2026-09-24) - Profession Capture developer tool; every table now drawn by LibAceGUIWidgets
New Features
Profession Capture (developer tool). Built for ProfessionDB (LibProfessionDB-1.0), inbox contract
e2868011. The client's DBC says which recipes exist; only a live client standing at the NPC can say which ones a player can obtain, and where. A walker turns on Capture in the newProfession Capturetab (Developer Tools only), visits trainers and recipe vendors and opens their own profession windows. The SavedVariables go back to ProfessionDB. NewModules/ProfCapture/ProfCapture.luaandGUI/ProfCaptureTab.lua, listed in all seven TOCs. Passive: it reads on window events (TRAINER_SHOW/TRAINER_UPDATE,MERCHANT_SHOW/MERCHANT_UPDATE,TRADE_SKILL_SHOW/TRADE_SKILL_UPDATE/TRADE_SKILL_LIST_UPDATE,CRAFT_SHOW/CRAFT_UPDATE,LOOT_OPENED) and sends no request of its own. Gated three ways:devTools, the module toggle (IsModuleEnabled("profcapture")), andprofCapture.capture, which defaults off.enabledstays on by default becauseMainWindowhides a disabled module's tab, and that would hide the switch.- Two API shapes, by
gameVersion.isRetail. Read from the Blizzard trees underF:\Blizzard API Docs. On the Retail client, including WoW Forever,GetTrainerServiceInforeturnsname, type, texture, reqLevel, subText, category(wow-ui-source-foreverBlizzard_TrainerUI/Mainline/Blizzard_TrainerUI.lua:425), and the spell id comes fromC_TooltipInfo.GetTrainerService(i).id. On every Classic client it returnsname, subText, type, isExpandedwith header rows. That order is the same in the classic_era, classic_anniversary and classic (Mists) trees. The spell id comes from a hiddenGameTooltip'sSetTrainerService+GetSpell. Vendors useC_MerchantFrame.GetItemInfoorGetMerchantItemInfo. Recipe lists useC_TradeSkillUI(GetFilteredRecipeIDs,GetRecipeInfo,GetRecipeSchematic) orGetTradeSkillInfo/GetCraftInfo. - The trainer filter. On show, every service filter (
available,unavailable,used) is turned on and the player's own is saved;TRAINER_CLOSEDputs it back. A read is kept only when all three are on, so a filtered list never overwrites a complete one. If Blizzard's trainer frame re-applies its own filter afterTRAINER_SHOW, the nextTRAINER_UPDATEturns ours back on and the re-list that follows is read. There is no timer. - Not verified in a client. Nothing here has run in game. Unverified points: that
SetTrainerServiceTypeFilterstill applies afterTRAINER_CLOSED; what an enchant'sGetCraftItemLinkcarries on Classic Era (Era's craft frame calls onlyGetCraftItemLink, neverGetCraftRecipeLink--Blizzard_CraftUI/Vanilla/Blizzard_CraftUI.xml:571-574-- so the spell id is read from it as a fallback, and a recipe with neither lands inunkeyed; Era's trade-skill frame does callGetTradeSkillRecipeLink,Blizzard_TradeSkillUI.xml:11); and thatGetFilteredRecipeIDsreturns every row while the player's profession-window filters are set. It honours those filters, so a filtered list is a partial list.
- Two API shapes, by
Recipe-source walk: the NPC-free route, Retail client only (WoW Forever included). A Walk recipe sources button in the tab.
ProfCapture:WalkRecipeSources()takes every recipe spell id LibProfessionDB ships (GetProfessions/GetRecipes). For each one it asksC_TradeSkillUI.GetRecipeSourceText(id): the text the Professions window shows under an unlearned recipe (Blizzard_ProfessionsRecipeSchematicForm.lua:632-638in the forever and live trees). The pass is synchronous: local calls, no timer and no travelling. Result:profCapture.walk = { recipes = { [spellID] = { prof, learned?, source? } }, total, answered, build, locale, ts }. The same text is also recorded during a recipe-list capture, assourceon an unlearned recipe and asnextRecipeID+nextRankSourceon a learned one.ProfessionDBis an OptionalDep of the Camelot and Mainline TOCs so it loads first. No Classic client has this call. The writblizzard-apiindex finds it only in the forever and live trees, and the classic_eraTradeSkillUIDocumentation.luahas no recipe-info or source function. So Classic has no walk. Not verified in a client: whether the call answers for a profession the walking character lacks, or with no profession window open. The walk'sansweredcount settles that on the first click. Specs: 5 more examples, covering an unsupported client, ProfessionDB missing, no store, a full walk with counts and purge, and the source text in a recipe-list capture. The Profession Capture tab shows the walk's "answered" line only on a client that has the call, since "0 of 0" on Classic read as a failed walk.SavedVariables shape, schema 1 (
TOGTools_DB.global.profCapture), for ProfessionDB's parser. Every record carriesbuild("<version>.<build>"fromGetBuildInfo, e.g.1.60.1.69977),localeandts(server time). Fields shown?may be absent.profCapture = { enabled = true, capture = false, trainers = { [npcID] = { npcID, name?, zone?, subzone?, mapID?, faction?, trainerType?, rank?, maxRank?, collapsedHeaders?, -- Classic only: how many headers were collapsed (rows hidden) seen, -- how many complete reads this NPC has had build, locale, ts, services = { { -- array, in the trainer's own order spellID?, name, subText?, category?, type, -- "available"|"unavailable"|"used" reqLevel?, skill?, skillRank?, cost?, abilities? = { "<ability name>", ... }, }, ... }, } }, vendors = { [npcID] = { npcID, name?, zone?, subzone?, mapID?, faction?, seen, build, locale, ts, recipes = { [itemID] = { -- Recipe-class (9) items only name?, price?, stack?, numAvailable?, -- nil = unlimited stock extendedCost?, } }, } }, known = { ["Name-Realm"] = { [professionID or profession name] = { name, skill, maxSkill, professionID?, -- professionID on Retail childProfessionID?, childSkill?, childMaxSkill?, collapsedHeaders?, build, locale, ts, recipes = { [spellID] = { name, learned, difficulty?, -- "optimal"|"medium"|"easy"|"trivial" itemID?, minMade?, maxMade?, numSkillUps?, categoryID?, source?, -- Retail: unlearned recipe's source text nextRecipeID?, nextRankSource?, -- Retail: learned recipe's next rank reagents = { { itemID, count }, ... }, } }, unkeyed? = { <recipe as above>, ... }, -- Classic rows with no readable spell id } } }, drops = { ["<itemID>:<sourceKind>:<sourceID>"] = { itemID, sourceKind?, sourceID?, -- from the loot source GUID, e.g. "Creature", 570 count, first, zone?, subzone?, mapID?, build, locale, ts, -- ts = last seen } }, walk? = { -- Retail only; the latest Walk recipe sources pass, replaced whole total, answered, build, locale, ts, recipes = { [spellID] = { prof, learned?, source? } }, }, }Two limits the parser should know. A trainer or vendor record is replaced whole on each complete read, so it holds the latest visit rather than a merge. Service
typebelongs to the character who opened the trainer. On Classic a service whose tooltip yields no spell id (Classic Era'sGetSpellon a trainer-service tooltip is not verified) is still recorded, withoutspellID: a raisingSetTrainerServiceorGetSpellis caught. Specs:Tests/profcapture_spec.lua, 34 examples covering both API shapes, the filter handling, the gate, a failing spell-id tooltip, vendors, both recipe frames, drops, the walk, and the event wiring. Full suite: 1075 passed, 0 failed;ProfCapture.lua376/376 lines covered.
Improvements
Every list now draws through LibAceGUIWidgets. The eleven tabs' tables (the six logs, Diagnostics, Item DB, Addon Load, Gratz, Smack) are drawn by LibAceGUIWidgets-1.0's
RowList, MINOR 34. The library added what they relied on at TOGTools' request (inbox threadc571dee6):- the header sort/filter menu, with cascading filters,
filterGroupand a cap; SetSearch,autoFitand hyperlinks in cells;Detachfor AceGUI's recycled parents;col.align,col.gapBeforeandcol.sortDescDefault.
GUI/RowList.luawent from ~1100 lines to a translator that maps the options the tabs were written against onto the library's, so no tab changed:format(entry)becomesformat(value, entry);sortKeybecomessortValue;- a RIGHT/CENTER
justifybecomesalign; headerTipDescbecomes the library'sheaderTip;tiebreakKeybecomes atiebreak(a, b)that checksname, then the key, then falls through to SetData order.
addon.UI.OpenColumnMenuinGUI/UI.luais removed, because nothing else called it.LibAceGUIWidgetsis a hard dependency in all seven TOCs (Tests/toc_spec.luapins it). Release order: this version must not ship before LibAceGUIWidgets MINOR 34 is published. The operator has settled that it is released first. As a backstop,RowList.LibraryIsCurrent()checksLibStub.minors['LibAceGUIWidgets-1.0'] >= 34. On an older library the first list built prints one chat line saying LibAceGUIWidgets is out of date, instead of silently drawing broken tables.Hyperlinks are opt-in per list, not on for every list. A link click also runs the row's own
OnMouseDown, so a list withonRowClickcould open its row menu and the chat-name menu together (Peer Review, audit threadcf6cc085). The six lists whose cells render item or player links passhyperlinks = true: Gathering, Guild Bank, Mail, Trade, Whisper and Item DB. Gratz, Smack, Diagnostics, Addon Load and Guild Log do not. The Gathering Log'ssortType = "date"option was removed; nothing reads it now.Visible differences:
- headers are the library's gold rather than TOGTools' orange, because the accent is session-global and TOGTools does not set it;
- the menu reads "Sort ascending/descending" rather than "Oldest > Newest";
- sorting is numeric-aware and case-insensitive;
- a search also scrolls the list to the top.
Specs:
Tests/rowlist_spec.luais rewritten. It pins every translation rule, and it runs the old tiebreak guarantees (findings rev-B and rev-J) end to end through the real library. A formatter's nil still draws as a blank cell rather than "nil" (the librarytostrings the result, and the translator maps nil to""). Full suite: 1075 passed, 0 failed, against LibAceGUIWidgets' working tree as it stood after Peer Review's fixes (threadaf2ebd61);GUI/RowList.lua56/56 lines covered. Not verified in a client: nothing has been drawn in game on LibAceGUIWidgets MINOR 34 yet.- the header sort/filter menu, with cascading filters,
The seven TOCs read as four blocks. Libraries, core, shared GUI and modules now sit under
#comment headings. The modules block no longer has a blank line between each engine and its tab. Load order is unchanged (Tests/toc_spec.luapins it across all seven).Test harness pin moved to WoWAPITesting
3d492c7(from7932484, ten days of Adoption-log entries).Tests/itemdb_spec.luadrops its ownstrsplitstand-in, because the harness'sstrsplitnow keeps empty fields and honours the piece limit as the client does. The spec-localIsInGroup/UnitInParty/UnitHealth/CombatLogGetCurrentEventInfoassignments in the Gratz, Smack and IdHitThat specs stay: they drive per-test scenarios, not a missing API. Full suite on the new pin: 1075 passed, 0 failed.Retail TOC declares
110207, 120100(was110207, 120008). LibAceGUIWidgets is now a hard dependency, so every Interface number TOGTools declares must be one the library declares too, or a client on that build skips the library as out of date and TOGTools with it. The library lists120100and never listed120008; LibAceGUIWidgets found120008in no TOC but TOGTools' own and no 12.0.8 patch anywhere, with 12.1 (120100) the current Retail patch (inbox threadc571dee6).Tests/toc_spec.luastill pins one 11.x floor and exactly one 12.x build. Not verified: this box does not record the Retail client's build, so120100is the library's evidence, not a reading of the installed client.
Documentation
- README and CurseForge description brought to parity. Both now describe the same features in the same detail. The README gains what only the CurseForge page had: the Name Prefix GRM duplicate-name toggle, the per-log details (Take-All merge, C.O.D., trade enchant slot, guild and bank history kept past the game's own window, date presets), Gratz placeholders and cooldown, Smack's channels and cooldown, I'd Hit That's layout options, and the Addon Load status badges. The CurseForge page gains what only the README had: a Settings section and the Developer Tools commands. Both gain a Developer Tools section (Diagnostics, Item DB, Profession Capture) and list LibAceGUIWidgets v0.2.4 (MINOR 34) under Requirements.
[v0.8.9] (2026-09-23) - Sister-guild lines cross at once through GreenWall 1.14.0's relay; older GreenWalls wait for a key press
New Features
- GreenWall 1.14.0+ relays Gratz and Smack lines with no key press. At TOGTools' request
(inbox contract
898b1e4c, the operator's directive that it be an API for ANY addon), GreenWall 1.14.0 addedGreenWallAPI.SendChat(addon, text, chatType)(API.lua,GreenWallAPI.version == 2). It carries the line as addon messages: a WHISPER to one GreenWall user per co-guild, who re-posts it as a GUILD/OFFICER addon message inside their guild. Classic has no CHANNEL addon messages, and SendAddonMessage has no hardware restriction. NewrelayThroughApiinCompat.luacalls it frombridgeThroughGreenWallwhengw.configexists and the API is version 2+, and returns before the key-press queue. It does not post to the sender's own guild, so the real guild send inaddon.Chat.Sendstays. The result table is read:okwithtruncatedprints "was cut short";no-peersis debug-only, because no co-guild member who could have seen it is online;not-configured,refused,too-long,secretand any unknown reason are printed with the quoted line, like every other lost relay.pcall'd, so a raise prints "GreenWall reported an error" and never takes down the handler that already spoke in guild. Older GreenWalls (no API, or version 1) fall through to the unchanged key-press queue below.Compat.luanow namesaddonNamefrom...for the API's addon-id assert. Specs:Tests/gratz_spec.lua"on GreenWall 1.14.0+ (GreenWallAPI version 2)", 10 examples against a stand-in of the documented contract: fire-time relay with no key press and no CHANNEL send; OFFICER; no age drop; silentno-peers; each refusal reason told; truncation told; a raising SendChat; not calling it beforegw.config; version-1 fallback; party/raid/say not bridged. Full suite: 1037 passed, 0 failed. GreenWall 1.14.0 shipped (GreenWall commit3893010); its shippedAPI.luaandRelay.luawere re-read before release and match the stand-in:version = 2, theSendChat(addon, text, chatType)signature and the five reason strings. Not verified in a client: no whisper-then-guild round trip has been seen in game.
Bug Fixes
Gratz and Smack lines never reached the sister guilds: the v0.8.8 GreenWall relay was dropped by the client every time. I built the v0.8.8 bridge on GreenWall's channel send (
gw.config.channel.<guild|officer>:send), which ends inSendChatMessage(segment, "CHANNEL", nil, number)(GreenWallChannel.lua:366-370). A numbered CHANNEL send is hardware-event restricted indoors and outdoors (warcraft.wiki.ggAPI_SendChatMessage), so an event-driven Gratz or Smack line was lost silently. A typed line works only because GreenWall'sParseTexthook runs inside the Enter key press. Addon messages on a custom channel are no way round it: the wiki says they are disabled on Classic for exactly this reason.Fix:
bridgeThroughGreenWallno longer sends. It queues{msg, chanType, key, t}and shows a hidden relay frame built atCompat.luaload; the frame'sOnKeyDown(flushHeld) sends every held line through GreenWall inside the player's next key press. GreenWall's send is synchronous while connected (Channel.lua:277-382), so the CHANNEL send happens inside that press. The frame callsSetPropagateKeyboardInput(true)beforeEnableKeyboard(true)so every key still reaches the game; propagation is combat-restricted, so a load in combat (a /reload mid-fight) arms the frame onPLAYER_REGEN_ENABLEDand lines wait until then. Lines older than 120 s are dropped rather than sent late; at most 5 are held, oldest dropped. Either drop is printed to the player throughaddon.lib:Print(reportRelayProblem), quoting the line and the reason, because a relay that never arrives looks identical to one that did from the sender's own chat, so a debug-only drop was invisible to the one person who could notice it (peer review, 2026-09-23). GreenWall is resolved again at send time, and each held line goes throughsendIfConnected, which asks the channelis_connected()(GreenWallChannel.lua:125-138, a freshGetChannelNameeach call) before sending. That check is load-bearing: a GreenWall that is loaded but off its bridge channel does not refuse a send, it queues the segment and returns 0 without raising (Channel.lua:325-326, 352-381), and the line would then go out on a later flush outside any key press, where the client drops it. A disconnected channel, missing channel objects, and a GreenWall error caught by thepcallare each printed throughreportRelayProblemwith their own reason. The error notice says the line may not have reached the sister guilds rather than that it was not passed:tl_flushdrains its queue FIFO (Channel.lua:354-376), so a raise on a segment GreenWall queued earlier leaves ours queued, and GreenWall's next send can still deliver it (peer review, 2026-09-23). GreenWall also cuts a long line rather than splitting it: the segmentopcode#guild_id##messagegoes throughstrsub(..., 1, GW_MAX_MESSAGE_LENGTH)(Channel.lua:322; 255 inConstants.lua:79), so the tail is lost with no error. A typed line is cut the same way;greenWallCutsLinenow tells the player when a relayed line was cut short, since the home guild saw it whole. That notice goes through the same printer, so it is namedreportRelayProblemrather than for the not-relayed case alone. Location:Compat.lua.Not verified in a client: that an insecure
OnKeyDowncounts as the hardware event for a CHANNEL send on Era, and that the frame still receives keys in combat. Needs an in-game test.Specs:
Tests/gratz_spec.lua"the GreenWall bridge", rewritten. The v0.8.8 specs passed because their GreenWall stand-in recorded the call and never asked the client whether it would deliver it. The stand-in now makes GreenWall's real CHANNEL send through the harness chat gate, and carriesis_connected()plus GreenWall's queue-without-raising behaviour when disconnected: one example pins that a send with no key press lands inwow.dropped, and the rest pin hold-at-fire, relay-on-key-press, relay-once, officer routing, the non-bridged types, GreenWall absent, no send function, a GreenWall error, 120 s expiry, the 5-line cap, GreenWall gone at press time, GreenWall disconnected at press time (nothing relayed and nothing left in its queue), a GreenWall with nois_connected(still relayed: the feature-detect fallback), a line one character over GreenWall's limit (relayed, and the player told it was cut; one at the limit is not reported), and the in-combat load arming on regen; the expiry, cap, error, gone, disconnected and an Officer-line example also assert the player-facing notice, and the error example asserts it does not claim the line was not passed.newWorldexposes the relay frame (frames[beforeCompat + 1]) andw.press().A guild line sent while GreenWall was loaded but not yet set up was dropped from the relay without a word.
bridgeThroughGreenWallreturnedfalsesilently whenevergreenWallChannel(key)orrelayFramewas nil. GreenWall'sgwglobal exists from itsGlobals.lua:36, butgw.configis only built in itsADDON_LOADEDhandler (GreenWall.lua:294), so a Gratz or Smack line in that window was never held and never reported -- the one silent path left after every other relay failure was made visible. Now silence is kept only for GreenWall not installed (_G.gwnil); otherwise the line goes throughreportRelayProblemand nothing is held. One reason per condition: no relay frame (noCreateFrameat load) is "the key-press relay could not be set up", which is TOGTools' failure and not GreenWall's; "GreenWall was not ready yet" only whengw.configis missing; a config lacking the channel orGW_MTYPE_CHATis a GreenWall whose shape changed, reported as "GreenWall is not available", since waiting would not fix it and "not ready" would repeat on every line. Per the peer-review ruling on threadc54b54b4: fix it, do not measure the window. Location:Compat.lua. Specs:Tests/gratz_spec.lua"tells the player when GreenWall is loaded but not set up yet, and holds nothing", "does not call a set-up GreenWall with a changed shape 'not ready'" and "tells the player when there is no relay frame, without blaming GreenWall" (newWorld({ noRelayFrame = true })hidesCreateFramewhileCompat.lualoads).The relay limits were restated in two test names and nothing tied the CurseForge page to them. Two specs were named "older than two minutes" and "at most five lines" -- a name is compared to nothing, so it would drift from
RELAY_MAX_AGE/RELAY_MAX_HELDsilently (peer review, thread5ca12a4d).Compat.luanow publishes both asaddon.Chat.RELAY_MAX_AGEandaddon.Chat.RELAY_MAX_HELD; the specs are named after the constants and derive their time advance, line count and expected "within N minutes" text from them. A new example, "the CurseForge page states the relay limits the code enforces", readsdocs/Curseforge_Description.htmland asserts its "after two minutes" / "at most five" wording matches the constants, so changing either side fails the suite. This entry restates the values on purpose: a dated record must not move with the code. Also added "still relays a line held exactly RELAY_MAX_AGE", pinning that the age check is strict (>). Coverage ofCompat.lua(harnesscoverage.lua): every relay line is covered; the 47 uncovered lines (67-83, 601-681) are AceGUI widget helpers, which need the client.README.md(which ships) also said "two minutes" and nothing pinned it (peer review, thread6d00c25e). It keeps the figures, because a player reading it from the AddOns folder may not have the page open, and the page example (renamed "the CurseForge page and README state the relay limits the code enforces") now asserts the same wording in both files (peer review, threade3c168f4). README still points at the page's "Sister guilds hear it too" bullet for the notices, and the example asserts that bullet exists. The example also accepts "one minute" for a 60 s limit. The no-relay-frame notice is defensive only -- Ace3'sAceEvent-3.0.luacallsCreateFrameat file scope whenever Ace3 runs at all, so a client without it errors in Ace3 first -- which is now commented at the guard (citing the Ace3 line by its text, not a line number) and is why the page does not list it.
Documentation
- The v0.8.8 entry below says Smack "stays omitted" from
TOGTools_Camelot.toc. That was never true: the same entry's Smack fix lists Smack in both_Mainlineand_Camelot, and the shipped Camelot TOC loads it. Smack runs on WoW Forever. Left in place because released entries are not edited; corrected here.
Older releases (v0.8.8 and earlier) are archived in CHANGELOG_ARCHIVE.md.
This mod has no additional files

