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.14.0

File nameTOGTools-TOGTools-v0.14.0.zip
Uploader
PmptastyPmptasty
Uploaded
Nov 13, 2026
Downloads
46
Size
593.5 KB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
9594199
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.14.0] (2026-11-13) - Quality of Life tab

New Features

  • A new Quality of Life tab, built from the Leatrix Plus wish list players voted on. The operator, 2026-11-12, after posting the numbered Leatrix Plus list to Discord: "lets post the list of leatrix plus in discord, and ask the users which ones they want, and we can start with that"; Vishiswaz voted for 19 (tally in docs/LEATRIX_PARITY.md), and also asked for one tab that grows over time. New engine Modules/QoL/QoL.lua (addon.modules["qol"]) with tab GUI/QoLTab.lua, listed in all seven TOCs after Retrack. Every feature is a registry entry { key, label, desc, available(), apply(on) }. available() feature-detects what the client has, so a flavour sees only the options it can run; apply(false) runs only when the player switches a feature off, never at login, so an untouched feature writes nothing. Every feature starts off (db.global.qol.on). Game settings (CVars) change out of combat only: a write in combat waits for PLAYER_REGEN_ENABLED, and only the latest value is sent. The value a CVar held before the first change is kept in db.global.qol.prior and put back on switch-off, so a value the player had tuned survives; with no kept value the client's default is used (C_CVar.GetCVarDefault / GetCVarDefault), then a fixed fallback. Leatrix Plus was read for which calls and settings each feature needs; nothing was copied. The first features:

    • Automate gossip (wish list #2): with Alt held, a gossip window with exactly one option and no quests picks that option (C_GossipInfo.SelectOption, present in every flavour's tree). Leatrix also auto-picks a hand-kept list of Forever NPCs without Alt; that list was not taken.

    • Resize quest text (#35): one slider, 10-30 (default 12), sets QuestTitleFont (+3), QuestFont (+1) and QuestFontNormalSmall, the three fonts Leatrix sets in both its Era and Forever builds. They are shared font objects, so other windows using them change too; the tab says so. The sizes before the first change are captured and put back on switch-off.

    • Disable drunken haze (#59): CVar ffxGlow to 0, the switch Leatrix uses on Era and Forever. It is the full-screen glow, so that goes too; the description says so.

    • Max camera zoom (#62): cameraDistanceMaxZoomFactor to 4.0 on Classic Era and TBC and 2.6 elsewhere. Leatrix sets 4.0 in its Era and Anniversary builds and 2.6 in its Retail and Forever builds; Wrath, Cata and Mists Classic are not verified and get 2.6.

    • Show train all button (#43): a "Train All" button on Blizzard's trainer window (Blizzard_TrainerUI, load-on-demand, so the button is built on its ADDON_LOADED or at once if it is already loaded) that calls BuyTrainerService for every available service; greyed out with nothing to learn, hidden on a training-points trainer, and its tooltip names the count and cost. GetTrainerServiceInfo puts the service type third on the Classic trees and second on the Mainline tree (Retail, Forever), read per flavour from Blizzard_TrainerUI.lua in each tree; QoL.ServiceType branches on gv.isClassic. On Classic the money display sits left of Train (Blizzard_TrainerUI.xml), so the button goes right of Exit there and left of Train on Retail and Forever.

    • Show ready timer (#47): a countdown bar under each ready popup the client has. The battleground popup (PVPReadyDialog, hooked through PVPReadyDialog_Display; Classic Era has it) counts down from GetBattlefieldPortExpiration(id) plus one second, as Leatrix does (its reason is not verified), and shows nothing when the game reports no time left. The dungeon-finder popup (LFGDungeonReadyPopup, not in Classic Era) has no expiry call, so it counts down from 40 seconds, Leatrix's figure, not verified against the game; LFG_PROPOSAL_SUCCEEDED / _FAILED hide it. The bar redraws once a second, never below zero.

    • Simple addon listing (#75): every flavour's AddonList_Update (Blizzard_AddOnList, loaded at startup, the same code in all five trees) files addons under ## Category headings. With this on, a hooksecurefunc on AddonList_Update replaces the scroll box's tree with one alphabetical list (QoL.SimpleAddonOrder): titles compared without colour codes, each addon keeping its Group (children sorted under their parent; a child whose parent is filtered out or missing listed on its own, as the game does), and the search box still filtering by title or group. The node data is the game's own { addonIndex = i }, so the game's row code draws it unchanged. Switching it off calls AddonList_Update to redraw the game's own list.

    • Minimap size and Hide minimap addon buttons (#36, Enhance minimap, part one). Size: MinimapCluster:SetScale, 50-200% in 5% steps (default 100%, db.global.qol.minimapScale), with the cluster's scale before the first change put back on switch-off. Frame changes go through the new QoL.OutOfCombat(key, fn), which runs at once out of combat and otherwise once at PLAYER_REGEN_ENABLED, latest call per key. Hide buttons: LibDBIcon-1.0's own ShowOnEnter(name, true), which fades a button in only while the mouse is over the minimap (libs/LibDBIcon-1.0.lua:431), for every button in GetButtonList() and, through the LibDBIcon_IconCreated callback, for buttons created later. A button an addon had already set to show on hover is left alone and is not switched back on afterwards. Only LibDBIcon buttons are affected; an addon that draws its own minimap button is not. Square minimap, on the Classic clients: their round frame art is the one MinimapBorder texture (Blizzard_Minimap/Classic/Minimap.xml:216 in the classic_era, classic_anniversary and classic trees), so the option hides it, sets Minimap:SetMaskTexture("Interface\\ChatFrame\\ChatFrameBackground"), adds a 2px black BackdropTemplate border, and replaces the GetMinimapShape global with one answering "SQUARE" (the previous function is kept and put back), all out of combat. The game's own round mask path is in none of the trees, so switching off restores the art, border and shape function and asks for a /reload rather than guessing the mask. LibDBIcon buttons are not moved at once when the shape changes; not verified when they next reposition. Square minimap on Retail and Forever: their round frame is MinimapCompassTexture plus MinimapCompassTextureUnderlay (Mainline/Minimap.xml:111-119; MinimapBorder is in the Classic trees only, so the art is chosen by which exists, and on Classic MinimapCompassTexture is the separate rotation ring and is left alone). Each piece's shown state is kept when it is first hidden and put back on switch-off, so Forever's underlay, hidden unless the map rotates, is not shown by mistake. Forever's MinimapCluster:UpdateMinimapConfig (Camelot/Minimap.lua:31, run on every rotateMinimap change) sets the round mask and art again, so a hooksecurefunc on it squares the map again while the option is on; Retail's is empty (Mainline/Minimap.lua:365). Leatrix's Forever build also re-anchors the map and moves the mail indicator into the corner; neither is done here, not verified how the indicators sit in game. Minimap header on hover, every client: the header pieces are found by parent key on MinimapCluster in every tree (BorderTop, ZoneTextButton, and the Classic-only ToggleButton, Blizzard_Minimap/Classic/Minimap.xml:28-63; on Retail and Forever BorderTop is a child of ZoneTextButton, Mainline/MinimapZoneTextButton.xml:22), set to alpha 0, and brought back to 1 from OnEnter hooks on Minimap and each piece; OnLeave fades them only when the mouse is over none of them, so moving from the map onto the zone name keeps it shown. Alpha, not Hide, so the zone button keeps its tooltip and click. The hooks are set once and do nothing while the option is off; switching off sets alpha 1. Leatrix only hides the zone bar outright. Minimap button bag: every LibDBIcon button (GetButtonList, and later ones through the shared LibDBIcon_IconCreated callback, now one watchNewIcons for both button options) is reparented into one BackdropTemplate panel, hidden until a right-click on the minimap. The minimap's own OnMouseUp (a ping on any button: Minimap_OnClick on Classic, MinimapMixin:OnClick on Retail and Forever) is wrapped, not replaced: only a right-click opens the bag, other clicks go to the game's handler, and switching off puts the game's handler back unless another addon replaced ours since. Opening lays the buttons out sorted by name, one column per four buttons up to ten (QoL.BagColumns, Leatrix's row widths), 30px cells, beside the minimap on the side nearer the screen centre; buttons an addon hid (db.hide) are not shown. It closes two seconds after the mouse leaves both the bag and the minimap (QoL.BagTick, an OnUpdate that runs only while open). Switching off parents each button back to Minimap and calls LibDBIcon's Refresh(name), which re-places it and keeps the addon's own hidden state. It and Hide minimap addon buttons each switch the other off, since a bag of buttons faded to nothing would show nothing. Leatrix's excluded-buttons list is not built.

    • Tooltip size, Hide tooltip health bar and Hide unit tooltips in combat (#37, Enhance tooltip, part one). Size: SetScale on GameTooltip, ItemRefTooltip, ShoppingTooltip1/2 and ItemRefShoppingTooltip1/2 where the client has them (Leatrix's list also covers Retail-only pet battle and garrison tooltips, left out), 50-200% in 5% steps (db.global.qol.tooltipScale), each tooltip's own scale put back on switch-off. Health bar: GameTooltipStatusBar:SetAlpha(0), so the game showing the bar again for the next unit changes nothing and switching off is SetAlpha(1); Leatrix blanks the texture instead. Combat: one TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, ...) (present in every flavour's TooltipDataHandler.lua, Classic Era included) hides GameTooltip while UnitAffectingCombat("player") and the mouse is over the world (WorldFrame:IsMouseMotionFocus(), the check both Leatrix builds use, with GetMouseFocus() == WorldFrame as the fallback); Shift shows it. Unit-frame tooltips are untouched. Show unit's target in tooltip shares that one post-call (registered once, hookUnitTooltip): QoL.TargetLine reads the unit from GameTooltip:GetUnit() and adds a wrapped Target: name line from UnitName(unit .. "target"), skipping a unit token or name that issecretvalue (feature-detected; Classic has none) reports secret, an empty name or no target; a tooltip hidden for combat gets no line. Show guild rank in tooltip and Class-coloured names in tooltip share the same post-call: QoL.RankLine adds Guild rank: <rank> from GetGuildInfo(unit) (in every tree) for a player unit (UnitIsPlayer), since the game's tooltip already names the guild (Leatrix rebuilds the whole guild line instead); QoL.ClassColour colours GameTooltipTextLeft1 from RAID_CLASS_COLORS by UnitClass's class file. Each skips a unit token, player flag, rank or class file that issecretvalue reports secret. The post-call now reads the unit once and calls Show only when it added a line. Tooltip follows the mouse: a hooksecurefunc on GameTooltip_SetDefaultAnchor (SharedTooltipTemplates.lua in every tree) re-owns the tooltip with ANCHOR_CURSOR when the mouse is over the 3D world, Leatrix's "cursor" anchor; tooltips over windows and unit frames keep the game's place. Hooked once, inert while off. Two sliders under it move the tooltip from the mouse, -100 to 100 each (db.global.qol.tipCursorX / tipCursorY, QoL.ClampTipOffset): ANCHOR_CURSOR takes no offsets, so any non-zero offset uses ANCHOR_CURSOR_RIGHT with them, the anchor the game's own code passes offsets with (Blizzard_SharedMapDataProviders/QuestDataProvider.lua:233 in the classic_era tree); both at 0 keeps ANCHOR_CURSOR. Which way a positive Y moves the tooltip is not verified in game. Reaction-coloured tooltip border shares the unit post-call: QoL.ReactionColour reads FACTION_BAR_COLORS[UnitReaction(unit, "player")] (the table the game's own GameTooltip_UnitColor uses, Blizzard_GameTooltip/Classic/GameTooltip.lua:70) and sets it on GameTooltip.NineSlice:SetBorderColor (the tooltip's border, TooltipBackdropTemplateMixin in SharedTooltipTemplates.lua); a reaction issecretvalue reports secret is skipped. A HookScript("OnTooltipCleared") (fired by GameTooltip, Classic/GameTooltipTemplate.xml:147) puts TOOLTIP_DEFAULT_COLOR back the next time the tooltip is cleared, and only when this tinted it, so item and spell tooltips keep the usual border; switching off does the same at once. Leatrix's corner offsets are not built. UnitIsPlayer, RAID_CLASS_COLORS and GameTooltip_SetDefaultAnchor were added to .luacheckrc and .luarc.json.

    • Mute custom sounds (#67): a multi-line box (db.global.qol.muteSounds) for sound file IDs with any notes beside them; QoL.ParseSoundIDs takes every run of digits as an ID, once each (Leatrix strips every non-digit instead, and caps IDs below 20,000,000 for a reason not verified, so no cap here). MuteSoundFile / UnmuteSoundFile (used by Leatrix's Era and Forever builds). A mute lasts only for the session, so the list is muted again at every login; an edit while on unmutes removed IDs and mutes new ones, leaving kept IDs alone. Switching off unmutes every ID on the list, even one another addon had also muted; whether the game counts mutes per addon is not verified. Leatrix's sound finder is not built.

    • Mute game sounds (#65): Leatrix's nine groups (chimes, fizzle, interface, login, ready, trains, vaults, piccolo, yawns), one tick each under the switch (db.global.qol.muteGroups), muted through the same muteSet as the custom list so the two never unmute each other's IDs. The IDs are in Modules/QoL/MuteSoundData.lua, generated by tools/build-mute-sounds.py (desk tool togtools-mute-sounds) from the wowdev community listfile with path patterns of our own; Leatrix's tables were read only to learn which sound each group means, and its file IDs were looked up to find the paths. Trains covers every playable race's /train sound in the listfile (51 files: the Classic races' choochoo files and the later races' train files); Login covers the login screen music and ambience of every expansion (52 files), where Leatrix's Forever build mutes the one Retail title track. Login is a logout group: muted on PLAYER_LOGOUT and unmuted when the addon enables after the next character is picked (IDs kept in db.global.qol.logoutMuted). No addon code runs on the character select screen, so a logout mute also mutes its music; the Login description tells players so (Peer Review 76f0f99d T3). Whether a mute set at PLAYER_LOGOUT holds on the login screen is not verified. The script gained a lookup mode (the path behind a file ID) and now ignores a blank argument, which the desk tool passes when args is empty. Not seen in game.

    • Enhance dressing room (#38), every client: eight UIPanelButtonTemplate buttons down the right of DressUpFrame.DressUpModel (a DressUpModel in all three Classic trees, Blizzard_CharacterFrame/Classic/DressUpFrames.xml:148; Blizzard_CharacterFrame is LoadFirst, so it exists before any addon). Undress calls Undress(); Head, Shoulder, Back, Chest, Shirt, Tabard and Weapons call UndressSlot for slots 1, 3, 15, 5, 4, 19 and 16-18, labelled with the game's HEADSLOT-style strings where it has them. It is one way: the game's Reset calls Dress() (DressUpFrames.xml:143), which dresses the model in your own gear. Retail and Forever dress a model scene instead (DressUpFrame.ModelScene, Mainline/DressUpFrames.lua in the live and forever trees), so there the buttons sit on the scene and act on ModelScene:GetPlayerActor(), read at each click as the game's own code does; the actor has Undress and UndressSlot (FrameAPIModelSceneFrameActorDocumentation in the live tree). The animation buttons are Classic only, since the scene's actor has no HasAnimation. Below them, < and > step the model through its animations (QoL.StepDressAnimation, wrapping at both ends) with a label naming the animation's number. Leatrix steps a hand-picked list of IDs; this asks the model instead, calling HasAnimation for every ID from 0 to QoL.DRESS_ANIM_MAX (1000) once each time the dressing room opens (an OnShow hook on DressUpFrame resets it), then SetAnimation (both on CharacterModelBase in the classic_era API docs). The client gives no animation names, so the label shows the number; whether 1000 reaches a model's highest ID is not verified. Leatrix's item icons and per-slot toggle back are not built.

    • Show druid power bar (#46), Vanilla and TBC clients only: a StatusBar on PlayerFrame (BOTTOMLEFT 98, 20, where Leatrix puts it) showing mana (UnitPower / UnitPowerMax of power type 0) with a cur / max label, coloured from PowerBarColor.MANA, shown while a druid's current power type is not mana and they have mana. PLAYER_ENTERING_WORLD, UNIT_DISPLAYPOWER and UNIT_MAXPOWER (player only) decide whether it shows; while shown an OnUpdate re-reads the value every 0.2 s so regeneration moves it. The Wrath, Cata and Mists clients load Blizzard's own bar (Blizzard_UnitFrame_Classic.toc: Wrath/AlternatePowerBar.xml for wrath and cata, Mists/ for mists), so they are not offered it; Forever and Retail are not covered. Switching off hides it and unregisters the events. Leatrix's status-text option and player-chain border colours are not built.

    • Show range colouring (#48): an action button's icon turns red (1, 0.3, 0.1, Leatrix's colour) while its target is out of range. The game's ActionBarActionButtonMixin:UpdateUsable already tints an icon blue for not enough mana and grey for unusable, and ActionButton_UpdateRangeIndicator only reddens the hotkey (Blizzard_ActionBar/Shared/ActionButton.lua:679-692 and 1273, the same Shared file in every tree). So a hooksecurefunc on ActionButton_UpdateRangeIndicator reddens the icon, a hooksecurefunc on each button's UpdateUsable (every button from ActionBarButtonEventsFrame:ForEachFrame, and later ones through its RegisterFrame) keeps it red when the game recolours it, and coming back into range, or switching off, gives the icon the colour the game's UpdateUsable would (C_ActionBar.IsUsableAction, else IsUsableAction). A range answer issecretvalue reports secret is skipped. Leatrix instead switches off the game's button update loop and redraws the attack flash itself; that is not done here, and the pet bar is not covered.

    • Show info cards (#49): a new /ut card (and /togt card) command, which a player can type or put in a macro bound to any key, reads what GameTooltip is showing and opens a small AceGUI window (QoL.OpenCard in GUI/QoLTab.lua) with the name, the ID and a Wowhead link selected for Ctrl+C. QoL.CardInfo tries GetItem (the ID from the item: link), then GetSpell, then GetUnit (the NPC ID from a Creature or Vehicle GUID); players get no card, and any link, spell ID, unit token or GUID that issecretvalue reports secret is skipped. Wowhead paths (QoL.WowheadBase) are the ones Leatrix Plus's own builds use, read from the installed Era, Anniversary, Forever and Retail copies: /classic/, /tbc/, /forever/ and none. Wrath, Cata and Mists Classic are not offered it because their paths were not checked. Leatrix's own key setting, its quest, achievement and other card types, and its card layout are not built. The command was used rather than a key binding of its own so no key-capture settings were needed; the card now also has a key binding (#76, below).

    • Show Wowhead links (#50): an InputBoxTemplate edit box at the top of QuestLogFrame with the Wowhead link for the selected quest (QoL.QuestURL: GetQuestLogTitle's eighth return, the quest ID, under QoL.WowheadBase), set from a hooksecurefunc on SelectQuestLogEntry, which the quest log's QuestLog_SetSelection calls (Vanilla/QuestLogFrame.lua:314). A header or an entry with no quest ID hides the box. Focus selects the text for Ctrl+C, typing puts the link back, Escape lets go. This is what Leatrix's Classic Era build does; its Forever build puts links on the world map and its "go to comments" setting are not built. Offered only where the client has the quest log calls and a known Wowhead path, so Classic Era and Anniversary.

    • Enhance flight map (#39): TaxiFrame:SetScale 100-300% (default 150%, db.global.qol.taxiScale) and the TaxiButtonN flight point icons 16-32 (default 20, taxiIcon), applied from a TaxiFrame OnShow hook because TaxiFrame_OnShow creates the icons, up to NUM_TAXI_BUTTONS (the same Blizzard_UIPanels_Game TaxiFrame.lua in every tree). The sliders apply at once while the map is open. The scale and each icon's size before the first change are put back on switch-off. Leatrix's Alt-drag to move the map re-anchors a panel the game lays out itself, and is not built.

    • Show flight times (#73): Leatrix ships a hand-collected table of flight times; this learns them from the player's own flights instead, so nothing is copied and no table goes stale. A hooksecurefunc on TakeTaxiNode notes the route (QoL.FlightRouteKey: the TaxiNodePosition of the CURRENT node and of each hop's destination from GetNumRoutes / TaxiGetNodeSlot, two decimals, joined start first, so a flight through a different stop is a different route) and the continent (QoL.FlightContinent: C_Map.GetBestMapForUnit walked up parentMapID to the mapType 2 map, because taxi positions are per continent map). A StatusBar near the top of the screen then waits up to 10 s for UnitOnTaxi("player"), counts the flight down when the route has a saved time (bar filled by the share flown) or up with "(first flight)" when not, and on landing saves the whole seconds in db.global.qol.flightTimes[continent][route]; a flight under 5 s is not saved. A hooksecurefunc on TaxiNodeOnButtonEnter (the Classic and Shared TaxiFrame.lua in the classic_era tree) adds "Flight time: m:ss" or "not flown yet" to a reachable node's tooltip. Offered where the client has all the flight map calls, which includes Retail and Forever. Those clients usually open the newer FlightMapFrame instead (Blizzard_Game/Mainline/EventImplementation.lua:735 in the forever tree), whose pins build their tooltip in FlightMap_FlightPointPinMixin:OnMouseEnter and never call TaxiNodeOnButtonEnter (Peer Review, thread 76f0f99d). So the mixin's OnMouseEnter is also hooked (QoL.OnFlightPinEnter, slot from pin.taxiNodeData.slotIndex), at once if Blizzard_FlightMap is loaded or on its ADDON_LOADED otherwise. The map copies each pin's method into its OnEnter script when the pin is first built (Blizzard_MapCanvas.lua:312), so pins built before the hook (the map opened before the option was switched on) get the line only after a /reload. Not seen in game on either client. The in-flight bar does not depend on the map, since the pins also call TakeTaxiNode. The time saved is the latest flight's, not an average; a /reload mid-flight loses that flight. Switching off stops a flight in progress without saving.

    • Key bindings (#76, Set keybindings): a new Bindings.xml puts three actions in the game's own Key Bindings menu under AddOns > Ultimate Tools, all unbound by default: open or close the window, the info card, and Diagnostics. Each <Binding> calls the new global TOGTools_RunBinding(cmd) (SlashCommands.lua), which runs Ace:SlashHandler with the matching slash command ("", card, diag), so a key and its command cannot do different things. BINDING_HEADER_TOGTOOLS and the BINDING_NAME_ labels are set in the same file. Leatrix binds keys from its own panel with SetOverrideBindingClick on hidden buttons; the game's menu needs no key-capture settings of our own. <Binding> in an addon's Bindings.xml is still read on Retail: Blizzard's own Blizzard_PingUI/Bindings.xml in the live tree uses it. New spec Tests/bindings_spec.lua reads Bindings.xml and checks every binding has a label and runs a command the slash handler carries out. Leatrix's announce-rare and recent-chat actions are not built.

    Tests/guildkey_spec.lua now also drives the guild filter dropdown on AceGUI's real Dropdown from the installed Ace3: it installs the harness's widget layer (env.frames) and reloads AceGUI after it, because the real widget reads its template's named child frames, which the plain frame model does not create (WoWAPITesting's answer on inbox thread 62210fcd). The stand-in block that swapped AceGUI.Create for a fake dropdown is gone: the real-widget block checks everything it did (operator approved the removal, test change 35f196cef027effc).

    The tab is split into sub-tabs (operator, 2026-11-13: "there is too much stuff. we should break it into a few tabs"): General, Minimap, Tooltips, Flights, and Sound & Camera, a nested AceGUI TabGroup as the Logs tab uses. QoL.GROUPS orders them and QoL.GROUP_OF places each feature; QoL.FeatureGroups() returns only the sub-tabs with a feature this client can run, so a client missing every minimap option shows no Minimap sub-tab. The last sub-tab viewed is kept in db.global.qol.subTab. A spec asserts every registered feature (QoL.RegisteredKeys()) names a real sub-tab, so a new feature cannot fall onto General unnoticed.

    Fixed before release: every click on the tab jumped back to the top (operator, in game, 2026-11-13). Ticking a feature with its own settings rebuilt the whole tab to enable or grey them, and the new ScrollFrame started at offset 0. Each sub-tab's ScrollFrame now keeps its offset in a status table that survives the rebuild (SetStatusTable; AceGUI's FixScroll re-applies status.offset after layout), and a tick rebuilds only its own sub-tab. The fix is not seen in game yet.

    New spec Tests/qol_spec.lua. C_GossipInfo, GetCVarDefault, C_CurrencyInfo, GetCoinTextureString and the trainer calls and frames were added to .luacheckrc and .luarc.json. Not yet seen in game on any client. Locations: Modules/QoL/QoL.lua, GUI/QoLTab.lua, TOGTools.lua (defaults), the seven TOCs, Tests/qol_spec.lua.

Improvements

  • CHANGELOG.md archived under the release-body ceiling. At release prep it measured 122,335 characters, over the 120,000 working ceiling (GitHub refuses a release body over 125,000). The oldest section, v0.9.0, moved whole to the top of CHANGELOG_ARCHIVE.md, ahead of v0.8.9; the trailing pointer now says "v0.1.0 to v0.9.0".
  • Gratz recovers achievements whose chat line was hidden in combat (Forever and Retail). The operator, 2026-11-12: "why can't we store it in memory until we're able to send it out?" The hidden line itself cannot be kept and sent: in chat messaging lockdown CHAT_MSG_ACHIEVEMENT / CHAT_MSG_GUILD_ACHIEVEMENT arrive as secret values, which tainted code may only store or pass, and C_ChatInfo.SendChatMessage takes secrets only from untainted code (SecretArguments = "AllowedWhenUntainted", ChatInfoDocumentation.lua in the forever tree). So a skipped secret line now sets _newsMissed, and once combat ends (PLAYER_REGEN_ENABLED), a restriction lifts (ADDON_RESTRICTION_STATE_CHANGED state 0) or the player zones (PLAYER_ENTERING_WORLD), Gratz calls QueryGuildNews(); on GUILD_NEWS_UPDATE it reads GetNumGuildNews / C_GuildInfo.GetGuildNewsInfo (GuildInfoDocumentation.lua:60, no secret flag on the returns) and gratzes each NEWS_PLAYER_ACHIEVEMENT (1) or NEWS_GUILD_ACHIEVEMENT (0) entry it had not listed before, through the same fireAchievement / fireGuildEntityAchievement as the chat path. Entry fields are read as Blizzard's own news UI reads them (Blizzard_FrameXML/GuildUtil.lua:56-108): newsDataID is the achievement ID, whatText the plain name, whoText the member. News carries a date and no time, so the session's first read (requested 3 s after login) is a baseline that fires nothing, and an achievement already gratzed from a readable chat line (_achHandled, keyed name|id or guild|id) is never repeated. While still in combat or lockdown the read leaves everything unread for later, and a news field that is itself secret stops the read and keeps the miss. Only entries the player's own guild news filters let through are listed, so a player who filtered achievements out gets no recovery. Every news call is feature-detected, so Classic clients are unchanged. Forever hides most of its achievement UI (Blizzard_AchievementUI/Camelot), so whether its guild news lists member achievements at all is decided by the server; not verified in game. The party level-up retry also runs on a restriction lift and on PLAYER_ENTERING_WORLD now, not only at combat end, since names inside an instance may stay secret after combat (not verified). Location: Modules/Gratz/Gratz.lua; specs in Tests/gratz_spec.lua ("achievement recovery from guild news").

[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.

Older versions (v0.1.0 to v0.9.0) are in CHANGELOG_ARCHIVE.md.

This mod has no additional files