promotional bannermobile promotional banner

Ultimate Tools

ToGTools is a convenient place to aggregate one off tools that don't make sense to have it's own addon.
Back to Files

TOGTools-v0.13.0

File nameTOGTools-TOGTools-v0.13.0.zip
Uploader
PmptastyPmptasty
Uploaded
Nov 12, 2026
Downloads
37
Size
549.8 KB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
9580972
Type
R
Release
Supported game versions
  • 12.1.0
  • 11.2.7
  • 5.5.4
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.60.1
  • 1.15.9

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 thread b01704dd). Without the stack, UGR could not tell which of its code paths made the call. _record takes a new stack argument, kept from the first sighting the way msg is 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. _ingestRaw passes rec.stack through from !!TOGT v0.3.0 (worked on as v0.2.5, which was never released). The fallback capture takes it itself through a new actionStack(): debugstack(3) in a pcall, nothing stored on a throw, a non-string or a Retail secret value (issecretvalue feature-detected). That is BugGrabber's fallback level for these events (!BugGrabber/BugGrabber.lua:288), and debugstack exists in the Forever tree (Blizzard_ScriptErrors.lua:56). The tab's rowRawText adds 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. Interpret still 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 forbidden Invite() showed the stack down to the calling addon's own code, fastguildinvite/FGI_APICompat.lua:45 inside its GuildInvite at 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: an UNKNOWN() block. Four new specs. debugstack was added to .luacheckrc and .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 _record also keeps locals, under the same first-sighting and fill-on-repeat rule. Stack and locals are each capped at 4000 characters (MAX_DETAIL, was MAX_STACK at 3000). _ingestRaw passes rec.locals through. The fallback capture's new errorDetail() takes both from Lua errors as well as from blocked actions. The level is Blizzard's own GetErrorData arithmetic (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 / |K become plain letters (Blizzard's RemoveKStrings). A defect in the first cut: actionStack wrapped debugstack in pcall, 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:155 line at the top of the operator's in-game stack. errorDetail calls debugstack / debuglocals directly. Explain now shows the message, then the stack, then Locals: and the locals, in BugSack's layout (rowRawText, split from the new rowMessage). The interpreter already stops reading callers at Locals:. 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 |K stripping, and missing, empty or secret answers; the global swaps are put back after each spec. debuglocals, GetCallstackHeight and GetErrorCallstackHeight were added to .luacheckrc and .luarc.json. A repeat by another call path is kept too. The operator compared one forbidden Invite() 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 popup OnAccept. Both were real, and keeping only the first sighting hid the second. _record now keeps every further distinct call path in e.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, by TrimForSave on PLAYER_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 in droppedPaths, 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, and count / last on each of more). The new Diagnostics.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 ..."). TrimForSave reorders 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 through onUpdate, but Explain was filled once, from a snapshot of the row's text. openExplain now takes the entry and keeps it as _explain.entry. _record calls the new Diagnostics.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 (ErrorInterpreter stackAddons stops at "Earlier, from"; new spec), and !!TOGT's earlydata_spec puts 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:155 capture 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.md archived 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 of CHANGELOG_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.TranslateColumn mapped a tab's justify = "RIGHT" onto the library's align, 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. justify is now dropped, so every cell is left-aligned under its header. Tests/rowlist_spec.lua pins it. Location: GUI/RowList.lua.
  • wow-version-replication.ps1 now 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 own Wow.exe next to the older _classic_beta_, which stays in $WowVersions while it is installed. The dry run lists _forever_\Interface\AddOns\TOGTools as 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.AceGUIFrameScripts left a script on a pooled widget whenever that event had no script before. It saved each prior script in saved[evt] and restored by iterating saved, 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 tooltip GUI/UI.lua attaches. 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.MakeColumnHeader deleted, with no caller since the lists moved to LibAceGUIWidgets' RowList; GuildFilterDropdown's entry / order / selection logic split into addon.GUI.GuildFilterChoices; Compat.lua is now at 247/247 lines covered (was 212/259). The dropdown's spec swaps AceGUI.Create for a stand-in, because the harness cannot yet build AceGUI's real Dropdown (it reads UIDropDownMenuTemplate'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 LibGuildRoster OnLevelComm -> Gratz onMemberLevelChanged -> tryFire -> sendToChannels -> queuedOrSend -> addon.Chat.Send (Compat.lua:421) -> C_ChatInfo.SendChatMessage. On Forever / Retail the client refuses addon chat while C_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. New addon.Chat.MessagingLocked() (feature-detected, pcall'd). addon.Chat.Send no 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's queuedOrSend holds the line in the combat queue while locked, flushCombatQueue keeps holding while still locked (combat ending inside a dungeon), and the new ADDON_RESTRICTION_STATE_CHANGED handler 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:104 in shortName <- isSelf <- the UNIT_LEVEL handler. On Retail-engine clients (Retail, Forever; issecretvalue is in the Forever tree) UnitName / UnitLevel and chat payloads can be secret while execution is tainted in combat, and name:match on one throws. Gratz had no secret guard at all, unlike Smack, Whisper Log and Gathering Log. New isSecret (feature-detected issecretvalue, so Classic is unchanged): shortName returns nil and isSelf false for a secret; the UNIT_LEVEL handler, its deferred re-read and refreshPartyTracker skip a secret name or level; the achievement handler returns early on a secret arg1 / arg2, before the senderName == playerGuild compare and arg1: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_ENABLED now also runs refreshPartyTracker, 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 in Tests/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. errorDetail exists twice, in !!TOGT/!TOGT.lua and in TOGTools' fallback capture, because the two addons share no code, and nothing asserted they agree. The new Tests/errordetail_parity_spec.lua loads the real !TOGT.lua from 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 / |K stripping, empty, secret and non-string answers, no debuglocals, no debugstack. 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. addPath kept 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 = 100 extra paths per entry, drops the least-recently-seen extra path when exceeded (dropStalestPath) and counts it in droppedPaths, 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 InjectEntry binary-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, while Logs:PruneEntries was 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. injectIndex now 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 one InjectEntry. 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" and addon.ShortName = "UT" in TOGTools.lua, used by: the ## Title of 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 stays TOGTools); 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 orange UT: instead of TOGTools:: Ace:Print/Printf in TOGTools.lua replace 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 print UT. /ut is registered beside /togt, which keeps working; /ut help lists every command as /ut with one line saying /togt still 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 !!TOGT dependency and the CurseForge project. The minimap button, the AddOns-list icon (new ## IconTexture in all seven TOCs), the Diagnostics error toast and its Titan/LDB entry use the operator's new textures/UT_MMB_Icon.tga (operator, 2026-10-26: "change that to be the image used in game") instead of ToGTools_PH_MMB. The first cut copied TOGPM's square crop, and the operator saw the coin stretched in game ("the icon seems streched?"). The new Tests/icon_spec.lua reads 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's iconCoords and the toast's SetTexCoord are 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 old ToGTools_PH_MMB.tga is no longer referenced by any code. README and the CurseForge page are not changed in this pass. New spec Tests/brand_spec.lua loads the real TOGTools.lua and SlashCommands.lua over the installed Ace3 and pins the prefix, both slash commands and the help text; Tests/toc_spec.lua and Tests/mailbox_spec.lua now 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 long on Classic Era, raised inside TOGBankClassic's Log:Deliver. User-reported 2026-10-26. GuildBankLog:InjectEntry rebuilt 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, inside baseKey. Now: the dedupe set and per-tuple highest numeric ordinal are cached per bucket (injectIndex, weak-keyed, valid only for the same entries table at the same length, and dropped explicitly by ingest, the three one-shot repairs, a prune that removed rows, and ClearAll); 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 (new Logs:RetentionCutoff(), which PruneEntries now uses too, so the two cannot disagree); and the redraw is coalesced to one per batch (C_Timer.After(0), synchronous with no C_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 every MINIMAP_UPDATE_TRACKING. Whether a death owes a restore is decided at PLAYER_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. Otherwise PLAYER_DEAD sets owed, cleared only when the tracking is seen on again or the restore gives up, and only an owed restore is ever armed. PLAYER_ENTERING_WORLD re-arms an owed restore, so a /reload between 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 next MINIMAP_UPDATE_TRACKING arms it (never between PLAYER_DEAD and 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.toc loads only MinimapTracking_Simple on vanilla ([AllowLoadGameType vanilla]; the dropdown is [ExcludeLoadGameType vanilla]). It reads the active tracking with GetTrackingTexture() and names it with GameTooltip:SetTrackingSpell(). I only read the C_Minimap list, 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 and GetTrackingTexture() returns an icon, the name is read from a hidden GameTooltipTemplate tooltip (TOGTools_RetrackTip). It is read from the game rather than mapped from the icon, because Track Beasts and Track Humanoids share one. The memory gets cast = 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 a SecureActionButtonTemplate button (TOGTools_RetrackCaster, RegisterForClicks("AnyUp"): SecureActionButton_OnClick acts on the release for a secure mouse press). Its resting type attribute is nil, so it casts nothing. PreClick sets type = "spell" / spell = <name> only for a click that passes every gate below, and PostClick clears it. Both skip in combat lockdown, so a sheet that cannot be hidden in combat still casts nothing. PLAYER_REGEN_DISABLED takes it down and PLAYER_REGEN_ENABLED puts 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 only type == "spell" entries as the remembered tracking, the split Blizzard's own MinimapTracking_Update makes (Blizzard_Minimap/Classic/MinimapTracking_Dropdown.lua). Every decision is traced through addon:Debug (Settings' debug switch) so the next test shows where a restore stops. A click within 1.5 s of the last SetTracking is 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 after PLAYER_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.) GetTrackingInfo returns the same MinimapScriptTrackingInfo table (name, active, spellID) in the Era, Anniversary, Classic, Forever and live API docs. On PLAYER_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's OnMouseDown calls C_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 with CanPropagateMouseClicks). 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_TRACKING reports 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 in addon.modules: IsModuleEnabled fails open for a key it does not know. Ace:OnEnable starts it directly, and turning the toggle off stands down a pending restore. Tests/retrack_spec.lua covers both paths; Modules/Retrack/Retrack.lua is at 100% line coverage. Tested in game on Classic Era only; not verified in game: whether SetTracking takes 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.


[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 ## Dependencies line, 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 .pkgmeta checker (Tests/wowapi/pkgmeta.lua, run for the first time on this repo). Root causes: (1) the Forever-only embedded copy libs/LibDBIcon-1.0.lua kept LibDBIcon's first line, --@curseforge-project-slug: libdbicon-1-0@; the packager greps every shipped .lua for that marker and it overrides .pkgmeta's required-dependencies entry, 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-dependencies lacked libaceguiwidgets (the slug every other fleet addon uses) and togt (!!TOGT, project 1533953, slug read off its CurseForge page). Both added. The checker now reports no FAIL; it is declared as the writ suite pkgmeta (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, all learned = true. Two causes, both read in wow-ui-source-forever. (1) C_TradeSkillUI.GetFilteredRecipeIDs() answers for every profession the character has; Blizzard's own list keeps a recipe only where C_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 (new ProfCapture.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 forces SetShowUnlearned(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, and GetRecipeSourceText (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 by parentProfessionID when the client sets one, else professionID, because LibProfessionDB files every recipe under the base id and answers nil for any other. Each known record now carries libProfessionID and libRecipes (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 carries sourceChecked = true and sourceTextType (the raw type returned), so a missing source reads 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 and GetRecipeSourceText gave none of them text (ProfessionDB's report). 9 new specs in Tests/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, inbox c934e6aa). 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

  • .pkgmeta has no comments any more (operator's standing rule, 2026-10-03: comments in .pkgmeta can break packaging). Its notes moved to CLAUDE.md under ".pkgmeta". The commented-out curseforge-project-id line went with them; the id (1533830) is recorded there.

  • Test harness pin moved 3d492c7 -> b20754a, with its adoption steps. Tests/gatheringlog_spec.lua no longer installs its own GetSubZoneText; the harness now does, returning wow.subZone (harness inbox 280f62ed). Tests/smack_spec.lua no longer stubs SetOverrideBindingClick / ClearOverrideBindings: the bindings and clear count it asserts now come from the harness's recorded wow.overrideBindings and wow.clearedBindings (harness 61c09b0, inbox 366486c9). 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 asserts wow.blocked and 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.DeriveGameVersion only recognised Forever when WOW_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's ProjectConstants.lua names only 1 and 2). So Forever at Interface 16001 was derived as Vanilla, isRetail was false, and every Forever/Retail branch in the addon was dead there -- ProfCapture took the Classic GetTradeSkillLine path, 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:71 gates on isForever, "only runs on the WoW Forever client"); ProfCapture:ReadTrainer read GetTrainerServiceInfo in 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 on isRetail120Plus (Smack.lua:142-147); and /togt vc tried 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.md archived 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 of CHANGELOG_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 a ScrollingMessageFrame (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 AceGUI MultiLineEditBox whose OnTextChanged (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's EditBox:SetText page 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 /reload starts 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 thread 15b4d7a3), which never shipped.
  • Settings in db.global.eventTrace (enabled, capture, exclude), which !!TOGT reads from TOGTools_DB at PLAYER_LOGIN. EventTrace:SetExclude applies a new list to the live stream at once, and empty text restores !!TOGT's default. SetCapture(true) restarts a stopped stream and SetCapture(false) calls !!TOGT's Stop(). The module is in moduleregistry_spec's GATE_EXEMPT, with the reason: the gate is !!TOGT's. 15 specs in Tests/eventtrace_spec.lua load the REAL !!TOGT files from the sibling folder, not a stand-in; Modules/EventTrace/EventTrace.lua is 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 thread b0734afe) 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.lua loads on every flavour and registered it unguarded at file load, and RegisterEvent throws on an event the client does not have. The registration is now pcall'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 any RegisterEvent as 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-logic addon.ErrorInterpreter (Interpret returns a result table, Explain returns the display text) reads a single path:line: reason line 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 its Interface/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's tainted by 'X' marker first, otherwise the first real addon in the stack (skipping BugGrabber/BugSack). The stack scan stops at Locals:. 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 through GetAddOnMetadata, and says whether it is a Pimptasty addon (author Pimptasty or a TOG* 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 new addon.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 in char.frames.explainDock. An AceGUI Frame moves from its title bar's mouse handlers and never fires the OnDragStop that DockWindow listens to, so the title bar's OnMouseUp calls h: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 new MainWindow.beforeRelease hook calls the library's W:UndockWindow (MINOR 38, requested on thread 2472500eacf8; 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 (thread 4e63f914), 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 to Explain and WantsAddonLoad, which now accept either text or a result. It used to interpret the same text three times. And the "Pimptasty addon" test is EI.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 compact source drops 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 (thread 737274f20c2f) and built there at MINOR 38 (library v0.2.8). TOGTools now copies through a single helper, addon.UI.ShowCopy(text, prompt, parent), which calls ShowCopyBox when the installed library has it and otherwise builds the same box from the library's existing ShowDialog (one button, pre-filled, selected). So nothing in TOGTools has to change when the library ships it. The hand-rolled StaticPopupDialogs["TOGTOOLS_DIAG_COPY"] is gone; the Diagnostics row Copy uses the helper. parent is 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 longer addon.logCategories), with tabKey/configKey/label/WINDOW_SIZE in GUI/DiagnosticsTab.lua, the usual automatic Settings > Modules toggle backed by diagnostics.enabled (default on), and showWhenDev so it stays visible under Developer Tools with the toggle off. The gate (Diagnostics.IsWanted, which is also recordingEnabled) is IsModuleEnabled("diagnostics") OR Developer Tools. Switching it on runs Diagnostics:Wake() (broker, fallback capture, draining the companion's early buffer), from the module toggle's OnModuleToggled and from the Developer Tools setting, replacing the startup code the Settings file used to carry. The toast, the Titan broker and /togt diag open the main tab. /togt diag no longer insists on Developer Tools: it refused players even after the tab was surfaced, which I missed when I changed the gate. /togt logs diagnostics still gets there, and /togt diag is 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) and Tests/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 new deps rule) 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 thread 513d5779bb63), and addon.UI.CopyField creates it when AceGUI: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 (thread 63629c29099f) and fixed as Version 2. On an older library, UI.CopyField draws the same box itself: the library's COPY_ICON with 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. retentionDays is only a default (TOGTools.lua), and GUI/Settings.lua has no control for it, so both now say "older than 90 days".
    • The Gratz achievement triggers were described as Retail-only. Modules/Gratz/Gratz.lua registers 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, via Version.lua isRetail120Plus).
    • Bloodlust was listed as working on Classic Era. Modules/Smack/Smack.lua says 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.lua picks 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).

  • 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 as GetCombatRatingBonus(CR_HIT_*) + Get*HitModifier() (wow-ui-source-forever Blizzard_UIPanels_Game/Camelot/PaperDollFrameStats.lua:489-496). The engine now asks NO_HIT_STAT = gv.isRetail and not gv.isForever everywhere it used to ask isRetail, 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 is SecretWhenUnitStatsRestricted (PlayerScriptDocumentation.lua), so each read goes through stat(): 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"). New nickMatchesName matches 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:GetCoverage answers "uncovered" for, not every non-"shipped" id, because "unknown" spans the whole range. Each quest is requested with C_QuestLog.RequestLoadQuestByID and read on QUEST_DATA_LOAD_RESULT (both confirmed in wow-ui-source-forever QuestLogDocumentation.lua:1219, :1336). The walk is paced by replies, not a ticker: at most 10 requests outstanding, and each reply sends the next. A _filling flag guards re-entry, because the event is declared SynchronousEvent. 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 as db.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 (QuestDB added to TOGTools_Camelot.toc OptionalDeps), the questDB defaults in TOGTools.lua, and C_QuestLog in .luacheckrc / .luarc.json. Spec: Tests/questdb_spec.lua; ungated exemption recorded in Tests/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 and Release clears only the released frame's own points, so a group another frame still anchors onto can come back from AceGUI:Create. I did not find the stray anchor in TOGTools' own code (every SetPoint onto a widget frame in GUI/ was read), so which addon leaves it is not verified. New UI.AnchorPath(from, target) walks a region's points breadth-first and returns the chain, and UI.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.GetRecipeSourceText for a recipe shown in the open profession window, and only for an unlearned one (wow-ui-source-forever Blizzard_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. retailKnown asks 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.GenerateCraftingDataProvider forces "Show unlearned" back off every time it rebuilds the list (wow-ui-source-forever Blizzard_ProfessionsTemplates/Camelot/Blizzard_Professions.lua:17-20), each change re-lists and fires TRADE_SKILL_LIST_UPDATE, and my handler turned it on again, so the two fought without end. OpenTradeSkillFilters, RestoreTradeSkillFilters, TradeSkillFiltersOn and the TRADE_SKILL_CLOSE registration are gone; the list the window shows is read as it is.
    • CaptureKnown merges a Retail read into the stored profession, so a narrower list (learned only) keeps the recipes an earlier read listed instead of overwriting them.
    • WalkRecipeSources also 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 sourceType from C_TradeSkillUI.GetRecipeInfo, labelled with BATTLE_PET_SOURCE_<n>, the same numbers and labels Blizzard's own Sources filter uses (Blizzard_Professions.lua:1255-1272). The operator's SavedVariables show GetRecipeInfo answered for all 2,496 unlearned recipes with no window open, while GetRecipeSourceText answered none. The walk was already calling it and threw that field away. retailKnown keeps sourceType too. 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 GetRecipeInfo key: no sourceType or anything like it, and GetRecipeSourceText returned nil. The forever tree's TradeSkillUIDocumentation.lua has no other source call, only the window's source-type filter, which would mean writing the window's filters (see above). The walk now keeps skillLineAbilityID per 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 as walk.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.

  • 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_ID is now 320,000, and raiseTopID lifts a stored topID below it on every Start and FillGaps, 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. FillGaps raises 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.ps1 could delete a live file from every replica install. Peer review 259f99e3 (TOGProfessionMaster): a save-by-replace raises Deleted and Created/Changed for one save, in either order, and when Deleted ran last Sync-File removed 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 /who sightings, sister roster copies and grouped UNIT_LEVEL as OnSisterMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source). Gratz routes it through fireGuildLevelUp, 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's Faction- 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; nil means 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's OnHomeMemberLevelChanged (same signature), which is free: every library user's own PLAYER_LEVEL_UP announced on the guild addon channel ('comm'), plus UNIT_LEVEL on 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, the GUILD_ROSTER_UPDATE registration and the login ticker. Baseline, online-at-both-readings and cross-source dedupe are now the library's rules (its Tests/sisterlevel_spec.lua); the Gratz specs that pinned them for the old scan went with it. Contracts 2f198123 and 9c178c96.
  • 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.ps1 now replicates into WoW Forever. _classic_beta_ (Battle.net product wow_classic_beta, which reads TOGTools_Camelot.toc) added to the version list, and a destination is kept when the version folder exists rather than its Interface\AddOns folder: 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 new Tests/toc_spec.lua case 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. requestQuery arms a 1.5 s fallback ingest for Retail, where GUILD_EVENT_LOG_UPDATE no 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.lua counts ingest passes on both paths; the Classic spec was run red with the cancel line removed (1 failed) and green with it restored.
  • /togt glcheck wrote to the saved Guild Log. GuildLog:Diagnose's instrumented ingest walk tinserted every new buffer row into the bucket, with no id, 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. sendToChannels now 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 thread 0f319d11. Location: Modules/Gratz/Gratz.lua (_dingSent, tryFire's dingKey). 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, the C_Timer.After fallback and the sub-tab refresh. Gratz.lua 404/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 holds RequestLevelRefresh, keeps member.level current for every member, and fires OnHomeMemberLevelChanged with 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 from OnEnable, OnModuleToggled, SaveProfile and DeleteProfile (the tab's form and "On" checkbox both go through SaveProfile). 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 /who cadence), 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 through GreenWallAPI.SendMessage (version 2) and fires OnSisterMemberLevelChanged source 'comm' on receipt (contract 58cbd95c), 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 reads Dep 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:GetData reads each addon's ## Dependencies through C_AddOns.GetAddOnDependencies (the call Blizzard's own AddOn list tooltip uses, Blizzard_AddOnList/AddonList.lua:849 in the Era tree) and resolves each one: C_AddOns.DoesAddOnExist false -> "Not installed"; IsAddOnLoaded -> "Loaded"; otherwise the client's reason from GetAddOnInfo(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 .toc file, so the tooltip reads it back through the client: title, folder name, ## Notes, status, ## Version and ## 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's onRowEnter/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. GetAddOnInfo on a dependency name is pcall'd: whether it raises or returns for an uninstalled name is not verified on every flavour, so a raise, or a MISSING reason, reads as "Not installed".
  • Sort priority keys on the bare label, so Dep Missing: LibX still sorts with the failures.
  • Tests/addonload_spec.lua covers the module at 149/149 lines. Eleven dependency specs, plus three for paths no spec had run before: the module's own ADDON_LOADED/PLAYER_LOGIN timing capture used when !TOGT is absent, and RefreshMemory. 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.0 to ## Dependencies and stop loading libs\LibGraph-2.0.lua, which is deleted. .pkgmeta adds libgraph-2-0-revived to required-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 .pkgmeta line 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 debugstack for the directory its .lua sits in, and expects the .tga files beside it (LibGraph-2.0/LibGraph-2.0/*.tga in the standalone addon). TOGTools shipped the .lua flat in libs\ 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.lua reads the library through LibStub("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.0 to ## Dependencies, drop the ## OptionalDeps: LibDataBroker-1.1, LibDBIcon-1.0 line and stop loading libs\LibDataBroker-1.1.lua / libs\LibDBIcon-1.0.lua; .pkgmeta adds libdbicon-1-0 to required-dependencies. The standalone addon (Funkeh, v12.0.3) loads LibStub, CallbackHandler-1.0 and LibDataBroker-1.1 from its own embeds.xml, so one dependency covers both libraries GUI/MinimapButton.lua asks 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.toc still embeds both files, because the standalone's TOC lists no 16001 and 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) sets diagnostics.libraryFiles and diagnostics.ignoredFiles to Disable, so vendored code in libs\ stops filling the Problems panel whenever one of its files is open in a tab. TOGTools' workspace.library also points at the sibling ../LibGraph-2.0/LibGraph-2.0 folder so LogGraph.lua keeps its type information.
  • GUI/MinimapButton.lua: the unused _self argument of the LDB OnClick is now _.
  • Tests/toc_spec.lua: INTENTIONAL_OMISSIONS lists libs\LibDataBroker-1.1.lua and libs\LibDBIcon-1.0.lua for every flavour except Forever, so the cross-flavour parity check pins the Forever-only embed instead of failing on it.
  • Docs: README.md and docs/Curseforge_Description.html list 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 new Profession Capture tab (Developer Tools only), visits trainers and recipe vendors and opens their own profession windows. The SavedVariables go back to ProfessionDB. New Modules/ProfCapture/ProfCapture.lua and GUI/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")), and profCapture.capture, which defaults off. enabled stays on by default because MainWindow hides a disabled module's tab, and that would hide the switch.

    • Two API shapes, by gameVersion.isRetail. Read from the Blizzard trees under F:\Blizzard API Docs. On the Retail client, including WoW Forever, GetTrainerServiceInfo returns name, type, texture, reqLevel, subText, category (wow-ui-source-forever Blizzard_TrainerUI/Mainline/Blizzard_TrainerUI.lua:425), and the spell id comes from C_TooltipInfo.GetTrainerService(i).id. On every Classic client it returns name, subText, type, isExpanded with header rows. That order is the same in the classic_era, classic_anniversary and classic (Mists) trees. The spell id comes from a hidden GameTooltip's SetTrainerService + GetSpell. Vendors use C_MerchantFrame.GetItemInfo or GetMerchantItemInfo. Recipe lists use C_TradeSkillUI (GetFilteredRecipeIDs, GetRecipeInfo, GetRecipeSchematic) or GetTradeSkillInfo / GetCraftInfo.
    • The trainer filter. On show, every service filter (available, unavailable, used) is turned on and the player's own is saved; TRAINER_CLOSED puts 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 after TRAINER_SHOW, the next TRAINER_UPDATE turns 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 SetTrainerServiceTypeFilter still applies after TRAINER_CLOSED; what an enchant's GetCraftItemLink carries on Classic Era (Era's craft frame calls only GetCraftItemLink, never GetCraftRecipeLink -- 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 in unkeyed; Era's trade-skill frame does call GetTradeSkillRecipeLink, Blizzard_TradeSkillUI.xml:11); and that GetFilteredRecipeIDs returns every row while the player's profession-window filters are set. It honours those filters, so a filtered list is a partial list.
  • 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 asks C_TradeSkillUI.GetRecipeSourceText(id): the text the Professions window shows under an unlearned recipe (Blizzard_ProfessionsRecipeSchematicForm.lua:632-638 in 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, as source on an unlearned recipe and as nextRecipeID + nextRankSource on a learned one. ProfessionDB is an OptionalDep of the Camelot and Mainline TOCs so it loads first. No Classic client has this call. The writ blizzard-api index finds it only in the forever and live trees, and the classic_era TradeSkillUIDocumentation.lua has 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's answered count 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 carries build ("<version>.<build>" from GetBuildInfo, e.g. 1.60.1.69977), locale and ts (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 type belongs to the character who opened the trainer. On Classic a service whose tooltip yields no spell id (Classic Era's GetSpell on a trainer-service tooltip is not verified) is still recorded, without spellID: a raising SetTrainerService or GetSpell is 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.lua 376/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 thread c571dee6):

    • the header sort/filter menu, with cascading filters, filterGroup and a cap;
    • SetSearch, autoFit and hyperlinks in cells;
    • Detach for AceGUI's recycled parents;
    • col.align, col.gapBefore and col.sortDescDefault.

    GUI/RowList.lua went 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) becomes format(value, entry);
    • sortKey becomes sortValue;
    • a RIGHT/CENTER justify becomes align;
    • headerTipDesc becomes the library's headerTip;
    • tiebreakKey becomes a tiebreak(a, b) that checks name, then the key, then falls through to SetData order.

    addon.UI.OpenColumnMenu in GUI/UI.lua is removed, because nothing else called it. LibAceGUIWidgets is a hard dependency in all seven TOCs (Tests/toc_spec.lua pins 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() checks LibStub.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 with onRowClick could open its row menu and the chat-name menu together (Peer Review, audit thread cf6cc085). The six lists whose cells render item or player links pass hyperlinks = true: Gathering, Guild Bank, Mail, Trade, Whisper and Item DB. Gratz, Smack, Diagnostics, Addon Load and Guild Log do not. The Gathering Log's sortType = "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.lua is 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 library tostrings 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 (thread af2ebd61); GUI/RowList.lua 56/56 lines covered. Not verified in a client: nothing has been drawn in game on LibAceGUIWidgets MINOR 34 yet.

  • 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.lua pins it across all seven).

  • Test harness pin moved to WoWAPITesting 3d492c7 (from 7932484, ten days of Adoption-log entries). Tests/itemdb_spec.lua drops its own strsplit stand-in, because the harness's strsplit now keeps empty fields and honours the piece limit as the client does. The spec-local IsInGroup / UnitInParty / UnitHealth / CombatLogGetCurrentEventInfo assignments 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 (was 110207, 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 lists 120100 and never listed 120008; LibAceGUIWidgets found 120008 in no TOC but TOGTools' own and no 12.0.8 patch anywhere, with 12.1 (120100) the current Retail patch (inbox thread c571dee6). Tests/toc_spec.lua still pins one 11.x floor and exactly one 12.x build. Not verified: this box does not record the Retail client's build, so 120100 is 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 added GreenWallAPI.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. New relayThroughApi in Compat.lua calls it from bridgeThroughGreenWall when gw.config exists 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 in addon.Chat.Send stays. The result table is read: ok with truncated prints "was cut short"; no-peers is debug-only, because no co-guild member who could have seen it is online; not-configured, refused, too-long, secret and 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.lua now names addonName from ... 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; silent no-peers; each refusal reason told; truncation told; a raising SendChat; not calling it before gw.config; version-1 fallback; party/raid/say not bridged. Full suite: 1037 passed, 0 failed. GreenWall 1.14.0 shipped (GreenWall commit 3893010); its shipped API.lua and Relay.lua were re-read before release and match the stand-in: version = 2, the SendChat(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 in SendChatMessage(segment, "CHANNEL", nil, number) (GreenWall Channel.lua:366-370). A numbered CHANNEL send is hardware-event restricted indoors and outdoors (warcraft.wiki.gg API_SendChatMessage), so an event-driven Gratz or Smack line was lost silently. A typed line works only because GreenWall's ParseText hook 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: bridgeThroughGreenWall no longer sends. It queues {msg, chanType, key, t} and shows a hidden relay frame built at Compat.lua load; the frame's OnKeyDown (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 calls SetPropagateKeyboardInput(true) before EnableKeyboard(true) so every key still reaches the game; propagation is combat-restricted, so a load in combat (a /reload mid-fight) arms the frame on PLAYER_REGEN_ENABLED and 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 through addon.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 through sendIfConnected, which asks the channel is_connected() (GreenWall Channel.lua:125-138, a fresh GetChannelName each 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 the pcall are each printed through reportRelayProblem with their own reason. The error notice says the line may not have reached the sister guilds rather than that it was not passed: tl_flush drains 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 segment opcode#guild_id##message goes through strsub(..., 1, GW_MAX_MESSAGE_LENGTH) (Channel.lua:322; 255 in Constants.lua:79), so the tail is lost with no error. A typed line is cut the same way; greenWallCutsLine now 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 named reportRelayProblem rather than for the not-relayed case alone. Location: Compat.lua.

    Not verified in a client: that an insecure OnKeyDown counts 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 carries is_connected() plus GreenWall's queue-without-raising behaviour when disconnected: one example pins that a send with no key press lands in wow.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 no is_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. newWorld exposes the relay frame (frames[beforeCompat + 1]) and w.press().

  • A guild line sent while GreenWall was loaded but not yet set up was dropped from the relay without a word. bridgeThroughGreenWall returned false silently whenever greenWallChannel(key) or relayFrame was nil. GreenWall's gw global exists from its Globals.lua:36, but gw.config is only built in its ADDON_LOADED handler (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.gw nil); otherwise the line goes through reportRelayProblem and nothing is held. One reason per condition: no relay frame (no CreateFrame at 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 when gw.config is missing; a config lacking the channel or GW_MTYPE_CHAT is 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 thread c54b54b4: 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 }) hides CreateFrame while Compat.lua loads).

  • 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_HELD silently (peer review, thread 5ca12a4d). Compat.lua now publishes both as addon.Chat.RELAY_MAX_AGE and addon.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", reads docs/Curseforge_Description.html and 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 of Compat.lua (harness coverage.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, thread 6d00c25e). 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, thread e3c168f4). 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's AceEvent-3.0.lua calls CreateFrame at 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 _Mainline and _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.