File Details
TOGProfessionMaster-v0.8.0
- R
- May 31, 2026
- 829.81 KB
- 20
- 5.5.3+4
- Classic + 2
File Name
TOGProfessionMaster-TOGProfessionMaster-v0.8.0.zip
Supported Versions
- 5.5.3
- 4.4.2
- 3.4.5
- 2.5.5
- 1.15.8
TOG Profession Master Changelog
[v0.8.0] (2026-05-30) — Crafting tab, cost-to-craft, and the LibProfessionDB data library
Heads-up: this release moves the recipe database into the new standalone LibProfessionDB library and depends on it. It cannot load until LibProfessionDB is installed (it's a required dependency in the TOC /
.pkgmeta), so it must not ship before that library is published.
New Features
Crafting tab. A full fourth tab modeled on TradeSkillMaster's crafting screen but in TOGPM's own style (AceGUI widgets, brand colour, shared tooltip handler). It replaces the native profession window when you open a profession — with a toggle button to drop back to the Blizzard UI — pre-populates a dropdown from your known professions, and lists every recipe with a difficulty-coloured name and an orange→yellow→green→grey skill-tier column. Local single-character crafting; a virtual-scroll raw-frame row pool keeps it instant on large recipe sets. Location: GUI/CraftingTab.lua, Modules/Crafting/CraftingEngine.lua.
Craft queue. Queue recipes and craft them top-down, with click-drag to reorder so you can stack-rank what gets made first. Completion-tracked (watches
UNIT_SPELLCAST_SUCCEEDED) and persisted as a proper per-character table. Location: Modules/Crafting/CraftQueue.lua.Cost-to-craft. A per-recipe Crafting Cost total (on the recipe-name row, above the Missing Materials label) plus a per-reagent Cost column, summed by a unified price provider. Sources, in priority order: the Auction House (TOGPM's own scan, or Auctionator when you opt in) then a shipped vendor-price table. Markers:
*= a reagent has no price yet (total is a lower bound),~= a contributing price is stale,—= nothing priced. Location: Modules/Price.lua, GUI/CraftingTab.lua.Built-in Auction House scan — no Auctionator required. Opening the Auction House auto-fires a one-pass full scan (legacy
getAllon Era/TBC/Wrath,C_AuctionHouse.ReplicateItemson Cata/MoP) that builds TOGPM's own price DB — no button to click. Modeled on Auctionator's FullScan: a dedicated scan frame silences every otherAUCTION_ITEM_LIST_UPDATElistener during the getAll so the Blizzard AH UI can't corrupt the result set; lowest per-unit buyout per item, batched across frames. Honours the server's ~once/15-min getAll throttle, and the scan is cached for the whole session so re-opening the AH inside the cooldown reuses it. Location: Modules/AHScanner.lua.Auto-populated
[AH]buttons. Because the full scan knows every listed item, the per-row[AH]buttons across the Professions / Crafting / Missing / Cooldowns / Shopping List tabs now light up straight from it — no per-recipe "Scan AH" click. Location: Modules/AHScanner.luaGetListingsFor.Optional Auctionator integration (off by default). A "Use Auctionator pricing" toggle under Settings → Auction House. Off by default so the addon uses its own scanned + vendor prices first; tick it to prefer Auctionator's price database when installed. Auctionator is an
OptionalDeps, never required. Location: GUI/Settings.lua, Modules/Price.lua.Shipped vendor-price table. A generated Data/VendorPrices.lua gives thread, dyes, vials, flux and other vendor-bought reagents a cost out of the box — gated to genuinely vendor-sold items (emulator
npc_vendor, unlimited-stock + gold-only, so drop/farmed mats are never mis-priced) and priced from wago ItemSparse.
Changes
Recipe data now comes from the standalone LibProfessionDB-1.0 library. TOGPM no longer bundles its own all-version-merged
Data/Recipes/*.lua; those are removed and replaced by a small bridge (Data/RecipeDB.lua) that pointsaddon.recipeDBat the library's point-in-time recipe set for the running game version + locale. Because the library data is already version-scoped, the per-client expansion gates in BrowserTab / MissingRecipesTab (minExpansion, thespellId > 25000heuristic, the skill cap) now step aside for it — which also fixes the class of bug where legitimate high-ID Classic recipes were wrongly hidden. ProfessionDB is declared in## Dependenciesand.pkgmeta required-dependencies.Enriched, searchable effect text. Enchanting recipes carry their effect (
+5 Weapon Damage,+1 All Stats, …). The Crafting and Professions tab searches now match recipe name OR effect (so5 damage,agility,miningall find recipes), and both tabs' tooltips show the effect. Search boxes lost the non-functional AceGUI "okay" button and got clearer tooltips. Location: GUI/CraftingTab.lua, GUI/BrowserTab.lua.
Localization
Crafting tab is fully localized. Every crafting-tab string is a locale key (no hardcoded text), defined in
Locale/enUS.luaand present in all 15 shipped locale files so translators can localize in place — English fallback until then. The per-reagent Cost column also right-aligns cleanly (coin strings no longer wrap to a second line). Location: Locale/, GUI/CraftingTab.lua.Dutch (
nlNL) native-speaker review. Reviewed by a Dutch speaker: game-mechanic terms (profession / reagent / Auction House / cooldown names, etc.) kept in English, "watch" → "in de gaten houden", "characters" → "personages". Location: Locale/nlNL.lua.
[v0.7.6] (2026-05-29) — GetTradeSkillLine signature bug — every Classic scan was writing maxRank as skillRank
Bug Fixes
- "Can learn now" filter on Missing Recipes was comparing recipes' required-skill against the character's MAX skill instead of their current rank. Reported in-game: a 267/300 Vanilla Blacksmith named Genguin with the filter on still saw 116 missing recipes including every 290 / 295 / 300 Plan (Volcanic Hammer, Thorium Leggings, Imperial Plate Chest, Runic Plate Shoulders, etc.) — recipes that were clearly above his actual rank. Root cause: Scanner.lua
ScanTradeSkillIntoread the trade-skill window header aslocal skillName, _, skillRank, skillMax = GetTradeSkillLine()— a 4-return signature withtexturediscarded in position 2. That's the modern WoW (pre-C_TradeSkillUI) signature, but on every Classic version (Vanilla / TBC / Wrath / Cata / MoP)GetTradeSkillLine()returns just(name, rank, maxRank)— 3 values, no texture. Confirmed against AllTheThings (src/UI/Windows/Tradeskills.lua:167) and MissingTradeSkillsList (ui/event_handler.lua:168), both of which run on every Classic expansion and use the 3-return signature. So our read was misaligned by one position: the_discarded the actual current rank,skillRankpicked upmaxRankinstead, andskillMaxcame back nil and got defaulted to 300 insideMergeRecipesIntoGdb. Net effect for every Classic trade-skill scan since the addon was written:gdb.skills[charKey][profId].skillRank = the character's MAX skill,skillMax = 300regardless of cap. Nothing read these fields until v0.7.4's "Can learn now" filter, so the bug stayed invisible for the entire pre-v0.7.4 history. Then the filter compared each recipe'srequiredSkillagainst askillRankthat was silently pinned to the player's max forever, and every recipe up to that max passed through. Fix: changed the read tolocal skillName, skillRank, skillMax = GetTradeSkillLine(). Existing affected players need to open each trade-skill window once post-update to overwrite their stalegdb.skillsentries with the correctly-read rank — same self-healing model as the v0.7.5ScanCraftSkillIntofix for Vanilla Enchanters. Location: Scanner.luaScanTradeSkillInto.
[v0.7.5] (2026-05-29) — "Can learn now" follow-ups + Browser cross-version gate + RecipeMaster crash sidestep + Alchemy data tidy
Bug Fixes
"Can learn now" filter on Missing Recipes resurrected every recipe the character already knew. Reported in-game: with the filter OFF a Vanilla Leatherworking char showed 33 missing recipes; flipping the filter ON jumped that to 155, because every recipe the char already knew (and the unfiltered list was correctly hiding) suddenly reappeared. Root cause was a structural bug in v0.7.4's first cut: the
canLearnOnlybranch was wired as anotherelseifin the existing skip-rule chain insideBuildMissingList, sitting BEFORE theelseif knownByChar(spellId)/elseif knownByChar(data.teaches)/elseif knownByChar(data.craftedItemId)checks.elseifshort-circuits the rest of the chain once any branch's condition is true — so as soon ascanLearnOnlywas on, the chain landed in MY branch, setskipbased purely on the skill-rank comparison, and never ran the known-by-char checks at all. Net effect: "known" recipes stopped being filtered out the moment the toggle came on. Fix: pulled the filter out of theelseifchain entirely. It now runs as a separateif not skip and canLearnOnly then ... endguard AFTER the elseif chain finishes — so all the existing skip rules (minExpansion, requiredSkill cap, season, knownByChar variants, rank-book caps, unknown-spell gate) get a chance to mark a row skipped first. The "Can learn now" filter only ever tightens the visible set; it never relaxes earlier rules. Location: GUI/MissingRecipesTab.luaBuildMissingList.Vanilla Enchanters saw an empty "Can learn now" list — every Enchanting recipe got hidden. Scanner.lua
ScanCraftSkillIntois the legacy Craft-API scan path Vanilla uses for Enchanting (and a couple of similar trade-skill-like Crafts) instead of the modern TradeSkill API. v0.7.4 and earlier only read the SKILL NAME fromGetCraftDisplaySkillLine()and hardcodedskillRank = 0, skillMax = 300when callingMergeRecipesIntoGdb. The actual API returns(name, rank, maxRank)— the 2nd and 3rd return values were just being discarded. v0.7.4's "Can learn now" filter then compared every recipe'srequiredSkillagainst the persistedskillRank = 0, and since virtually every recipe hasrequiredSkill > 0, every Enchanting recipe got hidden when the filter was on. Fix: read all three return values fromGetCraftDisplaySkillLineand pass the real rank/max through. Existing Vanilla Enchanters need to open their Enchanting window once post-update to overwrite the stalegdb.skills[charKey][333].skillRank = 0; the next scan re-populates correctly. Location: Scanner.luaScanCraftSkillInto."Can learn now" still let high-skill Blacksmithing / Tailoring / etc. recipes through for low-rank chars. Coverage gap in the shipped recipeDB: ~13% of Blacksmithing entries, ~16% of Tailoring entries, smaller fractions across other professions ship without an explicit
requiredSkillfield — because the upstream sources (recipe scroll'sRequiredSkillRank, trainer SQL'sReqSkillRank) didn't supply a value. v0.7.4's filter only checkeddata.requiredSkill, treated nil as "unknown → keep visible" per the original spec, and let those high-tier recipes through; a 267 Blacksmith with the filter on still saw 300-skill recipes in their list. Fix: whenrequiredSkillis nil, fall back todata.difficulty[1]— the orange threshold, which IS the lowest skill rank the recipe can actually be cast at. Better proxy than "unknown" for the recipes our DB has incompleterequiredSkillcoverage for. Recipes with NEITHER field still pass (true unknowns — same intentional permissive behaviour as v0.7.4). Location: GUI/MissingRecipesTab.luaBuildMissingListgate resolution."Show all recipes" on the Browser tab leaked cross-expansion content on Vanilla / TBC clients. The shipped
addon.recipeDBis a universal union of every recipe across Vanilla / TBC / Wrath / Cata / MoP (built from wago.tools' MoP build, which inherits every earlier expansion's SkillLineAbility entries), so iterating it unfiltered on a Vanilla client surfaced Wrath / Cata / MoP recipes — both via the "Show all recipes" toolbar checkbox AND via guild view when a peer in a different-version guild had broadcast their data. The MissingRecipesTab already applied a per-client gate (minExpansion+ spell-ID-greater-than-25000 defensive rule +requiredSkill > clientMaxSkillcap + season flag), but BrowserTab had no such gate. Fix: pulled the rule set into apassesClientGate(profId, recipeId)helper insideBuildRecipeListand applied it to every view path —guild,mine,missing, and theshowAlltoggle alike. Browser now agrees with MissingRecipesTab on what's reachable on the current client. Location: GUI/BrowserTab.luaBuildRecipeList.RecipeMaster's tooltip hook caused a 100+ error storm every time you opened Missing Recipes. RecipeMaster's
TooltipHandler.lua:165and:176registerOnTooltipSetItemandOnTooltipSetSpellhandlers on_G.GameTooltipviaGameTooltip:HookScript. On Vanilla theircachedRecipestable is empty, sogetRecipeInforeturns nil for every recipe-scroll tooltip;getAllCharactersRecipeStatusthen hands the nil intoisSkillLearnedByCharacterwhich nil-indexesrecipe.teachesatRecipeHandler.lua:43. Every mouseover threw. v0.6.1 tried wrapping ourSetItemByIDcall inpcall— script-handler errors are dispatched by WoW outside the caller's stack, so pcall never saw them. v0.7.5's first attempt swappedgeterrorhandler()for a no-op during the call — BugGrabber (BugSack's capture lib) hooks at a deeper level thanseterrorhandlerreaches, so it still logged them. Final fix: lazy-create a privateTOGPMMissingRecipeTipGameTooltip frame inheriting from"GameTooltipTemplate"(the same virtual template_G.GameTooltipitself is built from — see Blizzard_GameTooltip/Mainline/GameTooltip.xml line 4) and route every Missing-Recipes-row tooltip through that instance instead of the global. Same template = full Blizzard appearance, fullSetItemByID/SetSpellByID/SetTextAPI. Different frame instance = RecipeMaster'sHookScripton the global never fires. Zero errors, full rich tooltip preserved. Other tabs continue using the global GameTooltip — they don't surface "Recipe:" / "Pattern:" prefixed items so RecipeMaster'sisItemARecipegate never matches and the broken path never runs. Location: newMissingRecipesTab:GetCustomTip()lazy-init + everyGameTooltip:call in the row-OnEnter handler swapped fortip:. GUI/MissingRecipesTab.lua.
Data
Recipe: Elixir of Tongues (spell 2336) and Recipe: Cowardly Flight Potion (spell 6619) removed from the Alchemy recipeDB. Player-confirmed not obtainable in current Classic Era / Anniversary. Were showing as missing on every alchemist with no source data because the underlying recipe scrolls (item 2556 / 5641) genuinely don't drop / spawn. Cleaner to drop them than show with "Unknown" source forever. Location: Data/Recipes/Alchemy.lua.
Source categories added for Recipe: Restorative Potion (spell 11452) and Recipe: Greater Holy Protection Potion (spell 17579). Both were showing "Unknown" in the Missing Recipes Sources column. Restorative Potion tagged as
quest(rewarded by Uldaman Reagent Run / Badlands Reagent Run / Badlands Reagent Run II). Greater Holy Protection Potion tagged asdrop. UI only reads category presence, never the specific NPC/quest IDs, so the inner ID tables are intentionally empty — backfill specific IDs later if a tooltip ever surfaces them. Location: Data/Sources/Alchemy.lua.
[v0.7.4] (2026-05-29) — "Can learn now" filter on the Missing Recipes tab
New Features
- "Can learn now" checkbox on the Missing Recipes toolbar. Player request. Hides recipes the selected character isn't skilled enough to train yet — strict comparison: only shows rows where the character's current skill rank (from
gdb.skills[charKey][profId].skillRank) is at or above the recipe'srequiredSkill. Recipes with unknown skill requirement (where neither the recipe scroll'sRequiredSkillRanknor the trainer SQL'sReqSkillRanksupplied a value —requiredSkillis nil) stay visible regardless of the filter, so officers scanning gaps don't miss anything we can't classify. Sits next to the existing "Include trainer-only" checkbox in the toolbar; off by default; state survives tab switches but resets on UI reload (same pattern as_includeTrainer). Location: GUI/MissingRecipesTab.lua — newcanLearnOnlyparameter onBuildMissingList, new state fieldMissingRecipesTab._canLearnOnly, new locale keysMissingCanLearnOnly/MissingCanLearnOnlyDescin Locale/enUS.lua. Other locales fall back to English via AceLocale writeproxy until translations land.
[v0.7.3] (2026-05-28) — Salt Shaker banker exclusion actually works now
Bug Fixes
- Salt Shaker banker exclusion shipped broken in v0.7.2. The v0.7.2 fix added
addon.Bank.IsBanker(charKey)inCompat.luathat rolled its own check: walkTOG:GetBanks(), compare each entry againstcharKey:match("^([^-]+)")(the short name without the realm suffix). On connected-realm guilds — which is to say, virtually all live Classic Era / TBC / Wrath realms —TOG:GetBanks()returns rostermember.namevalues which are"Name-Realm"because cross-realm clusters report the realm suffix on every roster entry. My short-name comparison stripped the realm before comparing, so the LHS was always"Name"while the RHS was always"Name-Realm"— no entry ever matched, every banker fell through as non-banker, and the Salt Shaker filter never fired. Fix: delegate to TOGBankClassic's own canonicalTOG:IsBank(name)method (inTOGBankClassic/Modules/Guild.lua) instead of rolling our own loop.TOG:IsBankaccepts any name format, normalizes via itsNormalizeNamehelper, and does an O(1)memberRosterlookup against the live banker set — bypassing all of the format-mismatch hazards. Location: Compat.luaaddon.Bank.IsBanker. The caller in GUI/CooldownsTab.lua is unchanged — same signature, just a working implementation behind it.
[v0.7.2] (2026-05-28) — Bogus cooldown entries (mage talents, portals) evicted + transmute popup correctness + banker exclusion
Bug Fixes
Non-profession spells (e.g. Impact rank 5 / spell 12360, Portal: Undercity) rendered as cooldown rows under alchemist / other profession characters. Root cause was a missing whitelist on two paths: (1)
GUI/CooldownsTab.lua'sBuildRowswalked every spell ID ingdb.cooldowns[charKey]and fell through toGetSpellInfo/GetItemInfofor the row label, so any spell ID happily rendered as a "cooldown" — including Mage talents and class spells with no profession meaning. (2)Scanner:OnGuildDataReceived'scooldown:leaf handler accepted any spell ID from a peer broadcast with no validation against the addon's known cooldown set. Stale entries from old v0.6.x code paths or buggy peer broadcasts therefore stuck around forever, displaying nonsense rows on every reload. Fix: two-part. (a)BuildRowsnow guards the single-spell branch withdata.cooldowns[spellId] or spellId == data.saltShakerItem— anything not inData/CooldownIds.lua's whitelist gets silently skipped. Same defensive-display pattern asIsVisibleCrafter. (b) A one-shotaddon:RemoveBogusCooldownssweep runs at everyOnInitializeafter migration — walksgdb.cooldowns, strips any spell ID not in the whitelist (data.cooldowns/data.transmutes/groupBySpell/saltShakerItem), and stops the entries from re-broadcasting forward. Idempotent. Existing affected players see their cooldown DB cleaned on the next/reload; the display guard catches any new ones that arrive via future inbound payloads.Transmute popup showed transmutes the alchemist doesn't know. The cooldown-derived emit branch in
GUI/CooldownsTab.luaaccepted any spell ID intg.spellIdswhose liveGetSpellInfoname matched"Transmute"— even when the alchemist'sgdb.recipes[171]cross-reference returned no hit. Stale cooldown records (transmutes they had on CD before unlearning, or pre-v0.7.0 leftovers, or peer-broadcast pollution under wrong charKey) therefore surfaced as phantom popup rows for transmutes the char no longer knows. Fix: the cooldown-derived branch now requiresrecipeBySpellId[sid]to resolve (i.e., the spell ID is one the char actually owns per the recipe DB cross-reference). The "Transmute"-name fallback is removed — anything not backed by current recipe knowledge is silently filtered.Transmute popup showed different per-row timers for spells that share one cooldown. In Classic Era / TBC / Wrath, all alchemy transmutes share a single ~20-hour cooldown — but the game only records the CD under the spell ID the alchemist actually cast. The popup's per-row time column used
charCds[spellId]to look up the time per entry, so the cast spell showed (e.g.) "5h 17m" while every other transmute in the same popup showed "Ready" — visually inconsistent and misleading (the others aren't actually castable). Fix: transmute-group popup rows now use the group's sharedrow.expiresAt(the max future expiry across every transmute spell the char knows) for every entry. Non-transmute group popups (Mooncloth tier, etc.) still resolve per-spell — those CDs are genuinely independent. Location:GUI/CooldownsTab.luaShowGroupPopup.TOGBankClassic banker alts surfaced spurious Salt Shaker cooldown rows. A bank toon with no cooking skill had a stale Salt Shaker (item 15846) CD record in
gdb.cooldowns— most likely from a pre-repurposing scan, or peer-broadcast pollution under the wrong charKey, or pre-v0.7.0 data that survived migration. The CD was display-only noise (bankers can't cast Salt Shaker anyway), but it cluttered the Cooldowns tab with a row that always read "Ready" and could never actually go anywhere. Fix: addedaddon.Bank.IsBanker(charKey)helper inCompat.luathat compares the charKey's short-name againstTOGBankClassic_Guild:GetBanks(). The Salt Shaker branch ofBuildRowsinGUI/CooldownsTab.luanow skips emit when the row's charKey is a banker. No-op when TOGBank isn't loaded. NOTE: this first-pass implementation was broken on connected-realm guilds — see v0.7.3 for the fix.
[v0.7.1] (2026-05-28) — Vanilla / Classic Hardcore scan-key bug + minimap position persistence
Bug Fixes
Vanilla / Classic Hardcore recipes scanned but didn't display. On TBC and later,
GetTradeSkillRecipeLinkreturns|Henchant:SPELLID|h…for every profession, soScanner:ExtractTradeSkillIdreturns the spell ID — which is the same keyaddon.recipeDBis built around, and the cross-reference at display time works. On Vanilla (and 1.15.x Classic Hardcore which inherits the same APIs), the link format is|Hitem:ITEMID|h…for everything except Enchanting, soExtractTradeSkillIdreturns the crafted item ID instead. The scanner then wrotegdb.recipes[197][2996] = { crafters = … }(Bolt of Linen Cloth's item ID) while BrowserTab / DumpRecipe / every consumer looked upgdb.recipes[197][2963](the spell ID). Same recipe, two different keys, lookup misses, UI shows nothing. A character could scan 60 Tailoring recipes and see none of them. Enchanting always worked because it usesenchant:links on Vanilla too; BS only "worked" on TBC for the same reason. Fix: a reverse lookupaddon:GetSpellIdForCraftedItem(profId, craftedItemId)(built lazily per profession fromaddon.recipeDB[profId].craftedItemId).MergeRecipesIntoGdbandMergeCraftersIntoGdbnow resolve item IDs to spell IDs before storing, so future scans + peer broadcasts land on the right key. Plus a one-shot recovery passaddon:RemapItemKeysToSpellIdsruns at every OnInitialize — idempotent, walksgdb.recipes, and moves any item-ID-keyed entry whose value matches acraftedItemIdin the shipped DB onto the corresponding spell-ID slot (unioning crafter sets when both keys carry data). Existing affected users get their data recovered on the next/reload. Location: TOGProfessionMaster.lua (helper + recovery), Scanner.lua (MergeRecipesIntoGdb+MergeCraftersIntoGdb).Minimap button always reset to its default angle (220°) on every
/reload. GUI/MinimapButton.lua'sSetupMinimapButtonwas passing a FRESH local table (minimapData = { hide = ..., minimapPos = ... }) toLibDBIcon:Registeron each load. LibDBIcon does write the new angle back into the table when the user drags the button — but the throwaway local goes out of scope at function end and the updatedminimapPosnever reachesAce.db.profile. On the next reload, SetupMinimapButton reads the original default value and re-pins the button there. Fix: give LibDBIcon a sub-table that lives directly on the AceDB profile (Ace.db.profile.minimap). LibDBIcon's writes now land in a persisted location, so the button stays where you put it across reloads, character switches, and full client restarts. The legacyprofile.minimapPosfield stays in place as a one-time seed for existing users so nobody loses their last-set position on the first v0.7.1 launch.
[v0.7.0] (2026-05-28) — Flat universal recipe DB + libguildroster visibility gate + slim sync protocol (backwards-breaking)
This is a major architectural rework. The SV schema flattens from per-guild buckets to a single universal recipe table, the sync protocol drops the recipe-metadata leaf entirely (metadata lives in the shipped addon.recipeDB now), and crafter visibility is gated against libguildroster so departed members get swept out automatically. Wire-protocol incompatible with v0.6.x and earlier — the DeltaSync namespace bumps from TOGPmv2 to TOGPmv3, so old clients and new clients silently don't sync with each other during the transition window.
Architectural rework
Flat universal recipe table.
TOGPM_GuildDB.global.guilds[guildKey]is gone. Recipes live in a singlegdb.recipes[profId][recipeId] = { crafters = { [charKey] = guildTag } }table — one row per recipe, every crafter (across every guild) tagged inline with the guild they were sync'd via. Per-character data (cooldowns,skills,specializations,factions,accountChars,altGroups,syncTimes) moves to the top level (charKey is globally unique, no guild scope needed). The recipe-metadata fields (name, icon, reagents, itemLink, recipeLink, isSpell, spellId) that used to be stored per recipe are no longer in the SV at all — they're looked up from the shippedaddon.recipeDBat render time via the newaddon:GetRecipeName / GetRecipeIcon / GetRecipeReagents / GetRecipeCraftedItemIdhelpers. Location: TOGProfessionMaster.lua (newGUILD_DB_DEFAULTS, recipe-meta accessors).Guild registry with FNV-1a tags. Each guild gets registered in
gdb.guildRegistry[tag] = { name, faction, key }wheretagis a 6-hex-char FNV-1a-32 hash of the guildKey ("Faction-GuildName"). Deterministic — every client computes the same tag for the same guild, so a crafter sync'd from Alice's client and Bob's client gets the same tag locally. The reserved tag"personal"covers guildless own alts (only visible to the owning player). Location: TOGProfessionMaster.lua (fnv1aHash6,GetGuildTagFor,GetCurrentGuildTag).libguildroster visibility gate at every display site. New
addon:IsVisibleCrafter(charKey, crafterTag)rule:- Own alts (tracked via
accountChars) are ALWAYS visible regardless of guild. - Tag mismatch with the player's current guild → hidden AND queued in
gdb.pendingPurgefor the timed sweep. - Tag matches but charKey isn't in
GuildCache:IsInGuild→ hidden + queued, unless they're an alt of someone in the roster (bank alts of in-guild mains stay visible viaIsAltOfInRosterCharacter). Covers the "left the guild" + "switched guilds" cases symmetrically: when the user is guildless or in a new guild, every old-guild crafter fails rule 2 and gets queued for purge. Location: TOGProfessionMaster.lua.
- Own alts (tracked via
Timed purge sweep on
OnRosterReady + 60s.GuildCache:RegisterCallback("OnRosterReady", ...)schedulesaddon:RunPendingPurge()60 seconds after the initial guild roster scan completes. The 60-second buffer covers stragglerGUILD_ROSTER_UPDATEevents on large rosters (>500 members) where the roster trickles in over multiple ticks. The sweep walksgdb.pendingPurgeand for each charKey strips every reference acrossgdb.recipes(crafter sets),cooldowns,skills,specializations,factions,syncTimes, andaltGroups(own entry + sibling references). EmptyguildRegistryentries get dropped as their last crafter is removed. Net effect: the DB stays trimmed to active guild members forever, no long-term bloat. Location: TOGProfessionMaster.lua (RunPendingPurge), OnEnable wires the callback.Slim sync protocol. DeltaSync namespace bumps from
TOGPmv2toTOGPmv3. Therecipemeta:<profId>leaf is REMOVED entirely (every byte of name/icon/reagents/links was redundant since receivers already have it inaddon.recipeDB). Thecrafters:<profId>leaf shrinks to bare{[recipeId] = {[charKey] = true}}— guild tags are derived locally at receive time from the receiver's own current guild context (inbound data always arrives through the receiver's guild channel, so all crafters in the payload share the receiver's tag). Per-recipe payload drops by ~90%; hash-leaf negotiation gets much faster. Cooldowns leaf is unchanged. Location: Scanner.lua (BuildLeafPayload,OnGuildDataReceived,MergeCraftersIntoGdb).Receiver hides unknown recipeIds silently. If a sender's addon DB knows a recipe the receiver's (older or different-version) DB doesn't ship, the receive path skips it — no "Unknown #N" placeholder, no SV pollution. The right answer is "ship an addon update" rather than "sync metadata at runtime."
New Features
Dutch (nlNL) locale added. Same override-only mechanism as Thai and Filipino —
nlNLisn't a WoW-recognized locale code (Dutch-speaking players play on enGB or enUS clients), so AceLocale never auto-selects it. Dutch users opt in via the Settings "UI Language Override" dropdown which now lists "Nederlands" alongside the existing 14 languages. Best-effort translation; native-speaker review welcome. Location: Locale/nlNL.lua, GUI/Settings.lua."Show all recipes" toggle on the Browser tab toolbar. Default off (today's behavior — only recipes someone in the guild knows appear). When on, every recipe in the shipped
addon.recipeDBappears in the list with no-crafter rows rendered greyed out, so officers can scan the gaps at a glance."Show Missing" entry in the View dropdown. Surfaces ONLY recipes nobody in the guild knows (across the selected profession or all professions). The dropdown entry is hidden until "Show all recipes" is on, so the two controls operate independently — toggle "Show all recipes" to see what's missing inline, switch View to "Show Missing" for a focused gap list. Location: GUI/BrowserTab.lua (new toolbar checkbox + View dropdown entry,
BuildRecipeListaccepts{ showAll = bool }opts + newmissingview-mode).
Migration
v0.6.x → v0.7.0 is one-shot at first OnInitialize.
MigrateGuildDb()runs once whengdb.schemaVersionis missing or < 7. Cooldown timers are preserved (merged out of every oldgdb.guilds[guildKey].cooldownstable into the new flatgdb.cooldowns) — those are time-sensitive and re-scanning loses the active expiry. Everything else is wiped: recipes / skills / specializations / hashes / lastScan all rebuild organically on the next trade-skill scan and DeltaSync exchange (cheap now that we're not syncing metadata). The oldgdb.guildstree is dropped.accountCharsandsyncLogsurvive in place (already top-level). There is no rollback — going back to v0.6.x on a v0.7.0-migrated SV would see an empty DB.Bugs caught during pre-ship testing:
- UI Language Override was completely broken.
ApplyLocaleOverridereadself.db.profile.uiLanguageOverride, butselfisaddonand the AceDB instance lives on the AceAddon object (addon.lib), so the read was alwaysnil. The function fell through to"auto"and never applied the override — so picking Spanish, French, anything in Settings did nothing. Fix: read fromaddon.lib.dbcorrectly. This bug pre-dated v0.7.0 (introduced in v0.6.2 when the override was added) and was just never noticed until a user tested with a non-default locale during v0.7.0 pre-ship. addon.PROF_NAMESfroze English strings at module load. The table was populated with[171] = L["ProfAlchemy"], ...at file-load time, beforeApplyLocaleOverridehad a chance to mutate the AceLocale table. So even with the override fixed, profession dropdowns + tooltips would still show English. Fix:PROF_NAMESis now rebuilt viaaddon:RebuildLocalizedTables()(driven by aPROF_LOCALE_KEYSlookup), andApplyLocaleOverridecalls the rebuild after mutating the locale table.TAB_DEFSinGUI/MainWindow.luahad the same module-load-time L capture problem. Tab labels stayed English even when the AceLocale table had been mutated to Spanish. Fix: converted to agetTabDefs()function that readsL["..."]at tab-creation time.schemaVersionwas wrongly in defaults. AceDB applies defaults BEFOREOnInitializeruns, so on the first v0.7.0 launch the defaultschemaVersion = 7got written into the SV beforeMigrateGuildDbcould run — the migration's early-return check then tripped, the cooldown-merge walk never executed, and active cooldown timers appeared wiped (the data was still ingdb.guilds[X].cooldownsbut stranded). Fix: removedschemaVersionfrom defaults so it's set ONLY at the end of a successful migration. Migration trigger also widened to re-run whengdb.guildsstill exists, so SVs that hit the first-launch bug recover their cooldown data on the next reload after patch.- Thai entry in the UI Language Override dropdown rendered as boxes. WoW's default fonts only ship glyphs for Blizzard's officially supported locales (Latin / Cyrillic / Han / Hangul) — Thai script falls outside that set, so the native-script label
ไทยcouldn't render. Cyrillic (Русский), Korean (한국어), and both Chinese variants all work because those scripts have native WoW font support. Fix: dropdown label changed to the Latin-scriptThai. The Thai UI strings themselves are unaffected — selectingThaistill applies thethTHlocale; only the dropdown LABEL changed. accountChars/altClaimscollision. The flat schema collapsed two formerly-separateaccountCharssemantics — the local-only boolean flag set ({[charKey] = true}forIsMyCharacter) and the per-broadcaster sync'd alt-group array ({[broadcasterKey] = [...]}) — into one table. When the same charKey was both an own char AND a broadcaster, the array stomped the boolean for its own slot but other charKeys stayed astrue, causing a runtime crash inBuildLeafPayloadwhen broadcastingaccountchars:<charKey>for those entries (#groupon a boolean). Fix: split the two semantics into separate fields —gdb.accountChars[charKey] = true(local flag, unchanged) and the newgdb.altClaims[broadcasterKey] = [...](sync'd array). All read/write sites (OnPlayerEnteringWorld,BuildLeafPayload,OnGuildDataReceived,RebuildAltGroups,HashManager.ComputeAccountCharsHash,HashManager.HasContent,HashManager.RebuildOnFirstLoad) updated to usealtClaimsfor the array semantics. The DeltaSync leaf key (accountchars:) is unchanged on the wire — only the internal field name moved.
- UI Language Override was completely broken.
ForEachGuildBucket+FindBucketForCharcompatibility shims — both old helpers now operate over the single global table, so any GUI consumer that hadn't been migrated yet keeps working without code changes. The walk-every-bucket model collapses to a single virtual "bucket" that exposes the same field names (recipes,skills,cooldowns, etc.).
Cleanup
- Dead-code removal.
recipemeta:<profId>hash + leaf handlers removed from HashManager.MergeRecipeMetaIntoGdb,BackfillBogusRecipeNames,BackfillReagentItemIds,ScrubObsoleteRecipeNames,isObsoleteItemName,isBogusName,cleanRecipeName,GetReagentScraper, and the namespace-collision/obsolete-name guards in Scanner are now dead — left in place but unreferenced (will be stripped in a follow-up patch once we're confident the new path is stable)./togpm backfillis now a no-op that just prints a notice ("metadata lives in addon.recipeDB").
Multi-version safety
The new schema and visibility gate are entirely additive on every supported client (Vanilla / TBC / Wrath / Cata / MoP). The shipped addon.recipeDB already loads per-game-version via the TOC includes, so each client sees the right recipe metadata at render time. The libguildroster gate falls back to "visible" if GuildCache:IsInGuild is unavailable (defensive: better to over-display than to hide a real crafter).
[v0.6.3] (2026-05-28) - Profession-spec bonus-output indicator on the Cooldowns tab (TBC + Wrath) + every supported WoW locale + Thai/Filipino via UI Language Override
New Features
8 new locale files + UI Language Override. Locale coverage now spans every WoW-supported locale (
enUS,enGB,deDE,esES,esMX,frFR,itIT,ptBR,ruRU,koKR,zhCN,zhTW) plus two community-contributed locales that Blizzard's client doesn't natively support but TOGPM ships translations for (thTHThai,filPHFilipino). The new UI Language Override dropdown in Settings (defaultAuto) lets any player force the TOGPM UI into any of the 14 languages regardless of their WoW client's actual locale — so a German player on an enUS Anniversary client can run the addon UI in German, and a Thai-speaking player on any client can run it in Thai. In-game item / spell / recipe / NPC names still come from Blizzard's APIs and render in the WoW client's actual language since those aren't shipped by this addon — only TOGPM's own UI strings get overridden. Dropdown labels show each language's name in its native script (한국어,简体中文,ไทย, etc.) so they're recognizable regardless of the current UI language.- How it works: new Locale/_init.lua defines
addon.NewLocale(code)which writes everyL["key"] = "value"to BOTH AceLocale's standard table (for the auto-detect path) AND a paralleladdon.Locales[code]store (for the override path). AtOnInitialize,addon:ApplyLocaleOverridemutates AceLocale's L table in place — so every existinglocal L = LibStub("AceLocale-3.0"):GetLocale(...)reference picks up the override without code changes elsewhere. A/reloadhint prints on change since some captured strings may already be formatted into widgets. Existing 5 locale files (enUS / deDE / esES / esMX / frFR / ruRU) were migrated to the new helper — header-line swap only, body ofL[...] = "..."assignments untouched. - Bug fix bundled in: v0.6.2's esES → esMX mirror block was broken (it lived inside esES.lua AFTER an
if not L then return endguard, so on an esMX client the file exited at line 7 before reaching the mirror — net effect: esMX clients fell back to enUS, not Spanish, despite us claiming Spanish support for esMX). v0.6.3 ships a dedicatedLocale/esMX.luawith identical content to esES, so esMX clients now get Spanish at addon load just like esES clients do. Divergence can be introduced later if any LATAM phrasing differs. - Translation quality: all 8 new locales (and the 2 community ones) are best-effort following the German-locale precedent — official Blizzard glossary for the 15 profession names per language, best-effort everywhere else. Native-speaker review welcome on every one.
- Multi-version safety: all 5 TOCs (
TOGProfessionMaster.toc,_TBC,_Wrath,_Cata,_Mists) updated to load the full 14-locale set + the_init.luabootstrap. The override mechanism's runtime cost is one table mutation at OnInitialize (and again on each Settings change) — zero per-frame overhead, zero impact on the auto-detect path.
- How it works: new Locale/_init.lua defines
Spec-bonus indicator on the Cooldowns tab. Small spec icon (12×12, sourced live from
GetSpellTextureso no new art assets) renders to the left of the crafter name on cooldown rows where the crafter's profession spec gives bonus output on that specific cooldown. Hovering the icon shows a tooltip with the spec name (auto-localized viaGetSpellInfo) and whether the bonus is guaranteed 2x output (tailoring cloth specs) or a chance to proc extra output (Transmutation Master). The detection works from any single TBC/Wrath alt running the addon —IsSpellKnownwritesgdb.specializations[charKey][profId] = specSpellIdand the value piggybacks the existingcrafters:DeltaSync leaf for guild-wide propagation (same opportunistic-merge model as skill ranks, no extra hash leaves). Coverage:- Transmutation Master (Alchemy spec 28683) → indicator on every TBC + Wrath transmute row, including the collapsed transmute-group row.
- Mooncloth Tailoring (spec 26797) → indicator on Primal Mooncloth (spell 26751).
- Shadoweave Tailoring (spec 26801) → indicator on Shadowcloth (spell 36686).
- Spellfire Tailoring (spec 26802) → indicator on Spellcloth (spell 31373).
- Intentionally out-of-scope: Elixir Master and Potion Master procs apply to elixirs/flasks/potions, none of which are shared-cooldown crafts (no row on the Cooldowns tab to indicate on). Engineering Gnomish/Goblin specs gate exclusive recipes rather than producing bonus output. The TBC cloth-spec proc was changed to a flat 2x at patch 2.1 — listed as "guaranteed" in the tooltip rather than "chance".
- Multi-version safety: the indicator render is gated on
addon.isTBC or addon.isWrathonly. Vanilla never had the proc system; Cata 4.0.1 removed it. On non-applicable clients the icon slot isn't reserved and column widths are unchanged (additive). Detection runs on all versions (writes emptyspecs = {}on Vanilla/Cata/MoP since noIsSpellKnownhits) so the addon never errors there. Location: Data/CooldownIds.lua (newSPEC_BONUSESmap exposed viaaddon:GetCooldownData().specBonuses), Scanner.lua (newScanner:DetectSpecializations+SPEC_SPELLScatalog, wired into login deferred scan andOnTradeSkillEvent;specializationspayload added to thecrafters:leaf inBuildLeafPayloadand parallel merge logic inOnGuildDataReceived), GUI/CooldownsTab.lua (newgetSpecBonushelper + 14px reserved icon column inDrawRow).
Code Reuse
- Re-introduced the spec detection infrastructure that v0.6.0 ripped out. v0.6.0's cleanup deliberately preserved the
gdb.specializationsAceDB schema entry and the purge logic in GUI/Settings.lua as "zero-cost plumbing for re-introducing spec-aware filtering as a proper feature later" — that bet paid off this patch. The newSPEC_SPELLScatalog mirrors the structure of the previous one but trims out the speculative dead code (no profession filter side, no SPEC_SPELLS-as-filter-input wiring); just detection → store → render.
[v0.6.2] (2026-05-27) - Three new EU locales: Spanish (esES), French (frFR), Russian (ruRU)
New Features
- Three new EU locale files shipped. Extends v0.6.0's AceLocale-3.0 infrastructure with
esES(Spanish — Spain),frFR(French), andruRU(Russian). Each file mirrors the 265-key structure of Locale/enUS.lua so any missing key falls back to English automatically. Profession display names use the official Blizzard glossary for each language (Alchemy → Alquimia / Alchimie / Алхимия, Tailoring → Sastrería / Couture / Портняжное дело, etc., all 15 professions). TheesESfile also mirrors its translations into theesMX(Latin-American Spanish) locale slot so esMX clients pick them up directly instead of falling all the way back to enUS — same Blizzard glossary, no need for a parallel file. Translations for the rest of the UI are best-effort following the German precedent (ship + iterate from player feedback); native-speaker review welcome. Location: Locale/esES.lua, Locale/frFR.lua, Locale/ruRU.lua.
Improvements
- All four non-English locales now load on every supported expansion. v0.6.0 wired German only into the main (
TOGProfessionMaster.toc) Classic Era TOC — the four variant TOCs (_TBC.toc,_Wrath.toc,_Cata.toc,_Mists.toc) still only includedLocale\enUS.lua, meaning a German player on TBC Anniversary or Wrath got the English UI even thoughLocale/deDE.luawas sitting right there in the addon folder. Backfilled theLocale\deDE.luainclude in all four variant TOCs alongside the three new EU locale entries; all five TOCs now ship the same 5-locale load list (enUS,deDE,esES,frFR,ruRU).
[v0.6.1] (2026-05-26) - Defensive pcall around SetItemByID + German profession-name validation + manual TBC phase-override mechanism
Data Quality
- Manual TBC content-phase override mechanism (tools/manual_phase_overrides.json) — corrects recipes the ATT-derived phase map (v0.5.4) misclassified. ATT sometimes derives a recipe's phase from an
awppatch tag or reputation gate that reflects when the recipe's DATA was last touched rather than when it became obtainable, so world-drop formulas available since TBC launch can wrongly end up gated behind a later phase. The override wins over the ATT value inbuild_authoritative_data.py'semit_recipe_file; aphase=1override strips the phase field entirely so the recipe is always visible. First entries (user-reported by a TBC player): Enchant Boots - Cat's Swiftness (spell 34007) and Enchant Boots - Boar's Speed (spell 34008) — both BoE world-drop formulas available since TBC launch (Phase 1) but ATT had tagged them Phase 4, hiding them from the Missing Recipes tab on Phase 2. Now always-visible. Future phase misclassifications get the same one-line-JSON-entry treatment (report → add → regenerate) rather than re-deriving the whole ATT dataset.
Improvements
- All 15 German profession names now native-speaker validated. v0.6.0 shipped with 9 names provided by the German contact and 6 my-best-guess standard WoW-DE terms (Herbalism, Skinning, Jewelcrafting, Inscription, Fishing, Smelting). Native-speaker review confirms all 6 guesses were correct as-shipped (Kräuterkunde, Kürschnerei, Juwelenschleifen, Inschriftenkunde, Angeln, Verhütten) — and corrected one of the original 9: Tailoring changes from
SchneiderkunsttoSchneiderei(the noun form for the profession). Updated Locale/deDE.luaL["ProfTailoring"]and theFilterProfessionDescexample string accordingly.
Bug Fixes
- 88x error storm in BugSack when opening Missing Recipes tabs against broken peer addons. User report after v0.6.0 ship: opening the Leatherworking Missing Recipes tab generated 88 RecipeMaster errors (
RecipeHandler.lua:43: attempt to index local 'recipe' (a nil value)) in BugSack. Investigation: RecipeMaster'sTooltipHandlerglobally hooks the tooltip-set event chain, then callsgetAllCharactersRecipeStatuswhich iterates the character's known spell IDs and indexes its ownrecipeDB[spellId]without nil-checking. When RecipeMaster'srecipeDBisn't initialised for the current profession context (the locals dump showedrm.recipeDB = {},rm.displayedProfession = ""), every lookup returns nil and the hook crashes. The bug exists in RecipeMaster, not in TOGPM, but ourGameTooltip:SetItemByIDcall at GUI/MissingRecipesTab.lua:821 is what fires the tooltip-set event chain that RecipeMaster's broken hook listens on. v0.6.0's Missing Recipes accuracy fixes (thecraftedItemIdknown-recipe fallback in particular) changed which recipes appear in the missing list — recipes the user genuinely doesn't have are now correctly shown instead of recipes they already knew. That shifted the set of hover targets toward exactly the subset RecipeMaster's broken nil-check trips over, exposing a latent bug at high volume. - Fix: narrowly
pcall-wrap theGameTooltip:SetItemByID(f._itemId)call only — same defensive pattern as the existing comment block at the same site contemplated forLoonBestInSlot. Blizzard's C-levelSetItemByIDruns to completion before third-party hook callbacks fire, so the tooltip itself still populates correctly; the pcall catches errors that bubble back up from the hook chain so they don't pile up in BugSack. Other call paths (SetSpellByID, text fallback, the surroundingOnEnter/OnLeavescripts) are unchanged. Tradeoff: pcall would hide future errors from our own code if we ever break the SetItemByID call site, but the pcall is narrowed to that single line to keep the blast radius small. The underlying RecipeMaster bug still needs reporting upstream — fix on their end is a one-line nil-check atRecipeHandler.lua:43before indexingrecipe.
[v0.6.0] (2026-05-26) - Multi-language support: German (deDE) is the first translation + Classic Era Missing Recipes accuracy fixes + spec dead-code cleanup
New Features
Multi-language support; German (
deDE) is the first translation. Wired AceLocale-3.0 through every user-facing string in the addon — 265 keys covering the main window, all four tabs (Professions, Cooldowns, Reagents, Missing Recipes), the settings panel, all tooltips (column headers, button hovers, recipe rows, minimap), the supply-mail composer (subject + body), the bank-request dialog, every error / status print, slash-command help output, and every alert. Recipe names / item names / reagent names come back from the WoW client'sGetItemInfo/GetSpellInfolocalized for free because they're keyed by ID — zero work to ship those. The GermanLocale/deDE.luamirrors the structure ofLocale/enUS.luaexactly; any missing key falls back to English automatically via AceLocale's default-locale mechanism. The 9 core profession names (Alchemy → Alchimie, Blacksmithing → Schmiedekunst, Cooking → Kochkunst, Enchanting → Verzauberkunst, Engineering → Ingenieurkunst, First Aid → Erste Hilfe, Leatherworking → Lederverarbeitung, Mining → Bergbau, Tailoring → Schneiderkunst) were provided by a German-native contact; the remaining 6 (Herbalism, Skinning, Jewelcrafting, Inscription, Fishing, Smelting) use standard WoW-DE terminology pending native-speaker review. Native-speaker review for the full string set is also welcome. Adding additional locales (frFR,esES,ruRU,zhCN, etc.) is now a copy-template-and-translate task — no further code changes needed. Location: Locale/deDE.lua, Locale/enUS.lua, TOGProfessionMaster.toc.Profession names propagated through the locale table.
addon.PROF_NAMES(in TOGProfessionMaster.lua) now reads fromL["ProfAlchemy"]etc. instead of carrying hardcoded English. The table is the single source of truth for profession display names across the entire addon — every dropdown (Profession Browser, Cooldowns filter, Missing Recipes filter), the[TOGPM]item tooltip line, and the crafter-online alert text all pull from it. One locale change propagates to every site automatically.
Bug Fixes
- Missing Recipes on Classic Era flagged recipes you already know as missing. User report on a Cooking character: 82 recipes shown as missing despite knowing ~99% of them. Same on Leatherworking: 243 missing despite knowing nearly all of them. Two distinct issues stacked:
- Known-recipe detection mismatch. For non-Enchanting professions,
gdb.recipes[profId]is keyed by crafted-item ID (e.g. Charred Wolf Meat lives at key 2679, the item id, not key 2538, the spell id). The match against the authoritativeaddon.recipeDBthen relies onrd.spellIdbeing populated on the scanned entry.BuildSpellNameCachein Scanner.lua walks the player's spellbook to map recipe-name → spell-id, but on Classic Era this enumeration misses some profession recipe spells (they're not flagged as "spell" spellbook items the way TBC+ flags them), leavingrd.spellIdnil. Withoutrd.spellId,knownByChar(spellId)returned false for recipes the player actually knows, and they showed up as "missing." - Post-Vanilla recipes leaking onto Era clients. The recipe DB has
minExpansioncoverage gaps — Vanilla baseline recipes correctly carry no tag (intentional, they show on every client), but a handful of TBC / Cata / SoD recipes were also untagged in v0.5.7 (Crystal Throat Lozenge spell 30047 TBC, Smoked Redgill 470370 post-Cata, Prowler Steak 1225758+ SoD/Anniversary, etc.) and slipped past the existing cross-expansion gate. Fixes in GUI/MissingRecipesTab.luaBuildMissingList:
craftedItemIdfallback in the known-recipe check. When iteratingaddon.recipeDB[profId]by spell ID, also checkknownSpells[data.craftedItemId]— since the gdb table key IS the crafted-item ID for non-Enchanting professions, the lookup hits via the table key even whenrd.spellIdis nil. Sits alongside the existingdata.teachesfallback.- Defensive cross-expansion gate for untagged recipes on Classic Era. When
data.minExpansionis nil ANDclientExp == 1(Vanilla) AND the recipe's spell ID is > 25000, hide it. Every Vanilla recipe spell ID sits in the 2000–25000 range; anything higher with no tag is post-Vanilla content the Era client can't learn. Belt-and-suspenders to the existingrequiredSkill > clientMaxSkillfilter, which only catches reqSkill > 300 (low-skill TBC recipes like Crystal Throat Lozenge slip past that check). Both fixes are purely additive (elseifbranches in the existing skip-decision chain) — TBC / Wrath / Cata / MoP behavior is unchanged because the defensive gate isclientExp == 1only, and thecraftedItemIdfallback only adds matches, never removes them.
- Known-recipe detection mismatch. For non-Enchanting professions,
Data
- 1 additional never-shipped recipe excluded: Leatherworking spell 19106 "Onyxia Scale Breastplate" — the planned matching chest piece to the shipped "Onyxia Scale Cloak" (spell 19092, Onyxia attunement reward). The Breastplate was cut before going live; recipe scroll item 15780 and crafted item 15141 also never shipped. Removed from Data/Recipes/Leatherworking.lua and added to
MANUAL_EXCLUDED_SPELLSin tools/build_authoritative_data.py so future rebuilds don't reintroduce it. User-reported on Classic Era.
Code Cleanup
- Removed unused specializations filter infrastructure. Identified during the subclass-aware-filtering investigation that
data.specializationis never populated on any recipe in the shipped DB — the filter line inBuildMissingListthat checked it, along with theplayerSpec/specs/skillsBucketlocal lookups feeding it, plusScanner:DetectSpecializationsand itsSPEC_SPELLScatalog, was speculative future-proofing with no data source behind it. The dead-code path created a latent bug too: whenplayerSpecwas nil (Vanilla, or any character pre-spec-quest on TBC+), the filterdata.specialization ~= playerSpecwould have hidden every spec-tagged recipe — except it never fired because no recipe was tagged. Ripped out the read path in GUI/MissingRecipesTab.lua and the write path in Scanner.lua. Thegdb.specializationsAceDB schema entries and the purge logic in GUI/Settings.lua are kept in place — zero-cost plumbing for re-introducing spec-aware filtering as a proper feature later (would need a per-recipe spec data source that doesn't currently exist in any upstream source we mine).
Known Gaps (intentional, deferred)
- Profession-subclass filtering (LW: Tribal / Elemental / Dragonscale, Engineering: Goblin / Gnomish, BS: Armorsmith / Weaponsmith, Alchemy TBC: Potionmaster / Elixir / Transmutation, etc.) is deferred to a follow-up patch. Requires authoring per-recipe spec tags that aren't in any upstream data source we currently mine (wago.tools
SkillLineAbilitydoesn't expose spec gating). The dropdown UI infrastructure can be wired up cheaply once the data exists. User-driven dropdown (with auto-detect default viaIsSpellKnown) is the intended UX shape. - Specific NPC names in the Missing Recipes "Sources" tooltips still render in English on a deDE client. Recipe / item / reagent names already localize automatically via the WoW client's API (keyed by ID), but creature names aren't in any DBC (they're server-side data). Phase 2 work: extend tools/build_authoritative_sources.py to emit a
Data/NPCNames_deDE.luafrom thecreature_template_localetables that ship in the AzerothCore / TrinityCore TDB dumps we already have. Same template coversfrFR/esES/esMX/ruRU. - A few diagnostic slash-command outputs stay English (
/togpm status,/togpm versioncheck,/togpm spellcache,/togpm dumprecipe). These reference internal protocol terms (DeltaSync, P2P sessions, namespace, hash-cache, etc.) that don't translate cleanly and are power-user / maintainer tools.
[v0.5.7] (2026-05-26) - Per-profession manual curation pass — 100% TBC Phase 2 requiredSkill coverage + 8 expansion-rebalance corrections + 9 never-shipped recipes excluded
Data Quality
TBC Phase 2 visible recipes: 100% authoritative
requiredSkillcoverage across all 10 craftable professions (2,271 recipes; was 94.9% / 117 gaps in v0.5.6). Filled via a per-profession manual review pass with cited values — every entry in tools/manual_skill_overrides.json carries thereqSkill, thesourceattribution (Apprentice auto-grant / TRAINER_SHOW capture / Wowhead / quest reward / SoD content / etc.), theverified_byuser, and the recipe'snamefor human readability when grepping the file. Coverage now stops at the edge of the in-game data — every recipe the player can see in their Missing Recipes tab on TBC Phase 2 has an authoritative skill requirement.8 expansion-rebalance corrections. TBC Anniversary's trainers enforce HIGHER
ReqSkillRankvalues for several Vanilla Mining / Engineering recipes than the emulator SQL (AzerothCore Wrath + TrinityCore Cata) we pull from — Blizzard rebalanced trainer requirements between Vanilla and TBC and the emulators carry the pre-rebalance values. Validated via the v0.5.6TRAINER_SHOWcapture on Galdof; corrections now ship viamanual_skill_overrides.jsonat top priority over the emulator SQL: Smelt Iron 100→125, Smelt Gold 115→155, Smelt Steel 125→165, Smelt Mithril 150→175, Smelt Truesilver 165→230, Smelt Thorium 200→250, Coarse Blasting Powder 65→75, Coarse Dynamite 65→75.9 never-implemented spells removed from the recipe DB entirely. Blizzard's DBC retains some planned-but-cut recipes that never shipped in any live patch. Previously these appeared as un-learnable rows in the Missing Recipes tab. Added to
MANUAL_EXCLUDED_SPELLSat the build pipeline so they're filtered out before emit:- Enchanting: 22434 "Charged Scale of Onyxia" (sibling of the v0.5.5-excluded Refined Scale of Onyxia)
- Alchemy: 11447 "Elixir of Waterwalking" (DBC tagged as Alchemy but actually a daily quest reward)
- Engineering: 12719 "Explosive Arrow", 12720 (truncated placeholder name), 12722 "Goblin Radio", 12900 "Mobile Alarm", 12904 "Gnomish Ham Radio", 30561 "Goblin Tonk Controller", 30573 "Gnomish Tonk Controller"
119 hand-curated requiredSkill entries shipped. Up from 12 in v0.5.6. Breakdown:
- 27 Apprentice auto-grants across all 10 professions (Linen Cloak, Linen Bandage, Bolt of Linen Cloth, Light Leather, Handstitched Leather Boots, Rough Sharpening Stone, Copper Bracers, Smelt Copper, Rough Blasting Powder, Rough Dynamite, Minor Healing Potion, Elixir of Lion's Strength, Charred Wolf Meat, Roasted Boar Meat, Runed Copper Rod, Delicate Copper Wire, etc.) — all reqSkill=1 (Apprentice tier).
- 6 Mining smelt rebalance corrections + 2 Engineering powder rebalance corrections (see above).
- 11 Vanilla mid-tier Mining / BS / LW / Tailoring / Cooking / Engineering / Alchemy quest / drop / vendor recipes (Smelt Dark Iron 230, Mooncloth Boots 290, Gordok Ogre Suit Tailoring+LW 285, Crafted Light Shot 30, The Mortar: Reloaded 205, Dimensional Ripper - Everlook 285, Ultrasafe Transporter - Gadgetzan 285, Tranquil Mechanical Yeti 250, Goldthorn Tea 175, Smoked Desert Dumplings 285, Crystal Throat Lozenge 300, Restorative Potion 215, Gurubashi Mojo Madness 315, Enchant Bracer - Minor Health 70).
- 6 TBC Alchemy primal transmutes (all 385).
- 6 TBC Alchemy flasks + Super Rejuvenation Potion (all 390).
- 5 TBC Alchemy Major Protection cauldrons — the Discovery-system recipes (all 360).
- 47 SoD / Anniversary Phase 1-7 content recipes across all professions — values from Wowhead / community sources, tagged with the SoD phase in the JSON source field.
Inscription stays out of TBC visibility scope. Inscription was introduced in Wrath; its recipes are correctly filtered out by
minExpansion ≥ 3for TBC clients. The 50.5% Inscription coverage from v0.5.6 doesn't affect what TBC users see (it's all hidden). Future manual curation passes can extend the overrides to Wrath/Cata/MoP-visible recipes as Anniversary advances expansion content.
Manual Override Workflow (for future maintainers)
When a user reports a missing-skill recipe:
- Check the Missing Recipes tab — note the spell ID from the
[TOGPM] itemId=... spellId=...debug line on the tooltip (enable via Settings → Item tooltip). - Look up the real requirement on Wowhead / trainer / in-game.
- Add to
tools/manual_skill_overrides.jsonwithreqSkill,source,verified_by, andnamefields. Group with the appropriate_comment_*separator for readability. - Re-run
python tools/build_authoritative_data.pyand verify with a quick coverage probe. - For recipes that are in DBC but never shipped in live, add to
MANUAL_EXCLUDED_SPELLSinbuild_authoritative_data.pyinstead.
[v0.5.6] (2026-05-25) - Recipe-skill data quality pass — name-match scroll linker + hand-curated overrides + in-game trainer observation capture + tooltip toggles off by default
New Features
Recipe-scroll name-match supplemental linker in build pipeline. Closes a long-standing data gap: many WoW recipe scrolls implement their teach effect via a generic "Learning" spell (spell 483) whose effect is then chained server-side to grant the specific craft spell. Blizzard's
ItemEffecttable only captures the item→Learning link, NOT the item→specific-craft link — so our pipeline previously missed ~80-100 recipe-scroll links per profession (e.g. Pattern: Mooncloth Robe → Craft Mooncloth Robe spell). The new linker exploits WoW's predictable recipe-scroll naming convention ("Pattern:" / "Plans:" / "Recipe:" / "Formula:" / "Schematic:" / "Design:" / "Manual:" / "Technique:" + craft name), strips the prefix, and matches the remainder againstSpellName. 97% hit rate on Vanilla (1,047 of 1,082 recipe scrolls match a spell). The 35 unmatched are mostly singular/plural mismatches, legacy "Imbue X" enchant naming, and skill-rank books that aren't recipes. Validated at the recipe level: every name-matched scroll now flows itsItemSparse.RequiredSkillRankinto the recipe'srequiredSkill, AND the scroll's item ID becomes the recipe'sitemId(so MissingRecipesTab can render the scroll icon + chat-insert link). Coverage on TBC-visible recipes jumped from ~80% to 94.9% (only 117 of 2,280 visible recipes still show-for unknown skill requirement). Location: tools/build_authoritative_data.pyspell_by_namelookup + recipe-prefix walk +items_by_spellaugmentation.Hand-curated
manual_skill_overrides.jsonas top-priorityrequiredSkillsource. For the residual recipes whose data isn't in ANY DBC dump OR emulator SQL source (true apprentice auto-grants like Linen Cloak / Smelt Copper / Delicate Copper Wire, plus a few quest-direct-grants whose scrolls don't exist in the current DBC like Smelt Dark Iron / Gordok Ogre Suit). Each entry includesreqSkill,source(e.g. "Apprentice JC auto-grant"),verified_by(whose call), andname(for human readability when grep-ing the JSON). Wired as the top of the priority chain: manual override > scrollRequiredSkillRank> trainer SQLReqSkillRank>-(unknown). Lets the maintainer correct any of the lower-tier sources by adding a single JSON entry without rebuilding the whole pipeline. Initial seed (JC apprentice auto-grants): Delicate Copper Wire 25255, Braided Copper Ring 25493, Woven Copper Ring 26925, Rough Stone Statue 32259 all = 1. Per-profession curation pass to follow incrementally. Location: tools/manual_skill_overrides.json, tools/build_authoritative_data.py_load_manual_skill_overrides.In-game trainer observation capture (
gdb.trainerObservations). NewTRAINER_SHOW/TRAINER_UPDATEevent hook in Scanner.luaOnTrainerShowthat captures the EXACTReqSkillRankBlizzard's server enforces for every spell a trainer offers (viaGetTrainerServiceSkillReq(i)— same numeric value as the trainer's "Requires Tailoring (100)" tooltip line). One trainer visit captures every spell that trainer teaches in a single event. Stored asgdb.trainerObservations[spellId] = { reqSkill, moneyCost, profId, observedBy, observedAt }— top-level table, deliberately named to be grep-able in SavedVariables for easy extraction by the maintainer. Three-tier link resolution from the trainer service link: (a) direct spell-ID extraction fromenchant:/spell:link prefixes; (b) reverse-lookup viacraftedItemIdagainst shippedrecipeDBwhen the link is anHitem:(the TBC pattern — service link is the produced item, not the spell); (c) name match for spells with no craftedItemId (Enchanting recipes). Single-user validation across 9 trainers (BS / Tailoring / LW / Engineering / Alchemy / Mining / Cooking / Enchanting / JC) captured 369 entries: 361 confirmed agreement with shipped values, 8 revealed legitimate TBC-era Blizzard rebalances on Mining smelts (Smelt Iron 100→125, Smelt Mithril 150→175, Smelt Truesilver 165→230, Smelt Gold 115→155, Smelt Thorium 200→250, Smelt Steel 125→165) plus 2 Engineering powders. DeltaSync wireup so observations propagate guild-wide is deferred to a later patch — the apprentice / quest-grant gap doesn't close via trainer visits anyway (trainers don't list those), so the marginal value of full guild propagation is low until we fill more of the manual override file. Setting toggle for opt-out also deferred. Location: Scanner.luaOnTrainerShow, TOGProfessionMaster.lua schema doc.
UX Changes
- Both
[TOGPM]tooltip lines (crafters list + item/spell IDs) now default OFF. Previously v0.5.5 shipped them ON, which added two lines to every item tooltip the user hovered (bags / AH / chat links / vendors / mailbox). Default flipped to off so the addon stays silent on tooltips until the user opts in via Settings → Item tooltip section. Existing users who installed v0.5.5 keep their explicit value (AceDB doesn't overwrite already-saved profile entries). New users / fresh profiles get the off-default. Location: TOGProfessionMaster.luaSETTINGS_DEFAULTS.
Investigation Notes (for future v0.5.x work)
- Confirmed dead-ends for the residual
requiredSkillgap: DBCSpellLearnSpelltable doesn't exist on wago,SpellEffect[Effect=36](LEARN_SPELL) has no rows linking profession-train spells to their auto-granted apprentice recipes, AllTheThingsProfessions.luaonly lists recipes with notable sources (drops/quests), Blizzard's Battle.net Game Data API exists for Classic namespaces but the recipe endpoint isn't implemented (and even retail's recipe endpoint exposes norequired_skillfield). The auto-grant logic lives entirely in emulator C++ source (e.g. AzerothCore'sLearnDefaultProfessionRecipes). Hand-curation viamanual_skill_overrides.jsonis the path forward — small per-profession passes as the maintainer has bandwidth. - Per-recipe coverage diagnostics added: TBC Phase 2 visible breakdown shows JC at 100%, BS 97.7%, Cooking 95.9%, LW 95.1%, Tailoring 95.1%, First Aid 94.1%, Enchanting 93.8%, Engineering 93.1%, Mining 90.5%, Alchemy 86.2% (laggard, 27 of 196 still
-).
[v0.5.5] (2026-05-25) - TBC tooltip rewrite + recipe icons + shift-click link insert + authoritative skill requirements + obsolete-name scrub + cross-expansion bleed + "My Characters" crafter list
New Features
Recipe rows in the Profession Browser now show the actual crafted-item icon. Previously the rows displayed whatever
rd.iconhappened to hold — historically populated by stub creation paths that often stored a generic spell icon (blue spell-scroll for BS, net for Tailoring,?for JC, violin/bag for LW, NPC face for Black Dragonscale, etc.). Icon resolution now goes: (1) crafted item ID parsed fromrd.itemLink(Hitem:N) — this works for both Classic Era item-keyed entries AND TBC spell-keyed entries where the recipe key is the spell ID but the itemLink still carries the produced item'sHitem:N. (2)GetItemIcon(recipeId)for Classic Era item-keyed fallback. (3) cachedrd.iconlast-resort. Spell-id/item-id collision detection: when the parsed itemLink ID equals the recipe's spell ID (common for Enchanting where stub creation poisoned the link with an unrelated item — e.g. spell 13522 / Enchant Cloak gotHitem:13522= "Recipe: Flask of Chromatic Resistance", spell 13868 / Enchant Gloves gotHitem:13868= "Frostweave Robe"), the collision is rejected and we fall back to the proper enchant scroll icon. Locations: GUI/BrowserTab.luaBuildRecipeListicon block.Recipe rows now produce a working chat-insertable link on shift-click. Previously shift-click silently did nothing on trainer-taught recipes and any stub-created entry where
rd.itemLinkandrd.recipeLinkwere both nil. NewResolveRecipeLink(entry)helper falls throughitemLink → recipeLink → GetItemInfo(id) → GetSpellLink(spellId) → synthetic "item:<id>". Applies to both the recipe-row hover handler in the list AND the drilldown header button. Location: GUI/BrowserTab.luaResolveRecipeLink.Global item tooltips now append a TOGPM IDs line as a second branded line under the existing crafters line. The new line shows
[TOGPM] itemId=N spellId=N(in the addon brand color) so users can paste IDs into Wowhead etc. to troubleshoot icon/link issues. Appears on hover anywhere an item tooltip shows — bags, AH, vendors, chat links, mailbox. Two new Settings toggles in the Game Options panel let users disable each line independently: "Show guild crafters on item tooltips" and "Show item ID / spell ID on item tooltips" (both default on). The same brand-color lines also appear at the bottom of the Profession Browser's recipe-row and drilldown-header tooltips for consistency across every TOGPM tooltip surface. Locations: Tooltip.luaAppendCraftersNow/AppendBrandIdsLine, GUI/Settings.lua, GUI/BrowserTab.luaAppendBrandTooltipLines.New per-recipe
craftedItemIdfield shipped in the recipe DB. Populated at build time fromSpellEffect[Effect=24].EffectItemType— the canonical "this spell creates this item" mapping. Lets trainer-taught crafts (which have no scroll item) render the produced item's icon in the Missing Recipes tab instead of falling to a generic spell icon. Example: Heavy Weightstone (spell 3117) now ships withcraftedItemId=3241and renders the actual stone icon; Coarse Sharpening Stone withcraftedItemId=2863; Silver Rod withcraftedItemId=6338; etc. Location: tools/build_authoritative_data.pycreated_item_by_spell, GUI/MissingRecipesTab.lua trainer-only icon branch.Skill column in Missing Recipes now shows the literal "Requires Blacksmithing (175)" tooltip value, sourced from authoritative data. Three-tier resolution: (1) recipe-scroll
ItemSparse.RequiredSkillRank— the LITERAL value the in-game tooltip shows on the scroll. (2) Trainer SQLtrainer_spell.ReqSkillRank— the LITERAL value the trainer NPC enforces; sourced from AzerothCore Wrath + TrinityCore Cata + NEW CMaNGOS Classic emulator data. (3) When neither source has data, the column shows-so the gap is visible — we explicitly do NOT fall back to SLAMinSkillLineRank(which is the placeholder1for many recipes) or the green-threshold proxy (which runs 0-5 points off the real value). Coverage: 80% of 5,864 recipes (4,693) have authoritative values; the remaining 20% (1,171, mostly auto-learned Apprentice-tier and a chunk of Inscription) show-. Validation against known recipes: Silver Rod=100, Silver Skeleton Key=100, Golden Rod=150, Arcanite Rod=275, Heavy Weightstone=125, Coarse Sharpening Stone=65, Adamantite Cleaver=330 — all match the in-game trainer/scroll tooltips exactly. Locations: tools/build_authoritative_data.pyrequired_skillresolution, tools/extract_emulator_trainers.pyextract_all_skill_ranks+ new CMaNGOS Classic flat-layout parser, GUI/MissingRecipesTab.lua.Cooldowns tab cloth-spec icons fixed. Primal Mooncloth / Spellcloth / Shadowcloth were showing a generic net icon because
GetSpellTexturereturns a placeholder for those craft spells. Added per-spell entries toICON_OVERRIDESmapping each cloth spec spell to its produced bolt's item icon (Primal Mooncloth → Bolt of Primal Mooncloth item 21845, Spellcloth → Bolt of Spellcloth 24272, Shadowcloth → Bolt of Shadowcloth 24271). Location: Data/CooldownIds.luaICON_OVERRIDES.
Bug Fixes
TBC Anniversary global item tooltip — the
[TOGPM] crafter1, crafter2line never appeared. Two distinct issues:- Hook never fired in TBC. The legacy
OnTooltipSetItemhook was registered only in theelsebranch when the modernTooltipDataProcessorAPI wasn't available. TBC Anniversary advertisesTooltipDataProcessor(sohasModern=true) but never actually fires the PostCall — so we registered ONLY the modern hook, which never ran, and the legacy hook was skipped. Fix: registerOnTooltipSetItemUNCONDITIONALLY alongside the modern path; the_togpmAppendeddedup inAppendCrafterskeeps the dual registration from double-appending. Location: Tooltip.lua PLAYER_LOGIN handler. FindCraftersreturned nil for every item hover even when the crafters were present ingdb.recipes. On TBC,GetTradeSkillRecipeLinkreturns|Henchant:SPELLID|h[...]|h|rlinks for EVERY profession (not just Enchanting).Scanner:ExtractTradeSkillIdmatches theenchant:prefix first and returns(spell_id, isSpell=true), so every TBC recipe gets stored ingdb.recipeskeyed by spell ID withisSpell=true.FindCraftersfilteredif not rd.isSpell and recipeId == itemID— which matched nothing because EVERY TBC recipe hasisSpell=true. Validated: user's Anniversary SavedVariables has 1,494 recipes withisSpell=truevs 11 withisSpell=false(Classic Era is the inverse). Fix: newResolveRecipeForItem(profRecipes, itemID)helper tries the fast item-keyed lookup first (Classic Era pattern), then falls back to walking the spell-keyed entries and matching via theHitem:NID parsed out ofrd.itemLink(TBC pattern). Non-destructive — doesn't require rescan or breaking sync hashes. Verified: Roasted Kodo Meat (spell 6414, isSpell=true, itemLink containsHitem:5474) now resolves crafters when hovering the Roasted Kodo Meat item 5474. Location: Tooltip.luaResolveRecipeForItem/FindCrafters.
- Hook never fired in TBC. The legacy
Settings panel — clicking the gear icon's close button threw a Lua error:
AceConfigRegistry-3.0:ValidateOptionsTable(): TOGProfessionMaster.args.tbcAnniversaryPhase.sortByValue: unknown parameter. The v0.5.4 TBC Anniversary phase dropdown usedsortByValue = truewhich isn't a valid AceConfig field. Replaced withsorting = {1, 2, 3, 4}— explicit order array — so the dropdown lists Phase 1 → 4 instead of AceConfig's default alphabetical-by-label sort. Location: GUI/Settings.lua.Missing Recipes — Classic-Era-exclusive recipes (Season of Discovery / Anniversary content) appeared on TBC/Wrath/Cata/MoP clients with broken icons. Examples: spell 427061 "Mantle of the Second War" (
?icon, no scroll), spell 1224639 "Scarlet Soldier's Stompers" (#238329 (loading…)placeholder, scroll item that doesn't exist in TBC's client DB). These are 1.15.x-only recipes that ship in our unified recipeDB because they appear in Vanilla's SkillLineAbility, but the older-expansion clients have no record of the spells OR the scroll items. Filter added inBuildMissingList: skip any recipe whereGetSpellInfo(spellId)returns nil (the spell isn't in this client) AND the scroll item is also missing from the client (GetItemInfoInstant(itemId)returns nil — synchronous, distinguishes "item doesn't exist on this client" from "cache cold"). Conservative — only fires when BOTH spell AND item are unknown to the client. Location: GUI/MissingRecipesTab.luaBuildMissingList.Missing Recipes trainer-only icon improvement. When a trainer-taught recipe has no scroll,
GetSpellTexture(spellId)often returned a generic/wrong icon. Now usesentry.craftedItemId(newly shipped in the recipe DB — see New Features) as the authoritative icon source viaGetItemIcon. Falls back toGetSpellTextureonly when nocraftedItemIdis available (Enchanting craft spells with no produced item — correctly shows the enchant scroll icon). Location: GUI/MissingRecipesTab.luaFillListtrainer-only branch.Dev-sync PowerShell script (
wow-version-replication.ps1) was copyingtools/,.claude/,CLAUDE.md, and.markdownlintignoreto non-source WoW installs. The script's hardcodedSkipPatternslist had drifted from.pkgmeta's authoritativeignore:list —tools/,.claude/,**/*.bat,.markdownlintignore, andCLAUDE.mdwere never excluded. Fix: alignedSkipPatternsto mirror.pkgmeta(so anything excluded from the CurseForge package is also excluded from local dev sync). Cleanup pass deleted previously-replicated artifacts from_classic_and_anniversary_installs. Includes a note that future ignores need to be added to BOTH files. Location: wow-version-replication.ps1.Recipe rows display obsolete Blizzard internal item names like "59 TEST Green Shaman Chest", "ZZOLD Design: Bracing Earthsiege Diamond", "DEPRECATED Formula: Enchant Chest - Exceptional Mana", "Manual: Crystal Infused Bandage [PH]" — user reported "Green Shaman Chest" displaying as "59 TEST Green Shaman Chest" with "tens of recipes like this". Two distinct root causes:
- Data side (~29 shipped recipes): tools/build_authoritative_data.py collects
items_by_spellfrom Blizzard'sItemEffecttable then picksitems[0]as the recipe'sitemId. For many recipes Blizzard's DBC retains an obsolete variant of the recipe scroll (ZZOLD/TEST/DEPRECATED/UNUSED/[PH]) with a lower item ID than the live version — so sorteditems[0]returned the obsolete one. Worst offender: 20 Wrath Jewelcrafting designs whose itemId pointed atZZOLD Design: ...items (41403-41422) instead of the live 41397-41396 set. Fix adds anis_obsolete_item_name()filter (patterns:\bTEST\b,\bQA\b,\bDEPRECATED\b,\bUNUSED\b,^ZZ,\[PH\],\bOLD$) that drops the bad item from the per-spell set before picking items[0]. Build pipeline now reports e.g.skipped 386 obsolete item-teach rows (ZZOLD/TEST/DEPRECATED variants)per build. Re-emittedData/Recipes/*.lua— only 1 of ~29 bad entries survives (Crystal Infused Bandage, item 23689, whose [PH] suffix exists only in Wrath+ DBC; on Vanilla/TBC clientsGetItemInfo(23689)returns the clean name). - Runtime side (spell-id / item-id namespace collision + cross-guildmate poison spread): Scanner.lua
MergeCraftersIntoGdbcreates a stub recipe entry whencrafters:<profId>arrives beforerecipemeta:<profId>, andBackfillBogusRecipeNamesruns at PEW to heal"? <id>"placeholders. Both callGetItemInfo(recipeId)whererecipeIdis a spell ID — and the spell-ID namespace overlaps the item-ID namespace. Example: spell 26926 = "Heavy Copper Ring" (Jewelcrafting) but item 26926 = "59 TEST Green Shaman Chest" in TBC/Wrath/Cata/MoP ItemSparse.GetItemInfohappily returns the TEST name and we cached it asstub.name, so every render shows the bad name forever. Worse: once cached, the bad name was broadcast viarecipemeta:<profId>and every other client'sMergeRecipeMetaIntoGdbstamped it in too — one poisoned guildmate could re-infect every other user every time they synced. Fix has four parts:- New
isObsoleteItemName()Lua helper (mirror of Python pipeline patterns:TEST/QA/DEPRECATED/UNUSED/ZZ-prefix /[PH]/ trailingOLD). MergeCraftersIntoGdbstub creation: whenGetItemInfo(spellId)returns an obsolete-marker name, treat as nil and fall through toGetSpellInfo(the correct API for a spell-id stub).BackfillBogusRecipeNamesrecovery: sameGetItemInfoguard so the recovery pass doesn't re-stamp the TEST name.MergeRecipeMetaIntoGdb(sync receive): drop incoming obsolete names from peers; treat existing local obsolete names as bogus and re-heal them. Kills the cross-guildmate re-infection loop — even if one guildmate is still running a pre-v0.5.5 version and broadcasts poisonedrecipemeta, our merge no longer accepts it.- New
ScrubObsoleteRecipeNames()one-time PEW pass: walksgdb.recipesand clearsrd.name/rd.icon/rd.itemLinkon any entry whose cached name matches an obsolete marker (left over from pre-v0.5.5 stubs in SavedVariables). Runs BEFORE the backfill schedule so the nextBackfillBogusRecipeNamespass repopulates the cleared names viaGetSpellInfo. Self-heals on/reload— no/togpm purgerequired. LogsScrubbed N obsolete recipe names (TEST/ZZOLD/DEPRECATED/[PH]) from cachewhen something is actually cleaned. Locations: Scanner.luaisObsoleteItemName/MergeCraftersIntoGdb/BackfillBogusRecipeNames/MergeRecipeMetaIntoGdb/ScrubObsoleteRecipeNames, tools/build_authoritative_data.py_OBSOLETE_NAME_PATTERNS/is_obsolete_item_name.
- New
- Data side (~29 shipped recipes): tools/build_authoritative_data.py collects
"My Characters" view on the Profession Browser shows guildmate crafter names in each recipe's crafter list, making it look like the addon is treating guildmates as your alts — user reported seeing "Gumshots and 29+ others for anti-venom" with the view set to My Characters. Root cause is NOT in the visibility filter (
mineVisiblecorrectly requires at least one MY-character crafter on line 207-213) and NOT inIsMyCharacter/accountChars(a new/togpm myaltsdiagnostic confirmedglobal.accountCharsis clean: only the logged-in toon is flagged as MINE, every guildmate correctly returnsfalse). The bug is that the CRAFTER LIST RENDER on each visible recipe still iterates every entry inrd.craftersand adds non-mine entries as plain crafter names alongside the user's own alts — so a recipe that one of YOUR alts knows AND that 30 guildmates also know renders with 30 guildmate names next to your alt. Reads exactly like "TOGPM thinks these guildmates are my alts" even though they're not. Fix: inBuildRecipeList's render loop, theelsebranch that appends guildmate crafters is now gated onviewMode ~= "mine"— inmineview, only own-alt crafters render.guildview shows everyone as before. Location: GUI/BrowserTab.luaBuildRecipeList.Recipes from later expansions leaking into earlier clients' Missing list (Wrath transmute spell 53771 "Transmute: Eternal Life to Shadow" showing on TBC; Wrath/Cata/MoP recipes generally surfacing as unobtainable rows on TBC Anniversary). Root cause exposed by user testing: the v0.5.3 cross-expansion filter (
requiredSkill > clientMaxSkill) wasn't working becauserequiredSkillis sourced fromMinSkillLineRankin wago.tools'SkillLineAbility, which defaults to1for many recipes (it represents the "becomes orange" threshold, NOT the learn requirement). The check1 > 375is never true, so nothing got filtered. Recipes were leaking because the build pipeline unions data forward across all 5 expansion builds (Vanilla → MoP), and wago.tools' MoP-build SkillLineAbility contains every spell ever shipped — meaning Wrath/Cata/MoP recipes ended up in the merged DB with no per-expansion gate. The fix adds an explicitminExpansionfield per recipe (1-5 indexing Vanilla through MoP) set during the build-time merge to the EARLIEST build index where the spell first appeared in SkillLineAbility. A newBuildMissingListfilter hides any recipe whoseminExpansion > clientExpregardless of skill cap or phase tag — that's the primary cross-expansion gate now, with the skill-cap check kept as a belt-and-suspenders fallback. Validation: spell 53771 →minExpansion=3(Wrath), correctly hidden on TBC. Inscription drops to 0 visible recipes on TBC (it didn't exist pre-Wrath). Total: 3,388 of 5,865 recipes hidden on TBC clients that were previously visible. Locations: tools/build_authoritative_data.py (merge_expansions+emit_recipe_file), GUI/MissingRecipesTab.luaBuildMissingList,Data/Recipes/*.luare-emitted.Hand-curated
MANUAL_EXCLUDED_SPELLSexclusion list in the build pipeline for recipes that exist in wago.tools / DBC data but were never actually obtainable in the live game (planned-but-cut content, leftover dev entries). First entry: spell 22430 "Refined Scale of Onyxia" — Alchemy recipe planned for Vanilla AQ40 but never implemented (would have bottlenecked Onyxia loot turn-ins). User-reported; verified non-obtainable via Wowhead comments + community sources. Removed entirely fromData/Recipes/Alchemy.luaat emit time (count drops 363 → 362). Future user reports of similar phantom recipes get appended to the list with a# sourcecomment so the next maintainer can audit the entry. Location: tools/build_authoritative_data.pyMANUAL_EXCLUDED_SPELLS.
Known Gaps
- Alchemy cauldron recipes show source "Unknown" when they should show "Discovery" (made via Major Protection Potion discovery). Needs a new source kind + small hardcoded list. v0.5.6 candidate.
[v0.5.4] (2026-05-25) - TBC Anniversary phase filter (hide future-phase recipes like Sunwell on Phase 2)
New Features
TBC Anniversary phase filter on the Missing Recipes tab. TBC Anniversary is currently live on Phase 2 (Serpentshrine Cavern + Tempest Keep), but the recipe DB we ship contains every TBC recipe including Phase 3 (Black Temple, Hyjal), Phase 3.5 (Zul'Aman), and Phase 4 (Sunwell, Magisters' Terrace, Shattered Sun). Users on Phase 2 were seeing Sunwell jewelcrafting designs and Black Temple drops in their Missing list with no way to actually obtain them. New Settings entry "TBC Anniversary phase" (visible only on TBC clients via
addon.isTBC) lets the user pick the current live phase;BuildMissingListthen hides any recipe whosephasefield exceeds the selected value. Default = 2 (matches Anniversary live state on v0.5.4 release date). Users bump the setting as Blizzard advances phases. Locations: GUI/Settings.lua, GUI/MissingRecipesTab.lua, Locale/enUS.lua, TOGProfessionMaster.lua (default).Per-recipe phase data sourced from AllTheThings at build time. Phase numbers are derived in a dev-only Python pipeline that reads ATT's per-expansion Lua data files via
lupa(an embedded Lua VM in Python — chosen over regex-based parsing because executing ATT's actual Lua code is more reliable than trying to re-implement Lua semantics for ATT's compressed nested-call format). The pipeline merges recipe fields across 5 ATT files (Professions.lua,Instances.lua,Zones.lua,WorldDrops.lua,Craftables.lua) since the phase-relevant data (minReputation,awp) is scattered — e.g. spell 42588 (Design: Kailee's Rose) has only basic fields inProfessions.luabut its Shattered Sun reputation gateminReputation={935: 9000}only appears inZones.lua. Phase derivation combines three signals in confidence order: (1) boss-drop containment inside a known TBC raidinst()wrapper (Karazhan→1, SSC→2, Tempest Keep→2, Hyjal→3, Black Temple→3, Magisters' Terrace→4, Sunwell Plateau→4); (2) faction reputation gate (Shattered Sun Offensive→4, Scale of the Sands→3, Ashtongue Deathsworn→3); (3) ATT'sawppatch-added tag (≥2.4.0→4, ≥2.3.0→3). Fallback: nophasefield emitted, addon treats as always-visible. Coverage on the 2,252 shipped TBC recipes: 16 tagged Phase 2, 109 tagged Phase 3, 87 tagged Phase 4. Locations: tools/att_probe.py, tools/att_extract_phase.py, tools/build_authoritative_data.py,Data/Recipes/*.luare-emitted.
Known Gaps
- Recipes ATT doesn't tag with phase signals default to Phase 1 (always shown). 88 of our 2,252 shipped TBC recipes have no entry in ATT at all; ATT also doesn't tag every Sunwell-era recipe with
awporminReputation(their schema only requires phase metadata on a subset). These slip past the filter and remain visible on earlier phases — opposite of the user's complaint, but the failure mode this release is biased toward (show extra rather than hide what shouldn't be hidden) since we can't distinguish "ATT doesn't know" from "ATT knows it's launch content." - Phase 3.5 (Zul'Aman) is currently bucketed as Phase 3 or 4 depending on the recipe's other signals — the patch table doesn't separate 2.3.0 (Zul'Aman) from 2.4.0 (Sunwell) cleanly enough for a sub-phase split. When Anniversary advances to Phase 3.5 in autumn 2026 we'll add a separate setting value and re-derive.
- Wrath / Cata / MoP have no phase tags yet. Same pipeline will produce them when those Anniversary cycles approach phase changes that matter; v0.5.4 is TBC-scoped because that's the immediate user-reported bug.
[v0.5.3] (2026-05-23) - Cross-expansion recipe bleed + persistent "(loading)" placeholders
Bug Fixes
Recipes from later expansions appearing in the Missing list on earlier-expansion clients (e.g. MoP First Aid spell 102697 showing for a TBC Anniversary player who can never learn it; Wrath/Cata Jewelcrafting designs on TBC) — v0.5.0's recipe database is a UNION across every expansion's data shipped as a single
Data/Recipes/*.luaset, loaded by every TOC variant. So a TBC client loads MoP recipes too even though they're unreachable. Fix in GUI/MissingRecipesTab.luaBuildMissingList: hide any recipe whoserequiredSkillexceeds the current client's expansion cap. Caps: Vanilla 300, TBC 375, Wrath 450, Cata 525, MoP 600. Detected via the existingaddon.isVanilla/isTBC/ etc. flags from Compat.lua; no Data file changes needed. A future-expansion recipe shows up automatically once the player is on a later TOC variant that supports its required skill. Note: this is an expansion-level filter only — TBC content from later PHASES (e.g. Shattered Sun Offensive recipes during Phase 2 of TBC Anniversary) still surfaces because we don't have per-recipe phase metadata; that's a separate v0.5.4+ problem requiring phase tagging.Rows persistently showing
#22430 (loading…)instead of the recipe name —GetItemInforeturns nil when an item is not yet in the WoW client cache (triggers an async load) AND returns nil indefinitely if the item id genuinely doesn't exist on the current client (e.g. some Vanilla recipe scrolls were removed from the game over the years even though wago.tools still has the spell). The previous fallback was a static "(loading…)" placeholder that never resolved in the latter case. Fix in GUI/MissingRecipesTab.lua FillList: when the item name can't be resolved, fall back to the SPELL name (GetSpellInfo(entry.spellId)) and the spell icon (GetSpellTexture). The row now shows e.g. "Brown Linen Vest" via the spell name instead of "#22589 (loading…)" via the missing item. When the item later loads from the cache,GET_ITEM_INFO_RECEIVEDtriggers a refresh and we render the proper item display.
[v0.5.2] (2026-05-23) - Skill-rank-up spells in the Missing list + cache-cold item crash
Bug Fixes
Skill-rank-up + utility spells appearing in the Missing Recipes list ("Master Alchemy", "Find Minerals", "Apprentice Jewelcrafting", etc. surfacing as missing "recipes") —
SkillLineAbilityin wago.tools is wider than just craftable recipes: it also includes the rank-up spells trainers hand out at each tier (Apprentice / Journeyman / Expert / Artisan / Master / Grand Master / Illustrious), profession specialisation spells, and utility toggles like "Find Minerals" / "Find Herbs" / "Find Fish". v0.5.0 / v0.5.1 emitted every SLA row, so these non-recipes leaked into the list. Filter in tools/build_authoritative_data.py'semit_recipe_file: skip any spell wherereagentsis empty AND noitems(recipe-scroll items) exist. Every real craft has at least one reagent (you need materials); every scroll-taught recipe has at least one teaching item. Anything missing BOTH is by definition not a craftable recipe. Re-ran Phase A; 149 non-recipe spells filtered across all professions (Mining -23, Engineering -14, Tailoring -13, Inscription -13, Blacksmithing -13, Cooking -9, etc.). New recipe total: 5,865 (was 6,014).Blizzard_ObjectAPI/Classic/Item.lua:320: table index is nilcrash storm STILL firing at higher spell IDs (e.g. 27667, 33209, 35752+) even after v0.5.1's nil-itemId fix — different root cause this time: we now correctly pass a real, valid item ID toGameTooltip:SetItemByID, but when the item is not yet in the WoW client's item cache,LoonBestInSlot's post-hook (AceHook-3.0 wrapper around SetItemByID) readsGameTooltip:GetItem()and gets back nil — the cache hasn't populated yet — then crashes callingContinueOnItemLoad(nil). Symptom:Tooltip: fallback Show-hook fired for itemID = Nappears in chat (proving the item id IS valid) but BugSack still shows 289x Item.lua errors from LoonBestInSlot's stack. Fix in GUI/MissingRecipesTab.luaOnEnterhandler: useGetItemInfo(itemId)as both a cache-presence test and an async-load trigger BEFORE deciding to callSetItemByID. When cached → callSetItemByIDas before. When not cached → fall back to spell tooltip (SetSpellByID) AND the GetItemInfo call queues the item for fetch, so the next mouseover on the same row succeeds. Effectively the row "warms up" on first hover then renders the proper item tooltip on subsequent hovers. No more SetItemByID on cache-cold items, no more crash in LoonBestInSlot.
[v0.5.1] (2026-05-23) - Lua error storm in Missing Recipes from passing spell ids to item-id APIs
Bug Fixes
Blizzard_ObjectAPI/Classic/Item.lua:320: table index is nilLua error firing 148-247x per Missing Recipes session, with the stack rooted inTOGProfessionMaster/GUI/MissingRecipesTab.lua:683(the rowOnEnterhandler callingGameTooltip:SetItemByID) — v0.5.0 rebuilt the recipe DB keyed by recipe SPELL id (was the crafted-item id in the legacy PS data). The row code in GUI/MissingRecipesTab.lua still treatedentry.spellIdas an item id and passed it to every item-id API:GameTooltip:SetItemByID(spellId),GetItemInfo(spellId),GetItemIcon(spellId),GetItemQualityColor. Blizzard'sSetItemByIDsilently sets an empty tooltip when given an id that doesn't match an item — but downstream addons that hookSetItemByID(e.g. LoonBestInSlot's tooltip-update path) then crash trying to load the empty item, surfacing as the storm oftable index is nilerrors in BugSack. Fix has three parts:Add a real
itemIdfield to the runtime recipe DB. tools/build_authoritative_data.py was already capturingitems(list of recipe-scroll item ids that teach the spell, viaItemEffect) but stripping the field fromemit_recipe_file. Now keep the first id asentry.itemId(or omit when the recipe has no scroll — trainer-taught entries like "Brown Linen Vest" spell 2385 are correctly nil). Re-ran Phase A; verified e.g. Red Linen Robe (spell 2389) → itemId 2598 (Pattern: Red Linen Robe).Route every item-id API call through
entry.itemIdwith a spell-id fallback. GUI/MissingRecipesTab.luaFillList,OnEnter,OnMouseDown, the AH-scangetItemscollector, and the search filter all updated. WhenitemIdis nil (trainer-only recipe with no scroll), fall back toGetSpellInfofor the row name,GetSpellTexturefor the icon,GameTooltip:SetSpellByIDfor the hover tooltip, andGetSpellLinkfor the shift-click chat link. Bank + AH buttons hide when itemId is nil (no scroll to bank or auction).Guard the row tooltip handler against any nil id.
OnEnternow early-returns unlessf._itemId or f._spellIdis non-nil. The previous guard checked onlyf._itemId, which never triggered the early-return for trainer-only rows because we were also storing the spell id INf._itemId(the bug).
Knock-on benefit: search filter now finds recipes by spell name too, so trainer-taught recipes are searchable by their actual name instead of only by their (non-existent) scroll item name. Location: GUI/MissingRecipesTab.lua, tools/build_authoritative_data.py,
Data/Recipes/*.lua(re-emitted withitemIdfield).
[v0.5.0] (2026-05-23) - Authoritative recipe + source data from Blizzard DBCs + emulator world DBs
Bug Fixes
- Recipes the player already knows showing up as "Missing" on TBC / Anniversary / MoP (a class of bugs that v0.4.6 / v0.4.7 only partially addressed) — root cause was a keyspace mismatch between
recipeDBandgdb.recipes.Scanner:ExtractTradeSkillId(Scanner.lua:869) returns the SPELL id for Enchanting (its recipe links carryenchant:SPELLID) but the CRAFTED ITEM id for every other profession (Tailoring / BS / LW / Cooking / etc. recipe links carryitem:ITEMIDfor the produced item). The scanner stores each row keyed by what that returns, sogdb.recipes[197][2589] = {...spellId=2385...}for "Brown Linen Vest". OurrecipeDB[197][2385](keyed by recipe spell id) then did a directbucket.recipes[profId][2385]lookup inknownByChar— which missed every time because the bucket key is 2589, not 2385. The previousknownByCharwalked every guild bucket (v0.4.7's cross-bucket fix) but the key was wrong in every bucket. Fix: build aknownSpellsset once perBuildMissingListcall that indexes BOTH the table key (Enchanting hit) AND the storedrd.spellIdfield (every other profession's hit). O(N) one-time build, O(1) lookup, works for all professions. The v0.4.7 fix is preserved on top — the new index iterates every bucket too. Location: GUI/MissingRecipesTab.lua.
UX Changes
- Missing Recipes tab now surfaces recipes even when we don't have a known source for them — previously the tab filtered out any recipe without a
sourceDBentry on the assumption that the recipe wasn't actually obtainable. With v0.5.0's authoritativerecipeDB(sourced from Blizzard's DBC), that assumption is wrong: we KNOW the recipe exists in the game even when no emulator we read from catalogs the NPC. Recipes without source data now display with an "Unknown" source tag (L["MissingSrcUnknown"]) instead of being hidden, so the user still sees "I'm missing this" and can look it up externally. This is the main lever closing the visibility gap for MoP-introduced recipes (since we don't have a MoP emulator source yet). The "Include trainer-only" toggle still hides trainer-only entries when off — that's an unchanged UX choice tied to the tab's AH-hunting use case. Locations: GUI/MissingRecipesTab.lua, Locale/enUS.lua.
New Features
Recipe database rebuilt from authoritative sources — 6,014 recipes (up from ~2,479 in the previous PM/PS-derived ports) across TBC / Wrath / Cataclysm / Mists of Pandaria — the static
recipeDBwas Vanilla-only inherited from a PersonalShopper port in v0.3.0; intermediate v0.4.x patches considered porting from the ProfessionMaster addon's crowd-sourced data but on per-profession audit PM was missing 44% of the recipe universe (e.g. Cata Tailoring: 540 recipes in Blizzard's DBC vs 292 in PM, +248 missed; MoP Leatherworking: 952 vs 306, +646 missed). v0.5.0 replaces the PM path entirely with a two-phase Python pipeline (dev-only, not packaged) that pulls authoritative data directly from Blizzard:- Phase A — Recipe metadata from wago.tools' API, which exposes Blizzard's actual client-shipped DB2 tables for every Classic build. We hit one build per expansion (Vanilla 1.15.8, TBC 2.5.5, Wrath 3.4.5, Cata 4.4.2, MoP 5.5.3) and join
SkillLineAbility→SpellName→SpellReagents→ItemEffect→ItemSparsefor each profession's skill line. Output: recipe name, difficulty thresholds (orange / yellow / green / grey), required skill, reagent list (item id → count), and the item that teaches the spell (recipe scroll). Union-merged across all 5 expansions, latest-wins on rebalanced difficulty. - Phase B — Source data from emulator world DBs. Trainer NPCs from AzerothCore WotLK (
trainer_spell×creature_default_trainer) and TrinityCore Cata Classic TDB (trainer_spell×creature_trainer). Vendor NPCs from each emulator'snpc_vendor. Drop NPCs fromcreature_loot_templatewithreference_loot_templatechain expansion. Game-object drops (nodes / chests) fromgameobject_loot_templatewith the same chain expansion. The TrinityCore Cata TDB 7z is downloaded once (~60 MB) and stream-filtered to extract only the 8 tables we need, cached attools/emulator_data/trinity_cata_*.sql. Same approach for AzerothCore via raw GitHub SQL downloads. Recipe item ids from Phase A drive the reverse lookup that converts item-keyed source rows into spell-keyed source entries.
Total: 3,696 recipes now ship with at least one source entry (61.5% of the 6,014 recipe universe). Per-profession sourced count / total: Blacksmithing 570/934, Tailoring 462/732, Leatherworking 569/1063, Jewelcrafting 661/995, Enchanting 346/454, Inscription 276/653, Engineering 314/445, Alchemy 248/375, Cooking 200/258, First Aid 21/34, Mining 28/57, Fishing 1/14. Per source kind: 1,780 trainer entries, 1,197 vendor, 796 drop (creature + game-object), 355 container (clams / lockboxes / Crafty Quest Reward Boxes), 74 quest. The remaining ~2,300 recipes lacking sources are MoP-only (no maintained MoP emulator yet; deferred to v0.5.1), have specialization gates we don't yet parse, or sit in emulator-uncatalogued world-drop tables. Data files ship at ~8 MB total (1.4 MB largest, Blacksmithing sources) — Blizzard-authoritative comes with size weight.
New TOC entries:
Data/Recipes/Inscription.lua,Data/Recipes/Jewelcrafting.lua, and matchingData/Sources/files. Wired into TBC (Jewelcrafting only), Wrath / Cata / MoP (both). Vanilla TOC unchanged — neither profession existed pre-TBC. New dev tools: tools/wago_probe.py, tools/build_authoritative_data.py, tools/build_authoritative_sources.py; existing tools/extract_emulator_trainers.py extended to cover 7 new TDB tables (vendor, loot, container, reference, quest, creature, item_loot) AND a rewritten SQL row parser that's now string-quote-aware — the original regex-based tuple splitter bailed on the first(or)inside a quoted column (whichquest_templateis full of: descriptions like$N (level X)), parsing only 248 of AC Wrath's 9,464 quest rows; the new state-machine walker handles every row cleanly. Locations: Data/Recipes/, Data/Sources/, tools/, all 4 expansionTOGProfessionMaster_*.tocfiles.- Phase A — Recipe metadata from wago.tools' API, which exposes Blizzard's actual client-shipped DB2 tables for every Classic build. We hit one build per expansion (Vanilla 1.15.8, TBC 2.5.5, Wrath 3.4.5, Cata 4.4.2, MoP 5.5.3) and join
Known Gaps
- MoP-specific trainer / vendor / drop data is absent — TrinityCore dropped active MoP support after TrinityCore 5.4.8, so we'd need to locate a maintained MoP emulator fork (PandaCore or similar) to extract its world DB. MoP recipes are present in the recipe DB (so they appear in Browser and Cooldowns), but they won't surface in the Missing Recipes tab without a source entry. This is the single biggest contributor to the remaining 38.5% source-gap and the v0.5.1 priority.
- Specialization-locked recipes —
data.specializationfiltering inBuildMissingListworks against a field we don't yet populate, so specialization gates are effectively off (a non-Transmute Master Alchemist still sees Master Transmute recipes as "missing"). Cosmetic, not functional. - Some "world drop" recipes still uncatalogued — Blizzard puts a chunk of recipe scrolls in generic world-drop loot tables that emulators reference but don't always populate at the leaf level. A future pass on
reference_loot_templatechain depth may close some of this gap.
[v0.4.7] (2026-05-21) - Gear + help-i icons bleeding into other AceGUI addons
Bug Fixes
- Help "i" icon and Settings gear icon bleeding from TOGPM into other AceGUI-based addons (TOGBankClassic, PersonalShopper, Grouper, etc.) — both icons are raw
CreateFrames parented to the MainWindow's AceGUI Frame at GUI/MainWindow.lua:214 and GUI/MainWindow.lua:313. TheOnClosecallback at GUI/MainWindow.lua:192 correctly calledaddon.GUI.DetachPoolon both icons beforeAceGUI:Release, BUTOnCloseonly fires when the user clicks the X button. Any other exit path — the ESC key (line 119), the/togpmtoggle slash command, or any programmaticMainWindow:Close()— routed throughMainWindow:Closeat line 477 which calledAceGUI:Release(self.frame)directly without detaching. AceGUI then recycled the frame (with our icons still parented) into the nextAceGUI:Create("Frame")caller. Fix: extracted the detach + release logic into a sharedMainWindow:_ReleaseFrame(widget)helper that both theOnClosecallback andClose()now route through, so the icons areHide()+SetParent(UIParent)+ClearAllPointsregardless of which close path the user takes. The DetachPool helper itself was correct (no change to GUI/SharedWidgets.lua) — the bug was that one of the two close paths skipped it entirely. Location: GUI/MainWindow.lua.
[v0.4.6] (2026-05-21) - MoP cross-bucket known-recipes fix + packaging cleanup
Bug Fixes
- Recipes the user already knows showing up in the "Missing" list on MoP —
BuildMissingListpinned its known-recipes lookup to a single guild bucket viaaddon:FindBucketForChar(charKey, "skills"), but a character's recipes can live in a different bucket than where their skills happened to be cached (e.g., skills in the syntheticNoGuildBucketKeyfrom a brief no-guild moment while the latest scan landed in the current guild's bucket; or stale skill data in an old guild bucket while current scans are elsewhere). Pinning to one bucket caused recipes the player actually has to show as "missing". Fix: rewriteknownByCharto walk EVERY bucket inaddon.guildDb.global.guildsand return true the moment ANY bucket has this charKey as a crafter for this recipe. Same correctness logic the Cooldowns/Browser tabs already use in their "My Characters" views.skillMaxlookup also rewritten to walk all buckets and take the max. Location: GUI/MissingRecipesTab.lua.
Improvements
.pkgmetacleanup — added.claude/,CLAUDE.md,.markdownlint.json,.markdownlintignore, andtools/to the package-ignore list so dev tooling and AI-assist guidance files don't ship in the CurseForge package. Removed a redundant*.batentry (already covered by**/*.bat).
[v0.4.5] (2026-05-20) - Modern Auction House API support (Cata / MoP) + TBC tooltip fix
Bug Fixes
Crafter line missing from item tooltips on TBC Anniversary — the tooltip-hook setup had two paths, a modern
TooltipDataProcessor.AddTooltipPostCallpath (Cata / MoP / Retail) and a legacyOnTooltipSetItemscript path (pre-modern-engine Classic). On TBC Anniversary, the modern API doesn't behave the same way and the legacy script doesn't fire on the modern client engine, so neither path triggered. Result: hovering an item showed no[TOGPM] crafters: ...line. Fix: added a third universal fallback that useshooksecurefunc(GameTooltip, "Show", ...)plusGetItem()— fires after the tooltip is fully populated, works on every Classic and Retail client. Also tightened the de-duplication: the previous check (if tooltip._togpmAppended then return end) was buggy because on the modern client engine,OnTooltipClearedmay not fire between tooltips, leaving the flag set from a previous item and silently swallowing every subsequent hover. The check is nowif tooltip._togpmAppended == itemID then return end— bails only when THIS item was already appended to THIS tooltip frame. Added/togpm debuginstrumentation showing which tooltip path was wired up atPLAYER_LOGIN. Location: Tooltip.lua.Auction House integration broken on Cata Classic and MoP Classic — our
Modules/AHScanner.luaused only the legacyAuctionFrame+QueryAuctionItemsAPI. Cata Classic and MoP Classic (and Retail) use the modernC_AuctionHouseAPI withAuctionHouseFrame, so all the[AH]buttons, "Scan AH" controls, and price lookups silently no-op'd on those clients. Vanilla, TBC Classic, and Wrath Classic still use the legacy path and were unaffected. Fix: feature-detect at file-load (AH._isModernAH = C_AuctionHouse and C_AuctionHouse.SendSearchQuery ~= nil and C_AuctionHouse.MakeItemKey ~= nil) and branch every dispatch path.AH.IsOpen()checksAuctionHouseFrame:IsShown()on modern;AH.SearchFor(itemName)usesAuctionHouseFrame.SearchBarinstead ofBrowseName+AuctionFrameBrowse_Search; the scan loop usesC_AuctionHouse.SendSearchQuery(MakeItemKey(itemId), {}, false)instead ofQueryAuctionItems; result collection branches betweenITEM_SEARCH_RESULTS_UPDATED(unique items) andCOMMODITY_SEARCH_RESULTS_UPDATED(commodities — most reagents). NewAH._completeCurrentItem(listings)helper holds the post-scan dispatch logic so both API paths converge on the same continuation. Modern AH's built-in server throttling makes the legacyCanSendAuctionQuery()gate irrelevant on that path, but the version-aware_scanDelay+ Settings override carries over so users on either API can tune scan pacing. Safety timer (delay + 5s) advances the scan if no result event fires, protecting against item-cache misses on modern. Location: Modules/AHScanner.lua.
[v0.4.4] (2026-05-19) - Cross-profession sync gap + AH scan stall + Poisons removed
Bug Fixes
Recipes for professions the local character doesn't have were never received from guildmates who do — the DeltaSync OFFER protocol is broadcaster-driven: a peer only offers data for keys that appear in the broadcaster's hash list. A player who has Enchanting / Tailoring locally syncs that data fine (mismatched hashes → offer → fetch), but never receives Engineering / BS / LW recipes from guildmates who have them, because the broadcaster has no
recipemeta:202/crafters:202entry to advertise. Confirmed by /togpm status on a TBC Anniv user showing 7 profession buckets locally andcatchUpCycles=5(max retries exhausted) with zero sync events for profIds 164 / 165 / 202 across an entire debug log. Fix: newHashManager:PadMissingProfessionPlaceholders(DS, map)injects a placeholder hash entry (the stable hash of an empty table) for every available crafting profession the local player has no content for. Peers see the placeholder mismatch their real hash, offer, we accept, we merge — and on the next broadcast the real computed hash naturally replaces the placeholder. Placeholders live only in the broadcast map, not ingdb.hashes, so they don't conflate "I want this" with "I have this." Wired into both broadcast sites (getMyHashesinInitDeltaSyncandScanner:BroadcastHashes). Location: Modules/HashManager.lua, Scanner.lua.AH scan stalled after the first item on TBC / Wrath / Cata / MoP (Classic Era was unaffected) — debug logs from a TBC Anniversary user showed query 1 completing instantly, query 2 firing, then no
AUCTION_ITEM_LIST_UPDATEever arriving for it. The 1.5s scan delay from v0.4.3 was tuned to Classic Era's looser server throttle; TBC and later servers enforce a stricter window, and the second query was being silently dropped by the rate limiter without firing a response event, stalling the scan. Three-part fix: (1) the scan delay is now version-aware — defaults to 1.5s on Classic Era / Anniversary (where it always worked) and 3.0s on TBC / Wrath / Cata / MoP (matches what other Classic AH-scan addons use as a safe default). (2) EveryQueryAuctionItemscall is gated onCanSendAuctionQuery()returning true — when the API says throttled we put the item back at the front of the queue and retry in 0.5s slices until it agrees. This handles the case where the default delay still isn't enough for a particular server's current throttle state. (3) New Settings entry "AH scan delay (seconds)" lets users override the version default in the range 0.5–10s — empty / 0 / "off" uses the version-appropriate default. Lets each guild dial in the right value for their server. Location: Modules/AHScanner.lua, GUI/Settings.lua, TOGProfessionMaster.lua, Locale/enUS.lua.
Improvements
- Poisons profession removed entirely — Poisons (skill 40) was Rogue-only and became automatic-from-trainer in Wrath 3.1, so the data was dead weight on Vanilla / TBC and irrelevant on Wrath / Cata / MoP. Removed from
addon.PROF_NAMES,addon.PROF_AVAILABILITY, andaddon.CRAFTING_PROFS; deletedData/Recipes/Poisons.luaandData/Sources/Poisons.lua; dropped the two Poisons load lines from all five TOC files (vanilla,_TBC,_Wrath,_Cata,_Mists); updated docstring references in Browser and Missing Recipes tabs. Location: TOGProfessionMaster.lua, GUI/BrowserTab.lua, GUI/MissingRecipesTab.lua, all TOC files.
[v0.4.3] (2026-05-19) - Profession Browser "My Characters" filter walks all buckets + shared bucket-walk helpers
Bug Fixes
- "My Characters" filter on the Profession Browser tab still ignored alts outside the current guild after v0.4.2 — v0.4.2 fixed the same bug on the Cooldowns and Missing Recipes tabs but didn't carry the pattern over to the Browser.
BuildRecipeListwas still readinggdb.recipesfrom the current guild bucket only, so a recipe that an alt in Guild B (or in no guild) could craft never appeared in Main-in-Guild-A's "mine" view. No data migration is needed — the existing scans are already keyed correctly per bucket. Fix: newCollectRecipesForView(viewMode)helper that, in"mine"mode, walks every bucket inaddon.guildDb.global.guildsand unions the per-(profId, recipeId) crafter sets across buckets. First bucket's metadata wins on duplicate recipe rows; crafters from every bucket are unioned. In"guild"mode and any other view mode the helper returns the current bucket's recipes table unchanged, so guild-view behavior is identical to v0.4.2. Local read only — no sync-protocol implications. Location: GUI/BrowserTab.lua.
Improvements
- Shared cross-bucket walk primitives in
addonnamespace — v0.4.2 introduced three near-identical bucket-walking helpers (CollectCooldownsByCharin CooldownsTab,FindCharBucketin MissingRecipesTab, plus an inline walk in the transmute popup) and v0.4.3 was about to add a fourth (CollectRecipesForViewin BrowserTab). Promoted the common pattern to two reusable primitives in TOGProfessionMaster.lua:addon:ForEachGuildBucket(callback)iterates every bucket inaddon.guildDb.global.guilds, andaddon:FindBucketForChar(charKey, field)returns the first bucket whosefieldsub-table contains an entry for charKey (works for any per-character sub-table —"skills","cooldowns","specializations", etc.). All four call sites refactored to use them: the Browser'sCollectRecipesForView, the Cooldowns tab'sCollectCooldownsByCharand transmute-popup lookup, and the Missing Recipes tab'sGetCharactersWithProfessions/GetProfessionsForCharacter/BuildMissingList. Future tabs that need the same access pattern can hit the helpers directly instead of reinventing the iteration. Location: TOGProfessionMaster.lua, GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.
[v0.4.2] (2026-05-18) - Dynamic broadcast debounce + "My Characters" filter works for cross-guild and guildless alts
Bug Fixes
- "My Characters" filter on Cooldowns and Missing Recipes tabs only showed alts that were in the same guild as the currently-logged-in character — the whole point of the filter was account-wide visibility, but both tabs only read from
addon:GetGuildDb()(the current guild's bucket). An alt in a different guild had their cooldowns/skills stored in that guild's bucket, invisible here. An alt with no guild scanned nothing at all because the scanner early-returned onif not addon:GetGuildKey() then return end. Fix has three parts: (1)addon:GetGuildDb()now falls back to a syntheticaddon.NoGuildBucketKey = "__noguild"bucket when the player has no guild, so guildless scans always have somewhere to write. (2) Every broadcast helper (BroadcastHashes,BroadcastLeafToGuild,BroadcastSubhashesToGuild) now gates explicitly onaddon:GetGuildKey()returning a real (non-nil) value as belt-and-suspenders protection — the synthetic bucket's contents never reach the wire. (3) The Cooldowns tab in"mine"view walks every bucket inaddon.guildDb.global.guildsand merges rows for own characters (latestexpiresAtwins on duplicates from stale buckets); the transmute-group popup uses the same bucket walk. The Missing Recipes tab gets aFindCharBucket(charKey)helper that does the same walk soGetCharactersWithProfessions,GetProfessionsForCharacter, andBuildMissingListall surface cross-guild and guildless alts. Sync protocol is unchanged: only real-guild data ever crosses the wire. Location: TOGProfessionMaster.lua, Scanner.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.
Improvements
- Broadcast debounce now scales with the number of addon users in the guild — the static 30s floor on
Scanner:BroadcastHasheswas sized for busy guilds, but it suppressed legitimate post-scan recipe broadcasts in any guild small enough that the 10-min periodic catch-up tick couldn't reliably reach an online peer (the bug v0.4.1 worked around with a MoP-only static 3s value). Replaces the MoP gate with a VersionCheck-1.0-driven recount: atInitDeltaSyncand at the top of every 10-min tick, firesVC:FireBatch()and 21 seconds later (after the VC10_REQ broadcast + 8s jitter window + 12s collect period have all settled) setsScanner._broadcastSeconds = max(3, min(30, addonUsersOnline)). Linear scale: a 2-person test guild gets a 3s floor so recipe hashes propagate within seconds; a 30+ active-addon-user guild keeps the original 30s ceiling to protect the GUILD channel from saturation. The new count from each tick's batch applies to the next 10-min cycle; the current cycle uses whatever the previous recount produced (steady state converges in one tick). Falls back to the 30s default until the first recount lands, and degrades to the default if VersionCheck-1.0 isn't loaded. Helper:Scanner:ScheduleAddonUserRecount. Location: Scanner.lua.
[v0.4.1] (2026-05-18) - MoP profession availability + MoP guild sync fixes
Bug Fixes
Jewelcrafting and Inscription missing from profession dropdowns on MoP —
addon.PROF_AVAILABILITY[755]and[773](the JC / Inscription availability predicates) referencedaddon.isMists, but Compat.lua only definesaddon.isMoP. On a MoP clientisCatais false andisMistsis nil, soIsProfessionAvailable(755 / 773)returned false and every dropdown that filters via that helper dropped both professions. Fix: rename both references toaddon.isMoPso the predicate matches the flag actually set at load time. Other professions were unaffected (they have noPROF_AVAILABILITYentry and default to always-available). Location: TOGProfessionMaster.lua.Recipe sync between MoP peers silently blocked by broadcast debounce —
Scanner._broadcastSeconds = 30puts a 30s floor onScanner:BroadcastHashes, intended as a guard against bursty rapid-fireScheduleBroadcastcalls. On TBC/Classic/Cata with larger guilds, the 10-min periodic catch-up tick at Scanner.lua:405-409 reliably lands while some peer is online, so recipe hashes eventually escape. On MoP Classic with smaller guilds, peers rarely overlap during the 10-min window, and the post-scan broadcast (which is what carries newly-invalidatedrecipemeta:<profId>/crafters:<profId>hashes) gets suppressed for the entire 30s wall after every successful broadcast. Result: per-character cooldown sync works (those leaves are keyed bycharKey, so each broadcaster's first login hash list carries them cleanly), but guild-wide recipe leaves never propagate between MoP peers — both sides only ever see their own scans. The v0.2.0 protocol's offer/handshake dance requires the requesting side's hash list to reach the provider so the provider can WHISPER an OFFER back; with broadcasts suppressed, the dance never starts. Fix: gate the debounce onaddon.isMoP— 3s on MoP (enough to coalesce TRADE_SKILL_SHOW + TRADE_SKILL_UPDATE pairs beyond the 0.5sScheduleBroadcastcoalescer), 30s elsewhere. Thecount == 0differential check at Scanner.lua:1287-1290 already provides content-based throttling, so a 3s floor is purely a burst guard. Location: Scanner.lua.
[v0.4.0] (2026-05-18) - Cooldown-ready alerts + Cooldowns view filter + settings gear + scroll-pool / tab-pool fixes
New Features
Per-row "!" cooldown-ready alarm on the Cooldowns tab — same "!" toggle pattern as the Profession Browser's shopping-list alert, but only renders on your own characters (gated on
addon:IsMyCharacter(row.charKey)). Cyan when armed, grey when off. When the cooldown becomes ready, the alarm prints a chat line and playsSOUNDKIT.ALARMCLOCKWARNING_3(5274) — deliberately distinct from the existing gold-coloured crafter-online alert +PlaySound(878)so the two are immediately distinguishable in the heat of guild chat. Toggling "!" on a cooldown that is already ready fires immediately so the user gets a confirmation ping. New engine module: Modules/CooldownAlerts.lua, 30-second polling tick driven byAce:ScheduleRepeatingTimer; armed-set persisted inAce.db.char.cooldownAlertskeyed bycharKey:spell:<id>/charKey:transmute/charKey:group:<groupKey>with metadata captured at toggle time. Login-time pre-stamp avoids the burst of pings on UI reload. Location: GUI/CooldownsTab.lua, Modules/CooldownAlerts.lua, Locale/enUS.lua, TOGProfessionMaster.lua.Recurring cooldown-ready reminders — new setting "Cooldown ready reminder" (free-text minutes, 0/empty/
offdisables, validated 1–1440 range with friendly error). When set, every armed cooldown re-fires the alarm every N minutes while the cooldown stays ready, anchored to the first-fire wall-clock time so the cadence stays regular even if the first ping was delayed (login, instance exit, etc.). Crafting clears the dedup clock so the next ready transition fires a fresh first alert. Implemented by replacing the dedup boolean with a_lastFiredAt[key]timestamp inCooldownAlerts:Check— same value gates both the initial fire and the recurring re-fire window. Location: Modules/CooldownAlerts.lua, GUI/Settings.lua."Mute alerts in instances" setting — defaults ON. When the user is inside any
IsInInstance()instance (raid / dungeon / battleground / arena / scenario) the alarm stays silent. Pending alerts fire the moment the user steps out —Checkdoesn't set_lastFiredAt[key]while suppressed, so the next un-suppressed tick treats it as fresh. Capital cities and sanctuaries are deliberately NOT suppressed: the user still gets pinged while AFK in Stormwind. Location: Modules/CooldownAlerts.lua, GUI/Settings.lua.Guild / My Characters dropdown on the Cooldowns tab — mirrors the Browser tab's view-mode dropdown so the two tabs behave the same way when the user wants to focus on their own cooldowns. Sits in the toolbar after the Profession / Cooldown filter dropdowns. Filter applied in
FillRowsafter the profession+cooldown filter pass so both compose naturally; the same view filter also gates Scan AH'sgetItemsso the scan only walks reagents the user can actually see in the list. State session-only (CooldownsTab._viewMode), matching the existing profession/cooldown filter pattern. Location: GUI/CooldownsTab.lua.Settings gear icon on the main window — between the help "i" icon and the AceGUI Close button. 20×20 Button with
Interface\Icons\Trade_Engineeringtexture + ButtonHilight-Square highlight, anchored to helpIcon's BOTTOMRIGHT + (3, 2) for vertical centring against the 24-tall help icon. Click opens the AceConfig settings panel viaaddon:OpenSettings()— same target as/togpm settingsand Shift+left-click on the minimap button. Wraps the open call in aTOGPMEscProxysave-and-restore so the Blizzard Settings panel'sCloseSpecialWindows()doesn't slam the main window shut when the panel opens. Bottom strip layout:[status text...(-183)] -6- [help 24] -3- [gear 20] -3- [Close 100 (-27)]; statusbg's right edge moved from -163 to -183 to make room. Detached inOnCloseviaaddon.GUI.DetachPoolso it doesn't bleed into the next addon that acquires the recycled AceGUI Frame. Location: GUI/MainWindow.lua.
Bug Fixes
Cooldowns tab showed "Veil of Shadow" instead of "Salt Shaker" — the Salt Shaker item-based cooldown is stored under its item ID
15846, but spell ID15846is a generic NPC ability named "Veil of Shadow."BuildRowsresolvedcdNameviadata.cooldowns[spellId] or GetSpellInfo(spellId) or GetItemInfo(spellId) or tostring(spellId), andGetSpellInfo(15846)won the lookup chain beforeGetItemInfo(15846)could be tried. Fix: special-casespellId == data.saltShakerItemto go straight toGetItemInfo. Location: GUI/CooldownsTab.lua.Open profession dropdown closed on every guild sync (3-second cadence in active guilds) —
GUILD_DATA_UPDATED→MainWindow:QueueRefresh→tabs:ReleaseChildren()→ AceGUI released the toolbar's Dropdown widget mid-interaction, which closes its pullout. Fix: new helperaddon.GUI.IsAnyDropdownPulloutOpen()walks AceGUI's global pullout pool (_G["AceGUI30Pullout"..N]:IsShown()) andMainWindow:Refreshdefers the redraw by 250ms when one is open, re-queuing itself until the user dismisses the pullout. Same defense protects Cooldowns / Browser / Missing dropdowns for free. Location: GUI/MainWindow.lua, GUI/SharedWidgets.lua.Cooldowns tab scrollbar disappeared after switching from Browser — Browser's virtual-scroll trick (
scroll.LayoutFinished = function() endto prevent AceGUI from overwriting its manually-setcontent:SetHeight(#recipes * ROW_HEIGHT)) installs the override on the widget INSTANCE. AceGUI'sScrollFramestores methods as instance fields (Constructorcopies themethodstable directly onto each widget), so the override survivesAceGUI:Releaseand follows the recycled widget into whichever tab acquires it next. When Cooldowns received the polluted scroll, its AceGUI children laid out fine but the no-opLayoutFinishedpreventedcontent.heightfrom being set,FixScrollsaw viewheight==0, hid the scrollbar. Fix:PersistentScroll.Acquire(the new shared helper) captures the originalLayoutFinishedfrom a freshly-pulled scroll once per session and restores it on every acquire — Browser's no-op override is wiped before Cooldowns / Missing see the widget. Location: GUI/SharedWidgets.lua.Cooldowns tab scrollbar ALSO disappeared after Missing → Cooldowns — different root cause, same symptom. Missing installs
container.LayoutFinished = AnchorAllon the TabGroup container to drive its custom section + scroll-frame anchoring. The TabGroup widget stays alive across tab switches (only its children get released), so Missing'sAnchorAllclosure persisted on it and fired during Cooldowns' subsequent layout passes. The closure referenced Missing's staleself._scroll.frame/self._headerFrame— and because the AceGUI ScrollFrame pool recycled Missing's released scroll widget into Cooldowns,AnchorAllre-anchored Cooldowns' (same widget instance) scroll frame to Missing's hidden header / section frames, collapsing the viewport to ~0 px → no scrollbar. Fix:MainWindow:DrawTabclearscontainer.LayoutFinished = nilbefore every tab Draw, wiping the previous tab's override. Missing / Browser still re-set their own anchoring overrides as part of their own Draw. (Earlier attempt to restore the class default failed because TabGroup's classLayoutFinishedshrinkscontentduring intermediateAddChildpasses viaOnHeightSet, which collapsed Missing's section to 0 px and made the rows vanish — see the next entry.) Location: GUI/MainWindow.lua.Missing tab rows disappeared after the scroll-pool fix above — Missing had silently been free-riding on Browser's
scroll.LayoutFinished = function() endpollution. AceGUI'sFixScrollcallsself:DoLayout()on every scrollbar visibility transition (AceGUIContainer-ScrollFrame.lua:108/118), andDoLayoutruns the scroll's "List" layout — which callsscroll.LayoutFinished(_, _, 0)because Missing's scroll has no AceGUI children (raw frame pool insidescroll.content). The class defaultcontent:SetHeight(0 or 20)was clobbering Missing's manually-setscroll.content:SetHeight(#list * ROW_HEIGHT)whenever the scrollbar toggled. Pre-fix this didn't manifest because Browser's pool pollution made Missing's scroll silently inherit the no-opLayoutFinishedwhenever the user had visited Browser first. AfterPersistentScroll.Acquirestarted restoring the class default on every acquire (correct for Cooldowns, which DOES have AceGUI children), Missing had to install its own no-op explicitly — matches Browser's virtual-scroll pattern. Location: GUI/MissingRecipesTab.lua.Missing tab scroll position resetting on every redraw — same
_scrollStatuspersistence pattern that Cooldowns and Browser already use, applied to Missing via the new shared helper. Includes dropdown / filter / character changes — your scroll position now survives every redraw. Location: GUI/MissingRecipesTab.lua via GUI/SharedWidgets.lua.
Improvements
addon.GUI.PersistentScrollshared helper — three primitives (Acquire(tab, opts),Restore(scroll, saved, afterFn?),Reset(tab, scroll)) replacing the previously-duplicated scroll-status plumbing inBrowserTab,CooldownsTab, andMissingRecipesTab(was ~25-40 lines per tab).Acquirecaptures the savedscrollvalueinto a local BEFORE any synchronousFixScrollclobber, resets the status table to zero soFixScroll's implicit writes are harmless, then re-attaches the persistent status;RestoredoesSetScroll(saved)+ scrollbar visual nudge + optionalafterFncallback for virtual-pool tabs that need to callUpdateVirtualRows;Resetis the explicit jump-to-top primitive for filter-change call sites. Also restores the captured classLayoutFinishedon every acquire (see the Cooldowns scrollbar fix above). Location: GUI/SharedWidgets.lua.Locale label fixes for truncation — "Suppress cooldown alerts in instances" (38 chars) → "Mute alerts in instances" (24 chars, matches neighbouring "Suppress alerts on login"). "Cooldown ready reminder (minutes)" (33 chars) → "Cooldown ready reminder" (23 chars), with units moved into the description so the tooltip carries the "1–1440 minutes (24 hours)" detail. Dropped the now-redundant
SettingsCooldownReminderUsageline so the AceConfigDialog tooltip consolidates onto a single description line. Location: Locale/enUS.lua, GUI/Settings.lua.Gear-icon tooltip arrow swapped to ASCII —
→(\226\134\146, U+2192) doesn't render in the tooltip font on Classic Era. Replaced with>like FastGuildInvite uses, wrapped in|cffffd700...|rfor visual hierarchy. Location: GUI/MainWindow.lua.
[v0.3.5] (2026-05-17) - Browser tab scroll position persists across guild syncs
Bug Fixes
- Profession Browser recipe list snapped back to the top every few seconds while scrolling — in any active guild, peers broadcast sync deltas every few seconds; each
SYNC_RECVfiresGUILD_DATA_UPDATED, whichMainWindow:QueueRefreshdebounces into aMainWindow:Refreshcall that doestabs:ReleaseChildren()and re-runsBrowserTab:Draw(). The freshly-acquired AceGUIScrollFramewidget always starts at scroll value 0 (itsOnAcquiredoesSetScroll(0)), so the user got yanked back to the top mid-scroll on every sync.CooldownsTabsolved this in an earlier release with a persistent_scrollStatustable re-attached viaSetStatusTableon each Draw (GUI/CooldownsTab.lua:991), butBrowserTabnever got the same treatment. Fix applies the same persistent-status pattern, with one extra wrinkle:BrowserTab:FillListcallsscroll:FixScroll()synchronously, andFixScrollcallsscrollbar:SetValue(0)on the recycled widget — if the scrollbar's residual value from a previous use was non-zero, that fires the defaultOnValueChanged→SetScroll(0)→ writesscrollvalue=0into our status table, destroying the saved position before the restore code can read it. So the saved scroll value is now captured into a local at the top ofBrowserTab:Draw()BEFORE the scroll widget is created (and before anyFixScrollclobber can happen), then restored viaSetScroll(saved)+UpdateVirtualRows()afterFillListcompletes and content height is set.RefreshList(profession dropdown + search-box changes) explicitly resets to scroll position 0 — a filter change should always show the top of the new result set, not whatever offset the previous list was scrolled to. Location: GUI/BrowserTab.lua.
[v0.3.4] (2026-05-05) - Help-icon widget bleed fix + DetachPool single-frame form + consolidated cleanup
Bug Fixes
Help "i" info tooltip on the main window bleeding into other Ace3 addons — the help icon next to the close button is a raw
CreateFrame("Frame", nil, f.frame)parented to the AceGUI Frame's underlying frame. TheOnClosecallback released the AceGUI Frame back to its widget pool but never detached our helpIcon, so the icon — texture, mouse handlers, and tooltip body that reads "Profession Browser / Cooldowns Tracker / Missing Recipes" — stayed parented to the recycled Frame. The next addon that calledAceGUI:Create("Frame")(any Ace3 config window, BigWigs options, etc.) acquired our Frame from the pool with the "i" icon riding on it, visibly inside their UI and serving up TOGPM's tab-help tooltip on hover. Same class of bug as v0.3.3's shopping-list widget bleed, just on a different raw frame. Fix: stashhelpIcononself._helpIconat creation and route through the sharedaddon.GUI.DetachPoolhelper inside the existingOnClosecallback, beforeAceGUI:Release(_widget). Location: GUI/MainWindow.lua._detailOuter(Browser tab's recipe detail panel) missingClearAllPointson cleanup — the inlineOnReleaseblock at the recipe-scroll widget did:Hide()and:SetParent(UIParent)on the detail panel, but not:ClearAllPoints(). Old anchors (TOPRIGHT → headerBar BOTTOMRIGHT,BOTTOMRIGHT → container.content BOTTOMRIGHT) lingered after the detach. If those anchor target frames were the AceGUI parents being recycled, the detail panel could carry a stale point reference into the next addon's frame layout. Now routed throughaddon.GUI.DetachPoolwhich always does the full Hide + UIParent + ClearAllPoints triple. Location: GUI/BrowserTab.lua.
Improvements
addon.GUI.DetachPoolnow accepts a single raw frame as well as an array — the helper introduced in v0.3.3 only handled arrays of pooled frames (viaipairs). For one-off raw frames parented to AceGUI widgets — like the main window's help icon, the Browser tab's_headerBar, and the detail panel's_detailOuter— call sites had to inline the same Hide + UIParent + ClearAllPoints triple instead of using the helper. Extendedaddon.GUI.DetachPool(poolOrFrame)to detect a single frame bytype(poolOrFrame.Hide) == "function"and apply the same cleanup. Result: every raw-frame cleanup site in the codebase now flows through one function. Location: GUI/SharedWidgets.lua.Consolidated four inline cleanup sites in BrowserTab to use the helper —
BrowserTab:Draw()had a defensive_headerBarcleanup at the top, the recipe-scroll'sOnReleasehad another_headerBarcleanup plus a_detailOutercleanup, all written as 4-5 lineif frame then frame:Hide() ... endblocks. Each replaced with a singleaddon.GUI.DetachPool(self._<frame>)call. The helper handles the nil check, the right cleanup operations, and matches the surroundingBrowserTab:DestroyPool/BrowserTab:DetachShoppingListPool/MissingRecipesTab:DetachPool/MainWindow OnClosecall sites — five places in the addon, one helper. Future raw-frame parented to an AceGUI widget = one-line cleanup. Location: GUI/BrowserTab.lua.
[v0.3.3] (2026-05-04) - Shopping-list widget bleed fix + rank-book filter + addon.GUI.DetachPool helper
Bug Fixes
Missing Recipes tab listed Expert / Artisan rank-up books even when the character's skillMax was already above the rank's cap — Cooking, First Aid, and Fishing have rank-up book entries (
teaches = "Expert"/"Artisan"/ etc.) in our static recipe DB. The build pass had no way to filter them:knownByChar(spellId)andknownByChar(data.teaches)both miss because the rank books aren't ingdb.recipesanddata.teachesis a string, not a spell ID. Result: a First Aid 300 character saw both "Expert First Aid" (raises max 150→225) and "Artisan First Aid" (raises max 225→300) listed as missing — even though the only way they got to 300 was by consuming both. Fix: newRANK_CAPSmap ("Expert" = 225,"Artisan" = 300,"Master" = 375, etc. — exhaustive across all expansion ranks for future-proofing) plus a skip check inBuildMissingListthat hides rank-book entries whose cap is at or below the character'sskillMax. WoW has no API to detect "did the player use this consumable" soskillMaxis the proxy: if it's at the cap, the book must have been used. Location: GUI/MissingRecipesTab.lua:BuildMissingList.Shopping-list rows on the Profession Browser tab bleeding into other Ace3 addons — the shopping-list section's
_slPooland_slReagentPoolare pooled rawCreateFramerows parented to the AceGUI InlineGroup's content frame (line 533/534 inFillShoppingListSection). When the InlineGroup was released back to AceGUI's pool — on tab switch, window close, or empty-list redraw — our pool frames were never detached, so they stayedSetParent'd to the InlineGroup. AceGUI then recycled the InlineGroup into another addon's UI with our rows still attached, visibly bleeding into that addon. Fix: newBrowserTab:DetachShoppingListPool()method wired to the InlineGroup'sOnReleasecallback. Locations: GUI/BrowserTab.lua, CLAUDE.md.
Improvements
- Consolidated pool-detach into one shared helper — the same Hide +
SetParent(UIParent)+ClearAllPointsloop was duplicated in three places (BrowserTab:DestroyPoolfor the recipe scroll, the newBrowserTab:DetachShoppingListPool, andMissingRecipesTab:DetachPool). Extracted toaddon.GUI.DetachPool(pool)in GUI/SharedWidgets.lua. All three call sites now route through the shared function so the next pool we add only requires a one-line OnRelease wiring instead of inventing the cleanup loop again. New CLAUDE.md rule documents the pattern: any tab that parents rawCreateFramerows to an AceGUI widget's content frame MUST registeraddon.GUI.DetachPool(pool)on the parent widget'sOnRelease. Location: GUI/SharedWidgets.lua, CLAUDE.md.
[v0.3.2] (2026-05-04) - Multi-version TOC sync (hotfix)
Bug Fixes
- Lua error on opening the Profession Browser on TBC / Wrath / Cata / MoP clients (
attempt to index field 'GUI' (a nil value)at GUI/BrowserTab.lua:316) —TOGProfessionMaster_TBC.toc,_Wrath.toc,_Cata.toc, and_Mists.tochad been frozen at the v0.2.7 file list and never picked up the v0.3.0 additions. Missing entries on every non-Vanilla TOC: 22Data/Recipes/*.lua+Data/Sources/*.luadata files,Modules/AHScanner.lua,GUI/SharedWidgets.lua, andGUI/MissingRecipesTab.lua. WithoutSharedWidgets.lualoading,addon.GUIwas never created, and BrowserTab's call toaddon.GUI.AttachTooltip(...)blew up the tab. Synced all four non-Vanilla TOCs to match the Vanilla TOC's file list. Locations: TOGProfessionMaster_TBC.toc, TOGProfessionMaster_Wrath.toc, TOGProfessionMaster_Cata.toc, TOGProfessionMaster_Mists.toc.
[v0.3.1] (2026-05-03) - Version-aware profession dropdowns + de-duplicated profession lookup + offline-peer chat spam fix
Bug Fixes
No player named 'X' is currently playing.chat spam when guildmates log out — peers broadcast their L0 hash list on the GUILD channel; our addon (and DeltaSync's internal P2P logic) responded by whispering them OFFER / RequestData messages. If the peer logged out between their broadcast and our reply landing on the wire, the server rejected the whisper and printed the system error to chat. A guild with active sync produced this spam dozens of times per logout event. Two-pronged fix: (1) added aScanner.GuildCache:IsPlayerOnline(sender)gate at everyDS:RequestData(sender, ...)call site in Scanner.lua — three sites total:onSyncAcceptedfor both the subhashes-roll-up and the leaf-data branches, plus the per-character leaf-data request inside the subhashes-response handler. Mirrors the TOGBankClassic pattern inModules/DeltaComms.luawhere every send site has anIsPlayerOnlineguard. (2) Added aCHAT_MSG_SYSTEMfilter viaChatFrame_AddMessageEventFilterthat swallows"No player named X is currently playing."(both quoted and unquoted variants) and"Player not found"system messages — race-window safety net for the gap between our online check and the actual send, plus DeltaSync's own internal sends that we don't directly control. Pattern matches the TOGBankClassic implementation inModules/Events.luaincluding the fast plain-text prefix check before the full pattern match (cheap on the high-volume CHAT_MSG_SYSTEM stream). Trade-off: a manual/wto a player who logged off also won't show the error, accepted because the addon generates orders of magnitude more such errors than user typos do, and the lack of typing response in the chat window already signals the failed send. Location: Scanner.lua:InstallChatFilter / Scanner.lua:Init / Scanner.lua:onSyncAccepted / Scanner.lua subhashes-response handler.Jewelcrafting and Inscription missing from the Profession Browser dropdown on TBC / Wrath / Cata / MoP clients —
BrowserTab.ALL_PROFESSIONSwas a hardcoded static list of 9 Vanilla professions (the comment literally said "Static list of all Vanilla crafting professions"). Users on later clients couldn't filter by JC (added in TBC) or Inscription (added in Wrath) even when guildmates had broadcast recipes for them. Now the Browser dropdown derives from the sharedaddon.PROF_NAMESmaster table filtered byaddon.IsProfessionAvailable(profId)andaddon.CRAFTING_PROFSset, so JC appears on TBC+ and Inscription appears on Wrath+ automatically. Location: GUI/BrowserTab.lua:GetProfDropdownEntries.
Improvements
De-duplicated profession lookup tables —
PROF_NAMESwas previously copy-pasted in 4 places (addon.PROF_NAMESin TOGProfessionMaster.lua, plus localPROF_NAMESin CooldownsTab / MissingRecipesTab, plusALL_PROFESSIONSin BrowserTab) and had drifted out of sync (Cooldowns had JC + Inscription, Missing didn't, Browser had neither). Consolidated into one master table onaddon.PROF_NAMESwith all 16 known professions. Adding a profession or fixing a display name is now one edit instead of four, and the three tabs can't drift apart again. Location: TOGProfessionMaster.lua, GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.Version-aware profession dropdowns across every tab — new
addon.PROF_AVAILABILITYmap declares which client versions a profession exists on (default = always-available / Vanilla). Newaddon.IsProfessionAvailable(profId)helper returns true/false based on the currentaddon.is*flags. Every tab's profession dropdown filters by this:- Jewelcrafting (755) — TBC, Wrath, Cata, Mists only (hidden on Vanilla)
- Inscription (773) — Wrath, Cata, Mists only (hidden on Vanilla / TBC)
- Poisons (40) — Vanilla, TBC only (hidden on Wrath+ since the WotLK 3.1 patch made Rogue poisons automatic)
No data migration: profession IDs and saved selections stay valid; the existing "selected profession not in list → fall back to first available" guards in each tab handle stale per-character state silently. Location: TOGProfessionMaster.lua.
New
addon.CRAFTING_PROFSset — explicit list of professions that produce learnable recipes (i.e. belong in the Browser / Missing Recipes lists). Excludes pure gathering (Herbalism / Skinning / Fishing). Mining stays in because Smelting produces craftable bars. Used by BrowserTab to filter its dropdown; Cooldowns and Missing have their own implicit category filters (cooldown definitions / character skill data). Location: TOGProfessionMaster.lua.
Known Gaps
- Missing Recipes data for Jewelcrafting and Inscription — the dropdown now shows JC on TBC+ and Inscription on Wrath+, but our static
recipeDB/sourceDB(copied from PersonalShopper, which is Vanilla-only) doesn't contain entries for these professions yet. A character with JC / Inscription will see the profession in the dropdown but no scrolls listed. Curating the recipe-scroll universe and source tags for these professions is planned for v0.3.2.
[v0.3.0] (2026-05-03) - Missing Recipes tab + AH scanner + shared widget factories
New Features
New "Missing Recipes" tab — third tab in the main window, modeled on PersonalShopper's Collector. Picks a character from a dropdown (defaults to the currently logged-in toon, includes any tracked alts on the account), then a profession from a second dropdown (filtered to professions that character has scanned), and renders every recipe scroll the character hasn't yet learned. Each row shows the scroll icon, color-coded item name, required skill, and source tags (Vendor / Drop / Quest / Crafted / Container / Fishing). Hover the icon or name for the standard item tooltip (anchored via
addon.Tooltip.Owner); shift-click the row to insert the scroll's hyperlink into the active chat edit box. A[Bank]button appears at the right of each row when TOGBankClassic reports the recipe scroll in stock — click to open the standard bank-request dialog (matches the BrowserTab / CooldownsTab / ShoppingListTab[Bank]pattern). Search box filters by name; "Include trainer-only" checkbox unhides recipes obtainable only from a trainer (off by default to match PS, since trainer-only scrolls aren't AH-buyable). Column headers (count, Skill, Sources) now have hover tooltips explaining each column. Location: GUI/MissingRecipesTab.lua.Embedded recipe-universe database —
Data/Recipes/<Profession>.luaandData/Sources/<Profession>.lua, copied verbatim from PersonalShopper's curated dataset (11 professions: Alchemy, Blacksmithing, Cooking, Enchanting, Engineering, First Aid, Fishing, Leatherworking, Mining, Poisons, Tailoring). The Lualocal _, addon = ...upvalue is per-addon, soaddon.recipeDBandaddon.sourceDBhere are private to TOGPM and don't collide with PS even when both addons are loaded simultaneously. No optional dependency on PS — the tab works fully standalone. Location: TOGProfessionMaster.lua, Data/Recipes/, Data/Sources/.ReagentWatch:IsWatching(itemId)API — sibling toRW:Watch/RW:Unwatch. Returns true when the item is currently on the watch list. Lets the new tab toggle the+button between "add" and "already watching" states without poking at the underlying SavedVariable directly. Location: Modules/ReagentWatch.lua.addon.AHmodule — new shared module that mirrors theaddon.Bankpattern for the auction house.addon.AH.IsOpen()tracks AH visibility viaAUCTION_HOUSE_SHOW/AUCTION_HOUSE_CLOSED;addon.AH.SearchFor(itemName)switches the AH to Browse, populates the name field, clears any narrowing filters (level/quality/usable), and firesAuctionFrameBrowse_Search.addon.AH.StartScan(items, opts)queues{itemId, itemName}pairs and walks them at 1.5s intervals (rate-limit-safe), callingQueryAuctionItemsper item withexactMatch=trueand collecting results fromAUCTION_ITEM_LIST_UPDATE; results cache per itemId for the session. CustomAH_OPEN_STATE_CHANGEDandAH_SCAN_COMPLETEcallbacks (viaaddon.callbacks) let each tab refresh its UI live as scan state changes. Auto-clears scan results when the AH closes (listings go stale fast). Targets Vanilla → MoP via the legacyAuctionFrameUI; retail (8.0+) needs a separateC_AuctionHousepath that's out of scope. Location: Modules/AHScanner.lua.Scan AH on every tab + per-row
[AH]button — Browser, Cooldowns, Missing Recipes, and ShoppingList all gained "Scan AH" toolbar buttons that walk the items relevant to that tab (shopping-list reagents / cooldown reagents / missing recipe scrolls). After scan completes, rows whose item has live listings reveal an[AH]button next to[Bank], gated onaddon.AH.GetListingsFor(itemId).count > 0exactly like[Bank]is gated onBank.GetStock(itemId) > 0. Click[AH]to jump the AH browse search to that item. Toolbar button label flips toScanning N/Mwhile running and click cancels. The Cooldowns tab's[+] Transmutepopup also got per-row[AH]buttons mirroring its existing[Bank]pattern, so multi-reagent transmutes (Arcanite needs Thorium Bar + Arcane Crystal) get one[AH]per reagent row. Locations: GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua, GUI/ShoppingListTab.lua.Cooldowns tab — two-level Profession / Cooldown filter dropdowns — two new AceGUI dropdowns on the toolbar (next to the Ready Only button) let you narrow the cooldown list by profession then by specific shared-timer cooldown. Profession dropdown lists professions that have cooldowns in the current game version; cooldown dropdown is contextual to the selected profession and is hidden when profession is "All". Selecting a different profession resets the cooldown selection to "All" within it. Full taxonomy across all expansions: Alchemy (Transmute group, Alchemy Research), Tailoring (Mooncloth, Specialty Cloth — combined for TBC Spellcloth/Shadowcloth/Primal Mooncloth + Wrath Spellweave/Moonshroud/Ebonweave since they're spec-locked and a single tailor only has one, Glacial Bag, Dreamcloth group, Imperial Silk), Leatherworking (Salt Shaker — filed here because Refined Deeprock Salt is an LW reagent despite the misleading name, Magnificence group), Enchanting (Magic Sphere — Prismatic + Void combined, Sha Crystal), Jewelcrafting (Brilliant Glass, Icy Prism, Fire Prism, JC Daily Cut group), Inscription (Inscription Research group, Forged Documents — both faction variants combined, Scroll of Wisdom), Blacksmithing (Titansteel Bar, Smelting group — Balanced Trillium + Lightning Steel), Engineering (Jard's Energy). For alchemists, all transmute spells across all expansions collapse to a single "Transmute" entry (one shared timer per character) so the dropdown doesn't get janky. Driven by
COOLDOWN_BY_PROFESSIONat the top of GUI/CooldownsTab.lua; the per-professionmatchpredicate is the union of its cooldown entries' matches, so adding a new entry automatically extends both the cooldown dropdown options AND the parent profession's coverage with no UI plumbing changes. Location: GUI/CooldownsTab.lua, Locale/enUS.lua.Per-tab window sizing — Cooldowns and Missing tabs are now LOCKED to a fixed 720×500 window (resize grip hidden) so switching between them produces no visible jump; only the Browser tab is resizable. Browser's last user-chosen size persists separately in
frames.mainWindow.browserWidth/browserHeightso locked tabs can't overwrite it. Each tab declares its size policy asWINDOW_SIZE = { width=W, height=H, locked=true }or{ minWidth=W, minHeight=H };MainWindow:ApplyTabSizereads the spec on Open and on every tab switch. Locations: GUI/MainWindow.lua, GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.Shared GUI widget factories — three reusable factories in a new GUI/SharedWidgets.lua collapse what was previously ~240 lines of copy-pasted plumbing across the three tabs into ~10-line call sites: (1)
addon.GUI.MakeScanAHButton(opts)— owns the Button widget, its label refresh closure, the OnClick handler that drivesaddon.AH.StartScan, the tooltip, and ONE module-level pair of AH callbacks that route via a small_activeButtonsregistry instead of N callbacks accumulating; (2)addon.GUI.MakeColumnHeader(opts)— InteractiveLabel + brand color + no-wrap + optional tooltip + optional sort onClick, enforcing the column-header rule from CLAUDE.md structurally instead of by convention; (3)addon.GUI.AttachTooltip(widget, title, desc)— standard hover tooltip with the Dropdown/EditBox label-area mouse trick built in (those widgets put theirSetLabelfontstring above the body andControl_OnEnterfires only for the body, so hovering the label produced no tooltip; the helper detectswidget.type == "Dropdown" or "EditBox"and adds a wrapper-frame mouse handler viaaddon.AceGUIFrameScripts). Location: GUI/SharedWidgets.lua.
Improvements
Cooldowns tab — fixed-width columns for smooth resizing — the tab originally tried responsive column widths driven by a
WINDOW_RESIZEDcallback, but AceGUI's Flow layout reflowed mid-drag and the user saw rows visibly stacking into 2-3 lines and snapping back when the drag stopped. Switched to fixed widths (Char 140 / Cooldown 360 / Time 80 = 580 total) — same approach as Missing/Browser tabs, which use raw-frame virtual-scroll pools that never reflow. Combined with the per-tab window sizing, the Cooldowns tab is now locked to a single window size, so columns fit perfectly without any responsive math. Removed:ComputeColWidths,GetAvailableWidth, theWINDOW_RESIZEDhandler. Location: GUI/CooldownsTab.lua.Cooldowns tab — ready cooldowns now sort A-Z by name within the ready cohort — every ready row had the same sort value (
-math.huge) under the time column, and Lua'stable.sortis not stable, so the ready cohort shuffled into a different order every redraw. With dozens of crafters running the addon, opening the window saw the same set of ready cooldowns appear in unpredictable orders, which was disorienting. Added a tiebreaker (cooldown name → character name, both ascending) that fires whenever two rows compare equal under the active sort column, so the Ready Only view stays in a stable A-Z order across refreshes. Also stabilises ties under the Character and Cooldown column sorts. Location: GUI/CooldownsTab.lua:SortRows.Tab-routing extended for the new tab —
MainWindow.TAB_DEFSandMainWindow:DrawTab()now include amissingbranch wired toaddon.MissingRecipesTab:Draw(container). The shared help-icon tooltip gains amissingentry describing the filters, trainer toggle, row actions, and source-tag legend so the in-game documentation matches the existing browser/cooldowns help text. Location: GUI/MainWindow.lua.TOC load order extended — 22 new data files (11 recipes + 11 sources) load after
Data/CooldownIds.luaand beforeScanner.lua;Modules/AHScanner.luajoins the modules group;GUI/SharedWidgets.lualoads afterGUI/MainWindow.luaso all tabs can use the factories;GUI/MissingRecipesTab.lualoads afterGUI/ShoppingListTab.lua. Location: TOGProfessionMaster.toc.
Bug Fixes
Cross-addon AceGUI tooltip leak — TOGPM tooltips appearing in OTHER addons' UIs — the toolbar dropdown / search / scan-button tooltips, plus the cooldown row's right-click whisper handler, all set
widget.frame:SetScript("OnEnter", ...)directly on AceGUI widgets. AceGUI clearswidget.events(the SetCallback registry) on Release but does NOT reset raw frame scripts, and AceGUI pools widgets account-wide across every addon — so leftover scripts kept firing in whatever addon next acquired the recycled widget. Two-part fix: (1) newaddon.AceGUIFrameScripts(widget, scripts)helper in GUI/MainWindow.lua installs the scripts AND wires up an OnRelease cleanup that RESTORES the prior script (not nils — many AceGUI Constructors install internalControl_OnEnterdispatchers there that drive the SetCallback registry, and nilling them would breakwidget:SetCallback("OnEnter")for whoever recycles the widget next). (2) Migrated all toolbar tooltip attachments to usewidget:SetCallback("OnEnter")instead of frame scripts — Button, Dropdown, EditBox, CheckBox all wireControl_OnEnterto fire the SetCallback registry, and AceGUI clears the registry on release for free. The frame-scripts helper is now used only for SimpleGroup right-click handlers (which have no native dispatcher). New CLAUDE.md rule documents the pattern. Locations: GUI/MainWindow.lua, GUI/CooldownsTab.lua, GUI/BrowserTab.lua, GUI/MissingRecipesTab.lua, CLAUDE.md.Scan AH button stayed greyed when AH opened on the active tab — each per-tab
AH_OPEN_STATE_CHANGEDhandler called_refreshScanBtnLabelUNCONDITIONALLY before the active-tab early-out, operating on a stale closure that captured a scanBtn from a prior tab visit (released the moment the user switched away). Calling SetText/SetDisabled on a recycled widget either no-op'd or stomped a pooled widget another addon now owned. Fixed by collapsing all three per-tab handlers into ONE module-level pair of handlers inaddon.GUI.MakeScanAHButtonthat route via a small_activeButtons[tabName]registry — only the current tab's live button is touched. Locations: GUI/SharedWidgets.lua, per-tab handlers removed from GUI/CooldownsTab.lua / GUI/BrowserTab.lua / GUI/MissingRecipesTab.lua.Tooltips on dropdown / editbox LABELS (not bodies) didn't fire — Dropdown and EditBox put their
SetLabel("...")fontstring at the TOP ofwidget.frameand the actual interactive body (the dropdown button / input field) BELOW it. AceGUI'sControl_OnEnteris wired only to the body, so hovering the label area produced no callback — the user mouseover on "Profession Filter" / "Search Recipes" labels never showed a tooltip. The newaddon.GUI.AttachTooltipdetectswidget.type == "Dropdown" or "EditBox"and additionally enables mouse onwidget.frame+ wires a raw OnEnter viaaddon.AceGUIFrameScripts— covering the label area while preserving release-time cleanup. Location: GUI/SharedWidgets.lua.Cooldowns transmute popup — multi-reagent transmutes collapsed to one row — the cooldown branch in
BuildRowsused the hardcoded single-reagentdata.transReagentsmap for every cast spellId, then markedseenSpellIdsto block the multi-reagent recipe-DB branch from re-emitting. So Arcanite (Thorium Bar + Arcane Crystal) showed ONE row with whichever reagent the hardcoded map happened to list, instead of TWO adjacent rows. Restructured: the cooldown branch now consults the recipe-DB (which captures multi-reagent data from the alchemist's actual trade-skill scan) FIRST, falling back todata.transReagentsonly when the recipe scan didn't cover that spell. The recipe-DB branch then only runs for spells the cooldown branch didn't already emit. Location: GUI/CooldownsTab.lua:BuildRows transmute group section.Cooldowns tab — Scan AH didn't include transmute reagents —
getItemsiteratedrow.reagentItemId, but transmute group rows havereagentItemId = nilbecause each transmute inside the group has its own reagent (sometimes multiple). The scan only picked up standalone non-transmute cooldowns (Salt Shaker, Mooncloth, etc.) — Arcane Crystal, Thorium Bar, Iron Bar, etc. were never queried. Now iteratesrow.transmuteEntries[].reagentIdfor transmute group rows so the scan covers everything the user can actually craft. Location: GUI/CooldownsTab.lua:MakeScanAHButton getItems.Enchanting recipes broadcast as
? <id>with "Retrieving item information" tooltips, never resolved by/togpm backfill— Enchanting recipeIds are enchant SPELL IDs, butMergeCraftersIntoGdb's stub creation only triedGetItemInfo(recipeId), which returns nil for spell-only IDs. The resulting nameless stub leftisSpell/spellIdunset, andBackfillBogusRecipeNamesthen refused to callGetSpellInfoon it because its spell branch was gated onrd.isSpell == true or rd.spellId(both nil for the stub). BrowserTab's tooltip code fell through toSetHyperlink("item:<id>"), which is exactly what surfaces the "Retrieving item information" message in WoW for IDs the client treats as items it doesn't have cached. Two-part fix: (1)MergeCraftersIntoGdbnow triesGetSpellInfoas a fallback whenGetItemInfofails, populating the stub's name/icon/isSpell/spellId in one shot. (2)BackfillBogusRecipeNames's spell branch now runs unconditionally (bounded only by the bogus-name + numeric-recipeId guards).GetSpellInforeturns nil for non-spell IDs so there are no false positives. Locations: Scanner.lua:MergeCraftersIntoGdb, Scanner.lua:BackfillBogusRecipeNames.
Performance
Missing Recipes tab — virtual-scroll rendering with raw frame pool (35 rows) — initial implementation rendered every visible missing-recipe row as ~6 AceGUI widgets, so a profession with 500+ missing recipes spawned 3000+ AceGUI children and the layout pass froze the WoW client. Even capping at 100 rendered rows still produced 600 widgets — enough to choke AceGUI's layout. Switched the result body to mirror GUI/BrowserTab.lua's pattern: a pool of 35 raw
CreateFramerows parented to the AceGUI ScrollFrame's content frame, repositioned and re-skinned as the user scrolls. Total widget count is now bounded at 35 regardless of list size. Pool is persistent acrossRefreshList(search keystrokes / trainer toggle / profession switch) via a newDetachPoolhelper that re-parents to UIParent on ScrollFrame release rather than destroying frames — WoW frames are session-lifetime and never GC'd, so destroying-and-rebuilding 35 frames on every search keystroke would leak. Location: GUI/MissingRecipesTab.lua.Missing Recipes tab — build pass no longer calls
GetItemInfo— initial implementation calledGetItemInfo(spellId)inside the build loop to filter rows by recipe-scroll prefix; for Tailoring (~3000 recipes) this triggered thousands of simultaneous async cache loads, locking the WoW client for ~20 seconds and trippingScript from "Bagnon" has exceeded its execution time limitwarnings (Bagnon registers forGET_ITEM_INFO_RECEIVEDand re-runs its handler for every cache fill). Restructured: build pass usessourceDBpresence as the recipe-scroll proof (noGetItemInfo), allgdblookups are hoisted out of the per-recipe loop. Per-rowGetItemInforuns only insideUpdateVirtualRowsfor the 35 currently-visible rows, so cache-miss volume is bounded by the visible window. A debouncedGET_ITEM_INFO_RECEIVEDhandler refreshes the list 0.5s after the cache-fill burst settles so placeholder names get replaced once items finish loading. Search filtering usesGetItemInfoInstant(does not trigger async loads) so typing in the search box never re-fires the cache-miss firestorm. Search keystrokes are debounced 200ms so each character typed doesn't trigger a full rebuild. Location: GUI/MissingRecipesTab.lua.
[v0.2.7] (2026-05-01) - Transmute popup overhaul: known-transmute list, multi-reagent rows, anchored positioning
New Features
The transmute group popup now lists every transmute the alchemist knows, not just the ones currently on cooldown — clicking the
[+] Transmuterow used to show only the spells with active cooldown records, which meant if Alice had cast Earth-to-Water 3 hours ago and you wanted to send her materials for Iron-to-Gold (which she hadn't cast recently and so had no cooldown record), the row simply wasn't there. The popup now augments the cooldown-derived spell list with every transmute recipe ingdb.recipes[171]wherecrafters[charKey]=true— the alchemist's full learned-transmutes set. Each row shows time-remaining when on cooldown, "Ready" in green otherwise, with its own reagent label,[Bank], and mail icon. Location: GUI/CooldownsTab.lua:BuildRows transmute group section.Multi-reagent transmutes render as one row per reagent — Arcanite Bar requires both 1 Thorium Bar and 1 Arcane Crystal; previously the popup collapsed it into a single row showing only one reagent because the hardcoded
data.transReagentsmap only stores one reagent per spell. Now each transmute emits one row per reagent from the alchemist's actualrd.reagentsscan, with name and time-remaining shown only on the first row of the group so the visual grouping stays clean. Each row's[Bank]and mail buttons act on its own reagent independently — useful when you have one reagent in stock but not the other. Location: GUI/CooldownsTab.lua:ShowGroupPopup, entry-build path.addon.Tooltip.AnchorFrame(frame, source)helper — sibling toaddon.Tooltip.Owner(Compat.lua). Anchors any popup/dialog to a source widget using the same screen-half logic that places GameTooltips: source in upper half → popup below source; source in lower half → popup above source. Accepts either a raw Frame or an AceGUI widget (unwraps viawidget.frame). The transmute popup uses this to sit adjacent to the row that opened it instead of centering on the screen — the user can mouse straight onto it without losing context. Reusable for any future click-popup that wants tooltip-like positioning.
Bug Fixes
Transmute popup appeared centered on the screen far from the row that opened it —
popup:SetPoint("CENTER", UIParent, "CENTER", 0, 0)always anchored to the middle, requiring the user to drag their mouse across the UI to interact with it. Switched toaddon.Tooltip.AnchorFrame(popup, sourceWidget), wheresourceWidgetis the AceGUI InteractiveLabel that received the click. The popup now sits directly below the row when the row is in the top half of the screen, and above it when the row is in the bottom half — same ergonomics as a tooltip. Location: GUI/CooldownsTab.lua:ShowGroupPopup.Reagent label and
[Bank]button stacked on top of each other in the transmute popup — both were anchored atSetPoint("RIGHT", entry, "RIGHT", -(mailW + 2), 0), the same offset, so they overlapped. Moved the reagent label(bankW + 2)further left so it sits cleanly to the LEFT of the bank button. Also bumped popup width 400→460 for breathing room. Location: GUI/CooldownsTab.lua:ShowGroupPopup reagent label SetPoint.Tooltips on the transmute popup rows rendered visually behind the row text instead of on top — the popup is at
TOOLTIPstrata, which is alsoGameTooltip's default strata. Same strata means whichever has higher frame level wins, and the popup's child labels/buttons win by default. Added ashowAbovePopup()helper called after everyGameTooltip:Show()in the popup that bumps the GameTooltip's frame level above the popup's. Spell, item, [Bank], and mail tooltips all now render on top. Location: GUI/CooldownsTab.lua:ShowGroupPopup.[Bank]buttons missing from the first transmute popup of a session, but appearing in subsequent popups — TOGBankClassic constructs_G.TOGBankClassic_Guild.Info.altslazily; the firstaddon.Bank.GetStock(reagentId)call during the popup creation loop hit that uninitialized state and returned 0, so no [Bank] button was created for any row. Subsequent popups queried after TOGBank had populated and worked normally. Fixed by always creating the bank button (initially hidden) and registering a per-row visibility refresher that runs onpopup:OnShowplus a deferredC_Timer.After(0.1, ...)tick — so even when TOGBank's data lands asynchronously after our popup opens, the buttons reveal themselves without requiring the user to close and reopen. Also changedpopup:Show()to follow an explicitpopup:Hide()afterCreateFrame, so OnShow fires the actual hidden→shown transition. Location: GUI/CooldownsTab.lua:ShowGroupPopup bank-refresher block.Non-alchemist viewers couldn't see proper spell tooltips for transmutes the alchemist had broadcast with
rd.spellId = nil—RefreshTransmuteCatalogueFromRecipeshad only one resolution path:GetSpellLink(rd.name), which only works for spells the local player knows. On a non-alchemist viewer, GetSpellLink returns nil for every transmute, sord.spellIdstayed nil and hover tooltips fell through to the output-item link. Added a static-catalogue name-match fallback that runs first: walkdata.transmutes(the cumulative VANILLA / TBC / WRATH / CATA / MOP transmute table), find the entry whose name matchesrd.name, and assign that spellId. Resolution works for non-alchemists because it's pure string compare against hardcoded data — no spell-knowledge required. Location: Data/CooldownIds.lua:RefreshTransmuteCatalogueFromRecipes.
Improvements
Removed the
MINOR>=Nruntime version check on DeltaSync-1.0 — the addon previously gated guild sync on a hard-codedLibStub.minors["DeltaSync-1.0"] >= Ncheck inScanner:InitDeltaSync, which made the addon's source contain library version assumptions that drift over time. Library version compatibility is the responsibility of the## Dependenciesdeclaration in the .toc, not a runtime check. Removed the check entirely; if a too-old DeltaSync is installed, behavior degrades gracefully via the existingif not DS then return endguard. Location: Scanner.lua:InitDeltaSync.Hover tooltip on every popup row, even when no spellId is available — for transmute rows that propagated with
rd.spellId = nil, the name zone now falls back toGameTooltip:SetHyperlink("item:" .. recipeId)which shows the output item's tooltip (sincerecipeIdIS the output item ID for non-spell recipes). Less informative than the spell tooltip, but better than nothing. The static-catalogue name-match fallback above means most rows actually do get the proper spell tooltip now. Location: GUI/CooldownsTab.lua:ShowGroupPopup name-zone OnEnter."Ready" label in green for transmutes castable right now — replaced the previous
?placeholder when a spell had no cooldown record. Both "no record" and "expired record" now render asReadyin|cff00ff00so the user can tell at a glance which transmutes are available to send materials for. Location: GUI/CooldownsTab.lua:ShowGroupPopup row time-string formatting.
[v0.2.6] (2026-04-30) - Recipe display recovery + lazy item-cache backfills + sync robustness
Bug Fixes
Tailoring (and other) recipes showed as
? <id>in the recipe browser — when a peer'scrafters:<profId>leaf arrived before the matchingrecipemeta:<profId>leaf,MergeCraftersIntoGdbcreated a stub{crafters={}}entry with no name/icon/links. The browser row then rendered the default question-mark icon plustostring(recipeId)as the name — visually "? 10002" — even though hovering the row showed the proper name (BrowserTab falls back toSetHyperlink("item:<id>")for tooltips, which WoW resolves from its own item cache). Three-pronged fix: (1) newcleanRecipeNamehelper extracts the real name from the[...]portion of itemLink/recipeLink when the raw scan name is a placeholder; (2)MergeCraftersIntoGdbnow populates name/icon/itemLink fromGetItemInfo(recipeId)at stub creation so even the first-paint render is correct when the item is cached; (3)MergeRecipeMetaIntoGdbself-heals on receive — if the stored name is a placeholder, replace it with the cleaned-up version from the new payload. Locations: Scanner.lua:cleanRecipeName, Scanner.lua:MergeCraftersIntoGdb, Scanner.lua:MergeRecipeMetaIntoGdb.Clicking [Bank] on a reagent row in the recipe drilldown did nothing for some users (the button was visible, the click just didn't open the popup) — the recipe drilldown's reagent row is a
Buttonframe and hadSetScript("OnClick", ...)for shift-click-to-insert-link. The child[Bank]button is also aButtonwith its own OnClick handler. On some WoW builds the parent-Button's OnClick swallows the click before the child-Button's OnClick can fire — explaining why the recipe-row [Bank] button (whose parent usesOnMouseDown, notOnClick) always worked, while the reagent-row [Bank] button intermittently didn't. Same fix as the recipe row: switch the parent's shift-click handler fromOnClicktoOnMouseDown. Different event = no conflict, child's OnClick fires normally. Location: GUI/BrowserTab.lua:DrawDetail reagent-row mouse-script setup.Transmute on the cooldown tab showed the specific spell name (e.g., "Earth to Water") instead of grouping into a generic "[+] Transmute" row with the per-spell popup, on non-alchemist viewers — the cooldown tab's
BuildRowschecksdata.transmutes[spellId]to recognise a cooldown as a transmute. The runtime augmentation that adds Anniversary client spell IDs todata.transmutes(addon:RefreshTransmuteCatalogueFromRecipes) only ran insideScanCooldowns, which fires off the LOCAL player's scan events. Non-alchemists rarely trigger it, so when an alchemist's transmute spell ID arrived via guild sync it never made it into the catalogue, and the row fell through to a regular per-spell display with no popup, no per-transmute reagents, and no per-transmute mail button. Fixed by calling the augmentation at the start ofBuildRowsitself — idempotent, cheap, runs every render. The recipe DB (populated from the alchemist's broadcast carrying spellIds backfilled viaGetSpellLink) supplies all the IDs needed. Once augmented, the transmute group + click-to-expand popup with per-spell reagents and[Bank]/ mail buttons works as designed. Location: GUI/CooldownsTab.lua:BuildRows.Roll-up sync sessions could stay in ACTIVE state after a subhashes drill-down dispatched its child leaf-data requests — when our addon received a subhashes response for
guild:cooldownsorguild:accountcharsand dispatched per-character leaf-data follow-ups in response, the parent session was never explicitly marked complete; it lingered until the underlying delivery timeout fired. With the parent's lifecycle now bracketed correctly — child leaf sessions are tracked independently and complete on their own data arrival — the roll-up's session slot is freed as soon as its drill-down dispatches, restoring full session-slot availability for the leaf fetches that follow. Location: Scanner.lua:OnGuildDataReceived subhashes branch.
Improvements
Recipe-name and reagent-itemId backfills now run multiple passes with cache-loading fallback — both backfills are scheduled at 3s/30s/120s post-login. The reagent backfill adds a third recovery path that calls
GetItemInfo(name)(cache-loading variant), which returnsnilon first call but issues an async server-side load — the next retry pass picks up the result viaGetItemInfoInstantonce the load resolves. The recipe-name backfill walksgdb.recipesand re-runs the full recovery chain (link[...]extract →GetItemInfo(recipeId)for items →GetSpellInfo(spellId)for spells), so both placeholder names and crafters-only stubs heal as the WoW item cache fills. Each pass logs only when something was actually checked, so silent runs after the data is healed don't spam chat. Locations: Scanner.lua:BackfillReagentItemIds, Scanner.lua:BackfillBogusRecipeNames, Scanner.lua:Init.Diagnostic ENTRY prints in guild-sync handlers — when
/togpm debugis on,Scanner: onDataReceived ENTRY ...,Scanner: onDataRequest ENTRY ...,Scanner: OnGuildDataReceived ENTRY ..., andScanner: onSyncAccepted ...fire at every handler entry, plus aBAIL — <reason>line on each early-return path ofOnGuildDataReceived. Without entry-level prints, when a sync handler silently no-op's (malformed payload, own echo, no guild, etc.) nothing is logged — making receive-side failures invisible. Now any future receive-path issue is observable from the user's chat without instrumenting code. Locations: Scanner.lua:InitDeltaSync, Scanner.lua:OnGuildDataReceived.P2P concurrency limits raised for active guilds — increased inbound and outbound concurrency from 3 to 8 and stretched the offer-collect window from 10s to 30s. In active 30+ member guilds the prior caps saturated within seconds because every peer was at the same limit, queuing requests faster than they drain. Higher caps plus a longer collect window let more sessions dispatch in parallel and accumulate offers from more peers before picking, increasing the odds of finding a peer that isn't at its own cap. Location: Scanner.lua:InitDeltaSync.
/togpm backfillslash command runs both backfills — runs the reagent-itemId backfill and the recipe-name backfill back-to-back. Useful for re-attempting on demand after the WoW item cache has had more time to populate (e.g., after opening a few trade-skill windows). Location: TOGProfessionMaster.lua:RunBackfill.
[v0.2.5] (2026-04-29) - GetSpellLink transmute-ID fallback + personal bank / mail in reagent counts
Bug Fixes
- Transmutes still didn't show on the cooldown tab on Classic Era Anniversary even with v0.2.4 — the v0.2.4 runtime-augment path required
rd.spellIdto be populated byBuildSpellNameCache(spellbook tab iteration viaGetNumSpellTabs/GetSpellBookItemInfo). On Anniversary, transmute spells don't appear in that enumeration, soScanTradeSkillIntostored the recipe withrd.spellId = niland the augment path had nothing to do. Section 5 of/togpm transmutedebugshowed transmute recipes present but with nil spellIds, confirming the spellbook-scan miss. Fixed by adding aGetSpellLink(rd.name)fallback inRefreshTransmuteCatalogueFromRecipes—GetSpellLinkworks for any known spell by name regardless of spellbook presentation, returns|Hspell:NNNNN|hfrom which we extract the ID, and the ID is also backfilled ontord.spellIdso the rest of the cooldown chain (knownTransmutesfilter, cooldown-tab grouping) works. Location: Data/CooldownIds.lua:RefreshTransmuteCatalogueFromRecipes.
Improvements
Reagent Tracker and Reagent Watch now show personal bank + mail alongside bag count — the
havetotal is nowbags + cached personal bank + cached mail, where personal bank is scanned onBANKFRAME_CLOSEDand mail is scanned onMAIL_CLOSED(mirroring TOGBankClassic's scan-on-close pattern). COD mail attachments are excluded (not really yours until you pay, matching TOGBank). TOGBankClassic guild-bank stock stays separate, surfaced as a+<N>annotation in the row — the user previously asked to keep guild-bank stock visually distinct from "in possession", so personal bank/mail join thehaveside and only TOGBank stays separate. NewAce.db.char.bankCountsandAce.db.char.mailCountscache per-character. First-visit-required: until the user opens their bank or mailbox once with v0.2.5 installed, the caches are empty (same first-time UX as TOGBank). Locations: Modules/ReagentWatch.lua, GUI/ReagentTracker.lua:GetPlayerBagCount, GUI/ShoppingListTab.lua:FillReagentWatch.Reagent Tracker color coding now reflects bag-vs-bank-vs-shortage — green = bags alone satisfy the recipe; yellow = bags fall short but bag + bank covers it (request from bank); orange = bag + bank still short, partial; red = nothing in bags or bank. The
+<N>bank annotation is light blue and always shown when bank stock > 0, deliberately separated from thehave/needcount so the player can read the bank contribution without it being conflated with personal possession. Display widened (COUNT_W48 → 90,WIN_W280 → 320) to fit the new format. Location: GUI/ReagentTracker.lua:Refresh.Reagent Watch panel applies the same color scheme — green = in your bags, yellow = only in the guild bank, grey = nowhere. Same
+<N>annotation pattern. Location: GUI/ShoppingListTab.lua:FillReagentWatch./togpm transmutedebugnow also runs the runtime augmentation up front — so the diagnostic reflects post-refresh state, including any spellIds resolved via the newGetSpellLinkfallback. Prints a confirmation line when new IDs were added. Location: TOGProfessionMaster.lua:DumpTransmuteDiag.
[v0.2.4] (2026-04-29) - Runtime-augmented transmute catalogue + extended transmutedebug
Bug Fixes
- Transmutes never showed up on the cooldown tab on Classic Era Anniversary (and any client whose actual spell IDs don't match
VANILLA_TRANSMUTES) — the static spell-ID catalogue inData/CooldownIds.luawas the only thingScanCooldownsconsulted. If the alchemist's actual transmute spells used different spell IDs than the ones we hard-coded (a real condition on Anniversary, where the recipe DB shows transmutes with spellIds that aren't in our catalogue),GetSpellCooldownwas called with the wrong IDs, the recipe-DB filter found zero matches, and nothing got stored. Newaddon:RefreshTransmuteCatalogueFromRecipes()walksgdb.recipes[171]for any recipe whose name contains "Transmute" and adds itsspellIdtodata.transmutesat runtime. Idempotent, cheap, runs at the start of everyScanCooldowns. The catalogue self-heals against any client-specific or locale-specific ID variation, picks up newly-learned transmutes from guildmate broadcasts, and the rest of the cooldown chain (cooldown-tab grouping, transmute popup, mail integration) works as a side effect. Locations: Data/CooldownIds.lua:RefreshTransmuteCatalogueFromRecipes, Scanner.lua:ScanCooldowns.
Improvements
/togpm transmutedebugnow also dumps the actual alchemy recipe DB and a spellbook walk — v0.2.3's diagnostic only showed which transmutes matched our catalogue. If the catalogue was stale, the diagnostic showed all zeros without a way to tell why. v0.2.4 adds two more sections: (5) total alchemy recipes ingdb.recipes[171]for the local char with their actualspellIdvalues (filtered to anything whose name contains "Transmute"), and (6) a full spellbook walk for any spell whose name contains "Transmute" with the spell ID the client is actually using. If section 6 prints IDs that aren't in the catalogue, the new runtime augmentation will pick them up automatically; it's still a useful triage signal. Location: TOGProfessionMaster.lua:DumpTransmuteDiag.
[v0.2.3] (2026-04-29) - /togpm transmutedebug diagnostic command
New Features
/togpm transmutedebug— one-shot diagnostic for the transmute-cooldown chain. Prints what the WoW API says is on cooldown, what's in the recipe DB for the local character, whatIsSpellKnownsays, and what's actually stored ingdb.cooldowns. No spell IDs to look up — just run it and paste the output. Useful for triaging "transmute isn't showing on the cooldown tab" reports without making the user dig up spell IDs. Location: TOGProfessionMaster.lua:DumpTransmuteDiag.
[v0.2.2] (2026-04-29) - Cumulative cooldown ID loading across expansions
Bug Fixes
- Transmute cooldowns from earlier expansions never showed up on Cata/MoP clients —
Data/CooldownIds.lua:Build()loaded transmute and cooldown spell IDs version-exclusively: onlyCATA_TRANSMUTESon Cata, onlyWRATH_TRANSMUTESon Wrath, etc. But spell IDs from earlier expansions stay valid on later clients — a Cata alchemist still has the 18 Wrath Eternal transmutes, the TBC primal transmutes, and the Vanilla element transmutes in their spellbook, all sharing the same 24-hour cooldown. Casting any one of them was invisible to our scan because we never iterated those IDs inScanCooldowns. Symptom: Cata alchemist casts Eternal Fire to Water → cooldown tab shows nothing for them. Same root cause for non-transmute cooldowns (Mooncloth, Spellcloth, Icy Prism) on later-expansion clients. Fixed by changingBuild()to load IDs cumulatively — the current expansion plus every earlier one. Doesn't change behavior on Classic Era (only Vanilla IDs load, same as before). Location: Data/CooldownIds.lua:Build.
[v0.2.1] (2026-04-29) - v0.2.0 sync convergence fixes
Bug Fixes
Cooldowns weren't syncing between peers — drill-down chain never fired —
HashManager:HasContentreturnedfalseforguild:cooldownsandguild:accountcharsroll-up keys, on the (incorrect) reasoning that we don't directly serve roll-up data. But DeltaSync'sOnHashListReceivedgates offers onhasContent(itemKey)— peers with stale data wouldn't offer for the roll-up becausehasContentsaid no, the broadcaster never received an offer forguild:cooldowns,onSyncAcceptednever fired, and the subhashes drill-down never happened. Symptom: PC with 10 cooldowns broadcasting; PC with 3 cooldowns receiving the broadcast but never reporting back. Fixed by returningtrueforguild:cooldownswhen we have any cooldowns ingdb.cooldowns, and similarly forguild:accountchars. We don't serve the roll-up data — the offer triggersonSyncAcceptedwhich callsBroadcastSubhashesToGuild, sending the per-character sub-hash list. Location: Modules/HashManager.lua:HasContent.Idle peers never broadcast — protocol can't push to them — v0.2.0's broadcasts only fire on event triggers (cooldown scan, recipe scan, login). With differential broadcasting, an idle peer's "no changes since last broadcast" results in a skipped send. Other peers never see the idle peer's hashes, so they never offer fresh data, so the idle peer never receives anything. The protocol doc specified a 10-minute periodic broadcast for exactly this case but the implementation only had event-driven broadcasts. Added a 10-minute repeating timer in
Scanner:Initthat resets_lastBroadcastHashes = niland broadcasts the full L0 hash list (non-differential), guaranteeing every peer is on the wire at least every 10 minutes regardless of local activity. Location: Scanner.lua:Init.Cooldowns tab scroll bar always visible and extending ~2x the window height below the bottom edge —
CooldownsTab:Drawset the container's layout to"List", but AceGUI's List layout doesn't honorchild.height == "fill"— it only manages widths. Only the Flow layout readsSetFullHeight(true)and anchors the child's BOTTOM to the parent content. Without that anchor, the AceGUI ScrollFrame's outer frame grew unbounded past the window edge and the scrollbar grew with it. Fixed by switching the container layout from"List"to"Flow". Toolbar, headers, and scroll all already hadSetFullWidth(true), so Flow stacks them vertically the same way List did — Flow just additionally constrains the scroll's height to fit the remaining space. Location: GUI/CooldownsTab.lua:Draw.
How to Force Sync on Already-Stale Data
If you upgraded between PCs and one is missing data, the periodic tick will catch up within 10 minutes. To force immediate sync, run /togpm forcebroadcast on the less-data PC — that broadcasts its hashes, peers see the mismatch, peers offer, your PC fetches the subhashes and missing leaves, and merges within seconds.
[v0.2.0] (2026-04-29) - Hash-then-fetch sync protocol, content-aware merge, relay-capable cooldowns + recipes
Major Protocol Overhaul
Replaced full-payload broadcast with hash-then-fetch sync — v0.1.x broadcast each peer's full ~30 KB profession + cooldown payload to the guild every 30 seconds, multiplied by every active broadcaster. v0.2.0 broadcasts a tiny ~600 B leaf-hash list per peer every 10 minutes (differential — only leaves whose content has changed). Peers compare hashes; on mismatch they whisper a short handshake; the chosen sender broadcasts only the differing leaf's data on the GUILD/BULK channel, where every peer with stale data merges for free. Steady-state guild traffic drops by orders of magnitude. See docs/v0.2.0-protocol.md for the full design. Locations: Scanner.lua, Modules/HashManager.lua.
Content-aware merge replaces destructive overwrite —
OnGuildDataReceivednow merges per leaf type so anyone with cached data can serve it without risk of clobbering fresher data: cooldowns merge withmax(local.expiresAt, incoming.expiresAt)per (charKey, spellId); recipe metadata merges richest-non-nil per field via the existingmergeReagentshelper; crafter sets union-add for relayed payloads and wipe-then-re-add when the broadcaster claims an authoritative own-scan; account-char groups replace authoritatively for the broadcaster's own slot and union for relayed slots. Receiving from any peer always converges to the same state. Locations: Scanner.lua:OnGuildDataReceived, Scanner.lua:MergeRecipeMetaIntoGdb, Scanner.lua:MergeCraftersIntoGdb.Cooldowns and recipes now relay through any peer —
HashManager:HasContentreturns true for any locally-cached leaf, not just owner-owned. If Alice's alchemist is offline, Bob's cached copy ofcooldown:Alice-Realmcan serve the leaf to Carol when she logs in. Recipe metadata + crafter membership relay similarly. Cooldown coverage no longer requires the data owner to be online. Location: Modules/HashManager.lua:HasContent.Hash + timestamp invariant: both immutable per data state — Each leaf entry
{hash, updatedAt}is a co-determined function of the data: both change atomically when content changes, both stay frozen otherwise.updatedAtis content-derived fromgdb.lastScan[charKey][scope], neverGetServerTime()at a no-op site. The v0.1.xHashManager:RebuildAllre-stamped every leaf'supdatedAton every receive — even no-op merges — which was the root cause of the "stale relayer with high updatedAt suppresses fresh owner's offer" routing bug. Replaced with targetedInvalidate*helpers that no-op when the new tuple matches existing. Location: Modules/HashManager.lua:setEntry.
New Hash Leaf Taxonomy
Replaces v0.1.x's cooldown:<charKey> + recipes:<profId> + guild:cooldowns + guild:recipes with:
recipemeta:<profId>— immutable recipe metadata for one profession (rare-change, bootstrap-only after first sync).crafters:<profId>— crafter membership map for one profession (frequent, deltas).cooldown:<charKey>— full cooldown bucket for one character.accountchars:<charKey>— alt group claimed by one broadcaster.guild:cooldownsandguild:accountchars— structured roll-ups over per-character leaves; broadcast at L0 with per-character leaves drilled-down on roll-up mismatch.
L0 broadcast carries 9 + 9 + 2 = 20 hashes × ~30 B per peer ≈ 600 B. Per-character leaves stay out of L0 to avoid 300-500-leaf broadcast bloat for large guilds.
Channel Allocation
GUILD/BULK for hash list broadcasts and per-leaf data responses (high throughput, throttle-tolerant); WHISPER for handshake control messages only (offers, requests). Whisper throttling no longer constrains bulk transfer.
Storage Changes
Two new top-level fields on each guild bucket; existing data is preserved verbatim:
gdb.accountChars[broadcasterKey] = { charKey, ... }— per-broadcaster authoritative alt group.gdb.altGroupsbecomes a derived view rebuilt from this.gdb.lastScan[charKey][scope]— content-derived timestamps (wherescopeis a profId,"cooldowns", or"accountchars"). HashManager reads these to compute leafupdatedAt.
Wire Format
Bumped DeltaSync namespace TOGPmv1 → TOGPmv2 to prevent v0.1.5 ↔ v0.2.0 cross-talk during rollout. v0.1.5 peers don't see v0.2.0 broadcasts and vice versa; once everyone upgrades, the v0.1.5 namespace dies.
Per-leaf payload format (payload.leaves[itemKey] = { data, hash, updatedAt }) carries content + the source's hash tuple. Multiple leaves can ride in one broadcast. Sub-hash drill-down responses (payload.type = "subhashes") carry per-character hashes for one roll-up parent.
Dependency Bump
Requires DeltaSync-1.0 MINOR>=9 (shipped in DeltaSync v2.0.3, 2026-04-29). The new offer condition (hash-mismatch instead of updatedAt > peer's) is required for the relay-capable sync model; older DeltaSync versions still load the addon but Scanner:InitDeltaSync refuses to enable sync and prints a clear error. Location: Scanner.lua:InitDeltaSync.
New Diagnostic Commands
/togpm dumphashes— print the local L0 hash list (itemKey, hash, updatedAt) for cross-peer comparison./togpm dumpcooldowns [charKey]— print stored cooldown bucket for a character (no arg = list every character with cooldowns)./togpm forcebroadcast— bypass the 10-min debounce and broadcast a full (non-differential) hash list immediately.
Bug Fixes
- Cooldowns tab letter (mail) icon wrapping under the cooldown name — AceGUI Flow's wrap math
(framewidth + usedwidth > width)is strict-greater, but in practice the mail icon was wrapping to a new row even when the inner widget widths summed exactly to col2's 456px. Reserved 12 px of slack in thecdNameWcalculation so even a small rounding/padding discrepancy in any AceGUI Label widget can't push the row total past col2 width. Location: GUI/CooldownsTab.lua:611-622.
Migration Notes
No existing data is destroyed. On first v0.2.0 load gdb.accountChars and gdb.lastScan initialize empty and populate as scans run + broadcasts arrive. gdb.altGroups is rebuilt from gdb.accountChars whenever it changes. Old recipes:<profId> hash entries become unused garbage in gdb.hashes and can be cleaned up in a future version. Existing recipes, cooldowns, and skill ranks remain usable through the merge.
[v0.1.5] (2026-04-29) - Transmute cooldowns, reagent itemId capture, non-destructive merge, Reagent Tracker bag-vs-bank fix
Bug Fixes
Transmute cooldown was detected but never stored — Cooldowns tab showed nothing while the recipe still appeared in the Browser tab —
Scanner:ScanCooldownsran two loops: the first found the active transmute viaGetSpellCooldown, the second wrote the expiry intogdb.cooldowns[charKey]. Both branches of the second loop (active-CD store and Ready seed) were gated onIsSpellKnown(spellId, false). On Classic Era that call returnsfalsefor transmute spell IDs (documented in docs/bugs.md DATA-004 — same root cause that bit the upstream ProfessionMaster fork), sotransmuteExpirywas computed correctly but immediately discarded — the recipe still appeared ingdb.recipesfrom the trade-skill scan, but no cooldown row ever materialised. Fixed by deriving "known transmutes" fromgdb.recipes[171](alchemy recipes carryspellIdfrom the spellbook scan viaBuildSpellNameCache) and force-including the spell ID that was actually found on cooldown so the active CD shows even on first login before any trade-skill window has been opened.IsSpellKnownis kept as a third fallback path for any client where it does work. Location: Scanner.lua:731-781.Reagent
[Bank]button and Reagent Tracker silently broken because reagentitemLinks were nil —GetTradeSkillReagentItemLinkandGetCraftReagentItemLinkreturnnilon Classic Era for reagents that aren't in the local item cache (e.g. items the player has never owned), even though the equivalent tooltip APIsSetTradeSkillItem(i, r)/SetCraftItem(i, r)work fine. With no link captured, the bank-stock check in BrowserTab.lua (drilldown panel + shopping-list expansion) and the Reagent Tracker'sBuildReagentList(GUI/ReagentTracker.lua:54) had no item ID to key off and either hid the row entirely (Reagent Tracker) or skipped the[Bank]button (drilldown). Fixed by routing every reagent through a hiddenGameTooltipscraper (SetTradeSkillItem/SetCraftItem→GetItem()) when the link API returns nil, and by also resolvingitemIddirectly viaGetItemInfoInstant(name)as a third-tier fallback for items that happen to be cached. Both fields are now stored on every reagent. Locations: Scanner.lua:467-491 (TradeSkill), Scanner.lua:631-655 (Craft).One peer with the broken reagent-link API would wipe the rich reagent data guild-wide —
Scanner:MergeRecipesIntoGdbwas overwritingexisting.reagentswholesale on every receive:if rd[6] ~= nil then existing.reagents = asTable(rd[6]) end. If a peer withGetTradeSkillReagentItemLinkreturning nil broadcast their version of a recipe, every receiver's previously-rich reagent table (with itemLink + itemId populated) got replaced by name+count-only entries — silently breaking the bank lookup, reagent tracker, and tooltip popups for everyone. Replaced with a non-destructivemergeReagentsthat matches incoming entries to existing ones by reagent name and preservesitemLink/itemIdwhenever the incoming payload lacks them. Location: Scanner.lua:580-636.Reagent Tracker counted guild bank stock as if it were in your bags — "0 in bags, 945 in bank" displayed as
945/30green —RT:Refreshsethave = bagCount + bankCount, so a reagent sitting in TOGBankClassic's bank was indistinguishable from one in your character's bags for satisfaction display. Bank stock is still surfaced separately by the[Bank]button on each row (only shown when stock > 0), so collapsing it intohavewas double-signalling. Fixed by settinghave = GetPlayerBagCount(item.id)only. The colour code (green/yellow/red) now reflects what you actually have on your character; the[Bank]button signals that more is available via guild-bank request. Location: GUI/ReagentTracker.lua:146-149.
Improvements
Login-time reagent backfill —
Scanner:BackfillReagentItemIdsruns onPLAYER_ENTERING_WORLD(3 s after PEW so guild + realm context are stable), walks every recipe's reagent table, and resolvesitemIdfromitemLink(parse) orGetItemInfoInstant(name)for any reagent missing both. Best-effort: items still uncached on this character can't be resolved at login, but they get filled in on the next trade-skill scan via the new tooltip scraper. Location: Scanner.lua:856-898.BrowserTabreagent rendering tolerant of missing links — Two new helpersResolveReagentItemId(r)andResolveReagentItemLink(r)lazy-resolve and cache item identity on each reagent table, so renderers transparently use whichever data is available. The detail-panel reagent row falls back toGameTooltip:SetItemByID(rItemId)whenitemLinkis nil, and the bank-stock check keys off the resolveditemIdrather than onlyitemLink. Location: GUI/BrowserTab.lua:45-79.New diagnostic commands —
/togpm dumprecipe <name>prints a recipe's stored fields and full reagent table to chat (used to diagnose the missing-itemLink bug above)./togpm backfillruns the reagent backfill on demand and printschecked=N fixed=N missed=N. Locations: TOGProfessionMaster.lua:140-141, TOGProfessionMaster.lua:377-419.
[v0.1.4] (2026-04-28) - Hand DeltaSync our AceAddon so sync goes through AceCommQueue
Bug Fixes
aceComm=falsein/togpm status— sync was bypassing AceCommQueue throttling — When the v0.1.1 externalization moved DeltaSync out oflibs/, the new external library expects the host addon to pass its AceAddon instance intoInitialize({ aceAddon = ... }). Without it, DeltaSync falls back to rawC_ChatInfo.SendAddonMessageinstead of routing throughself.aceAddon:SendCommMessage— so chunked payloads aren't throttled by AceCommQueue-1.0 and can interleave + CRC-fail silently under sync load. The Scanner'sInitializecall was missing this key entirely. Fixed by passingaceAddon = addon.lib(the AceAddon-3.0 instance with AceCommQueue already embedded onto it at TOGProfessionMaster.lua:46). After this fix,/togpm statusreportsaceComm=trueand chunked sync should be reliable. Location: Scanner.lua:87-93.
[v0.1.3] (2026-04-28) - GuildCache consolidation, "You (Alt)" disambiguation, sync-log datestamp
Improvements
LibGuildRoster-1.0removed; all guild-roster work now goes throughGuildCache-1.0— Deleted the embeddedlibs/LibGuildRoster-1.0/folder (~300 lines) and rewired theOnMemberOnlinecrafter-alert callback at TOGProfessionMaster.lua:179 to register onLibStub("GuildCache-1.0")instead. GuildCache-1.0 (bundled inside the standalone DeltaSync addon, MINOR ≥ 2) is now a true superset: query API (IsPlayerOnline,IsInGuild,GetOnlineGuildMembers,NormalizeName,GetNormalizedPlayer) plus CallbackHandler-1.0 transition events (OnMemberOnline,OnMemberOffline,OnMemberJoined,OnMemberLeft,OnRosterReady,OnRosterUpdated) plus real-timeCHAT_MSG_SYSTEMparsing plus login-race retry. One library, one source of truth. Requires theDeltaSyncaddon at a build that ships GuildCache-1.0 MINOR=2 (already a hard## Dependenciessince v0.1.1). Locations: all five*.tocfiles, TOGProfessionMaster.lua, CLAUDE.md, docs/FEATURES.md, .luarc.json.Sync log entries now show full date+time, not just time —
[14:23:11]was useful for "what just happened" but not for "did this sync happen today or yesterday?" Switched the format string in GUI/Settings.lua:317 from"%H:%M:%S"to"%Y-%m-%d %H:%M:%S". The underlyinge.ts(UNIX epoch seconds set attime()) didn't need any data change."You" disambiguation when several own alts appear in the same list — In the Cooldowns tab and the Browser tab's recipe-row crafter list, every one of your characters used to render as a single "You" label. With ten alts that meant ten rows all called "You" — useful for color-coding, useless for telling the alts apart. Now the currently-logged-in character still shows
You, and every other own alt showsYou (AltName)(short name without realm). The Browser tab also expands the previously-consolidated single "You" entry into one entry per own crafter so each alt that can craft a given recipe is listed individually. Locations: GUI/CooldownsTab.lua:550-557, GUI/BrowserTab.lua:127-172.
Bug Fixes
/togpm statuswas silently hiding the online-roster section —PrintStatusruns asfunction addon:PrintStatus()(soselfisaddon), but the GuildCache handle is stashed onScanner.GuildCache. The diagnostic readself.GuildCache(always nil), theif GuildCache thenblock silently skipped, and the user saw two----separators with nothing between them — easy to misread as "0 people online." Fixed by reaching across toScanner.GuildCacheexplicitly. Location: Scanner.lua:289./togpm statusshowedAceComm=nil AceCommQueue=nilafter the v0.1.1 DeltaSync externalization — The external DeltaSync no longer exposesuseAceComm/useAceCommQueueas direct fields on the lib handle; that data moved intoDS:GetCommStats(). Replaced the stale field reads withaceComm/registered/p2p/guildCacheline built fromGetCommStats()plus an explicitScanner.GuildCache ~= nilcheck so it's obvious at a glance whether the GuildCache library actually loaded. Location: Scanner.lua:246-253.
[v0.1.2] (2026-04-28) - Type-guard for malformed recipe wire data
Bug Fixes
- Browser tab crashed with
attempt to call method 'match' (a nil value)on opening — Six call sites in GUI/BrowserTab.lua (the recipe-row renderer at line 1466, the shopping-list color line at 594, two tooltipSetHyperlinkpaths at 909/940, and the detail-pane title/header-link block at 1199/1203) called:match/:findonentry.itemLink(andentry.recipeLink) after a plain truthy check. If any peer's wire payload landed a non-string at position[5]or[7]of a recipe array, the mergedgdb.recipes[*][*].itemLinkbecame non-string, the truthy check passed, and the method call crashed the UI. All six sites now gate ontype(entry.itemLink) == "string". Belt-and-suspenders type-guard added at the merge site in Scanner.lua:530 so future malformed wire data is coerced tonilinstead of being stored as-is —asString(rd[5])foritemLink,asTable(rd[6])forreagents,asString(rd[7])forrecipeLink.
[v0.1.1] (2026-04-28) - DeltaSync externalized as a standalone addon
Improvements
DeltaSync-1.0 is now an external dependency, not an embedded copy — Removed the entire
libs/DeltaSync-1.0/folder (DeltaSync.lua, GuildCache.lua, DeltaOperations.lua, P2PSession.lua) and switched to loadingDeltaSync-1.0from the standaloneDeltaSyncaddon viaLibStub. This is the same pattern TOGPM already uses forAceCommQueue-1.0andVersionCheck-1.0. The benefit: when multiple addons consume DeltaSync, LibStub picks one shared copy at the highest MINOR instead of each addon shipping its own fork — exactly the conflict that the v0.1.0 mod 7 convergence was working around. The standalone DeltaSync also includes a newerGuildCache-1.0library and an optionalDeltaSyncChannel.luatransport (TOGPM doesn't use either directly). Locations: all five*.tocfiles,.pkgmeta.Dependency declaration updated everywhere it lives —
## DependenciesinTOGProfessionMaster.toc,_TBC.toc,_Wrath.toc,_Cata.toc, and_Mists.tocnow listsDeltaSyncalongsideAce3,AceCommQueue-1.0, andVersionCheck-1.0..pkgmetarequired-dependenciesaddsdeltasyncso CurseForge enforces installation. The 4libs\DeltaSync-1.0\*.lualines are gone from every TOC.Roster helpers re-routed through the new
GuildCache-1.0LibStub handle — In the embedded copy,GetOnlineGuildMembers,NormalizeName,GetNormalizedPlayer,IsInGuild, andIsPlayerOnlinewere registered onto theDeltaSync-1.0LibStub handle itself (the embeddedGuildCache.luadeclaredlocal MAJOR = "DeltaSync-1.0"). The external lib promotes GuildCache to its own LibStub library (MAJOR = "GuildCache-1.0") bundled inside the DeltaSync addon. Scanner now resolvesLibStub("GuildCache-1.0", true)alongside DeltaSync and stashes it asScanner.GuildCache; all call sites in Scanner.lua, Modules/HashManager.lua, Tooltip.lua, GUI/BrowserTab.lua, and GUI/CooldownsTab.lua were updated to call through the new handle. Wire format and the rest of the DeltaSync public surface (Initialize,InitP2P,BroadcastData,RequestData,SendData,SerializeData,ComputeHash,ComputeStructuredHash, etc.) are unchanged.
Breaking Changes
- Users must install the standalone
DeltaSyncaddon — Without it, TOGPM still loads (theLibStub("DeltaSync-1.0", true)call uses the silent variant), but guild sync silently disables and you'll see "DeltaSync-1.0 not found — guild sync disabled" in the debug log. CurseForge will prompt for the dependency automatically once the v0.1.1 release ships with the updated.pkgmeta. Manual installs need to grabDeltaSyncseparately.
[v0.1.0] (2026-04-19) - DeltaSync-1.0 mod 7 convergence
New Features
- DeltaSync-1.0 bumped to MINOR=7, merging the TOGPM (mod 2) and PersonalShopper (mod 6) forks into a single shared library — Previously each addon shipped an incompatible fork at the same
DeltaSync-1.0MAJOR. LibStub always loaded whichever had the higher MINOR (PS mod 6), so TOGPM's P2P calls into a lib that didn't have them — forcing users to disable one addon. Mod 7 is the superset: kept PS mod 6'sNormalizeSender, host-suppliedself.aceAddonmodel, CHANNEL-distribution hooks, snifferFrame, andDebugStatus; ported in TOGPM mod 2's OFFER/HANDSHAKE channel types,OnComm_OFFER/OnComm_HANDSHAKEhandlers,BroadcastItemHashes/SendHashOffer/SendHandshake/InitP2Ppublic API, and CRC+stop-marker wire format (SerializeWithChecksum/DeserializeWithChecksum) with a legacy AceSerializer-only fallback so old mod 2 messages still decode. GuildCache hooks (GetNormalizedPlayer,NormalizeName,guildRosterwhisper-offline guard) are soft deps guarded by presence checks so PS can run withoutGuildCache.lua. Location:libs/DeltaSync-1.0/DeltaSync.lua.
Improvements
DeltaSync no longer embeds Ace libraries into its own
libobject — Mod 2 calledAceSerializer:Embed(lib)andAceCommQueue:Embed(lib)at load time, which coupled DeltaSync to Ace's MINOR upgrades and duplicated methods the host addon already had. Mod 7 referencesAceSerializer-3.0viaLibStub(...)at call-time insideSerializeWithChecksum/DeserializeWithChecksum(cached in a file-local upvalue), and delegates throttling to the host addon's ownSendCommMessage. The library is now a pure consumer of Ace via LibStub, never an embedder. Location:libs/DeltaSync-1.0/DeltaSync.lua.AceCommQueue throttling moved from the library to the host addon — Because DeltaSync mod 7 calls
self.aceAddon:SendCommMessage(...)instead of its ownlib:SendCommMessage, the wrap target has to be on the host addon. AddedLibStub("AceCommQueue-1.0"):Embed(Ace)immediately afterNewAddon(...)so every DeltaSync send from TOGPM is still queued and throttled — preventing CRC corruption from chunk interleaving under sync load.## Dependencies: AceCommQueue-1.0was already listed in every TOC, so no new runtime deps. Location:TOGProfessionMaster.lua.
Breaking Changes
- Wire format changed for existing TOGPM users on mod 2 — The merged mod 7 format is still AceSerializer + CRC + stop-marker (same as mod 2), but
OnComm_*receive paths now normalize sender names viaNormalizeSenderand route through the new checksum helpers. Mod 2 ↔ mod 7 messages remain decodable via the legacy fallback inDeserializeWithChecksum. No action required for existing users.
[v0.0.17] (2026-04-19) - Global [TOGPM] Tooltip & Bank Button Fix
New Features
[TOGPM]line on every item tooltip — Hovering any crafted item anywhere in the game (bags, AH, loot, merchant, chat links, comparison tooltips) now appends a single line at the bottom of the tooltip:[TOGPM] name1, name2, ...showing every guildmate (and your own alts) who can craft it. Online names are white, offline are grey. A blank row separates it from the item's own info. Works across all supported clients viaTooltipDataProcessor(MoP Classic+) orOnTooltipSetItem/OnTooltipCleared(Vanilla → Cata Classic) on GameTooltip, ItemRefTooltip, and the three ShoppingTooltips. Location:Tooltip.lua.
Bug Fixes
Tooltip crafter feature silently disabled since day one —
AceHook-3.0was never listed in theNewAddonmixins, soAce.HookScriptwas nil andTooltip.luaearly-returned on load. The global tooltip hook never ran in the addon's lifetime. Fixed by addingAceHook-3.0to the mixin list. Location:TOGProfessionMaster.lua.FindCrafterstraversing the wrong schema — Walkedgdb.guildData[charKey].professions[].recipes[].craftedItemId, which only exists in pre-migration SavedVariables. Rewritten to walkgdb.recipes[profId][recipeId]whererecipeIdIS the crafted item ID whennot rd.isSpell, collecting charKeys fromrd.crafters. Location:Tooltip.lua.[Bank]button showing on every recipe row — The recipe-row bank button was iteratingentry.reagentsand lighting up whenever any reagent had bank stock, so ~every row got a button that requested the wrong thing (e.g. Barbaric Belt asked for Leather). Replaced with a single check onentry.idso the button only appears when the crafted item itself is in bank stock, and the request dialog receives the crafted item's name/link. Suppressed entirely for enchants (no craftable item). Location:GUI/BrowserTab.lua.Custom recipe tooltip missing the
[TOGPM]line — The BrowserTab reagent-list tooltip path builds its content manually withClearLines()+AddLine(), bypassing all tooltip hooks. Added an explicitaddon.Tooltip.AppendCrafters(GameTooltip, entry.id)call beforeShow()in that path. Location:GUI/BrowserTab.lua.
Improvements
PROF_NAMESlookup promoted to addon namespace —_PROF_NAMESwas file-local inTOGProfessionMaster.lua, which preventedTooltip.luafrom showing profession names. Exposed asaddon.PROF_NAMES. Location:TOGProfessionMaster.lua.AppendCraftersexposed for explicit callers — BrowserTab's custom tooltip path bypasses hooks, soAppendCraftersis now assigned toaddon.Tooltip.AppendCraftersand callable directly. A per-tooltip_togpmAppendedflag prevents the post-hook from double-adding when the custom path also fires a subsequentShow(). Location:Tooltip.lua.Blank-line separator embedded via
|n— Two-line approach (AddLine(" ")+AddLine("[TOGPM]...")) was being reordered by the tooltip's internal build, landing at the top instead of the bottom. Switched to a singleAddLine("|n[TOGPM]...")so the blank row can't be repositioned. Location:Tooltip.lua..luarc.jsonglobals — AddedTooltipDataProcessorandEnumso the LSP stops warning on the MoP Classic+ branch. Location:.luarc.json.Shopping list tooltips use the smart anchor helper — Three
OnEntercallbacks inGUI/ShoppingListTab.luawere hardcodingGameTooltip:SetOwner(frame, "ANCHOR_TOPRIGHT"), which clipped off-screen when the window was near the top or right edge. Swapped foraddon.Tooltip.Owner(frame)so the tooltip anchors above or below based on which half of the screen the widget is in. Location:GUI/ShoppingListTab.lua.Help tooltip rewritten for the current UI — Browser and Cooldowns help blocks were written before the master-detail layout, the
!alert toggle, and the global[TOGPM]tooltip line existed, and the[Bank]description was stale after the v0.0.17 scoping fix. Rewrote both blocks with current section layout (Filters / Shopping list / Recipe area / Detail area / Everywhere else on Browser; Columns / Row actions / Controls on Cooldowns), consolidated sub-bullets into wrap-friendly paragraphs, and addedGameTooltip:SetMinimumWidth(480)so the tooltip lays out wide and short instead of tall and narrow. Location:GUI/MainWindow.lua.Help-icon tooltip anchor kept as
ANCHOR_TOP— The help icon lives in a fixed position at the bottom-right of the main window, so centered-above reads better than the helper's TOPLEFT/BOTTOMLEFT picks. Left the rawSetOwnerin place and added a comment so it isn't "fixed" back to the helper later. Location:GUI/MainWindow.lua.Transmute cooldown scan simplified — The transmute branch of
ScanCooldownshad a fragile cross-addon dependency on the globalGetCooldownTimestamp, which is defined by the separate ProfessionCooldown addon (not by WoW). When ProfessionCooldown wasn't loaded, we fell back toGetSpellCooldown; when it was loaded, we took a different code path that could behave differently. Removed theGetCooldownTimestampbranch entirely so the scan usesGetSpellCooldownon every client, matching the simpler pattern known to work in production. Location:Scanner.lua.[Bank]button added to the transmute popup — When you click a transmute group row in the Cooldowns tab, the popup lists each individual transmute with its reagent and a Mail icon. It was missing the[Bank]button that the main cooldown rows have. Added it (visible only when TOGBankClassic has stock of that specific reagent), wired to the sameaddon.Bank.ShowRequestDialogas the main rows. Widened the popup from 340 → 400 px to fit. Location:GUI/CooldownsTab.lua.
[v0.0.16] (2026-04-19) - Enchanting Tooltip Fixes & Crafter Alerts
New Features
- Crafter online alerts — When a guild member who can craft an item on your shopping list comes online, a chat message is printed and (unless suppressed) a sound plays and the screen flashes gold. Each shopping list row has a
!toggle button (gray = off, gold = on) to arm alerts per recipe. Alt-group awareness: if the online player is an alt of a crafter, the alert still fires with an "(alt of X)" note. Three settings in ESC → Options → TOG Profession Master → Crafter Alerts: master on/off toggle, suppress sound & flash, suppress on login (default on to avoid the login burst). Location:TOGProfessionMaster.lua,GUI/BrowserTab.lua,GUI/Settings.lua.
Bug Fixes
Enchanting tooltip showing wrong item — On Vanilla Classic Era, enchanting recipes scanned via the Craft frame stored only name and icon (no
isSpell, no reagents). The tooltip fallback chain would reach the lastelsebranch and callSetHyperlink("item:" .. spellId), resolving the enchant spell ID to a random item like "Sentinel's Leather Pants". Fixed by capturing reagents from the Craft frame (GetCraftNumReagents/GetCraftReagentInfo/GetCraftReagentItemLink) and settingisSpell = trueso the data format matches the TradeSkill path. Location:Scanner.lua.Enchanting tooltip not showing reagent list — The Professions tab tooltip priority checked
recipeLinkbefore reagents, but enchanting stores anenchant:SPELLIDlink there (not a displayable item link). Added|Hitem:guards onrecipeLinkanditemLinkusage, and moved thespellIdfallback to after the reagent branch so enchanting now shows the same reagent-list tooltip as leatherworking. Location:GUI/BrowserTab.lua.Shopping list alert toggle always staying enabled — The
!button on shopping list rows usedcur and nil or trueto toggle, which always evaluates totruein Lua becausenilis falsy. Replaced with an explicit if/else. Location:GUI/BrowserTab.lua.
Improvements
- VersionCheck-1.0 version field wired correctly —
Ace.Versionwas nil, so VersionCheck-1.0 fell back toGetAddOnMetadatato read the version string. Fixed by settingself.Version = addon.Versionon the Ace object inOnInitializebefore callingVC:Enable(self), so the library reads the version directly without the fallback. Location:TOGProfessionMaster.lua.
[v0.0.15] (2026-04-19) - Reagent Tracker & Professions Tab Master-Detail Layout
New Features
Reagent Tracker window — Standalone floating window (no backdrop or border) opened by right-clicking the minimap button or
/togpm reagents. Consolidates every reagent across all shopping list entries (e.g. 1 Runecloth from one recipe + 10 from another = 11 required). Each row shows the item icon, name coloured by item rarity, a have/need count (green = satisfied, yellow = partial, red = none), and a[Bank]button when a TOGBankClassic banker alt has stock. "Have" is live player bags + all banker alt stock viaTOGBankClassic_Guild. Window position is saved per character. Refreshes automatically onBAG_UPDATEand whenever the shopping list changes. Location:GUI/ReagentTracker.lua.Master-detail split layout in Professions tab — The floating recipe popup is replaced by a persistent right-side detail panel (268 px wide) inline in the Professions tab. Clicking any recipe row populates the panel without opening a separate window. The panel shows: recipe icon + name (hover for item tooltip, shift-click to insert link), right-justified shopping list qty controls (
−qty+×), per-reagent[Bank]buttons, and full crafter list with right-click-to-whisper. Location:GUI/BrowserTab.lua.[Bank]button in recipe list rows — Each left-column recipe row now shows a[Bank]button when any reagent is in TOGBankClassic stock. Recipe name column widened from 150 to 160 px; crafter column narrowed to RIGHT−56 to accommodate. Location:GUI/BrowserTab.luaBuildPool(),UpdateVirtualRows().
Bug Fixes
Bank buttons missing for ~5 minutes after login —
TOGBankClassic_Guild.InfoisniluntilGUILD_RANKS_UPDATEfires. Fixed by registering a one-shot event watcher inFillList()that triggers a deferred refresh of the recipe list, detail panel, and shopping list section once bank data is ready. Location:GUI/BrowserTab.lua.ESC proxy cleanup — Removed stale popup check from the ESC proxy
OnHidehandler; the recipe popup no longer exists as a floating frame. Location:GUI/MainWindow.lua.
[v0.0.14] (2026-04-19) - Restore BrowserTab, CooldownsTab, MainWindow & Compat Work
Bug Fixes
Restored Apr 18 evening work — A version-sync script bug was self-copying the wrong directory, silently discarding an evening's worth of changes. Recovered and recommitted: BrowserTab virtual scroll pool, CooldownsTab group/transmute popup, MainWindow ESC proxy wiring, and Compat API shims. Location:
GUI/BrowserTab.lua,GUI/CooldownsTab.lua,GUI/MainWindow.lua,Compat.lua.Version-sync script self-copy bug — The
wow-version-replication.ps1sync script was incorrectly including itself in the source glob, causing it to overwrite the destination copy with stale content. Fixed source path exclusion. Location:.vscode/tasks.json.
[v0.0.13] (2026-04-18) - P2P Sync, Transmute Scan & Version Check Command
Bug Fixes
P2P sync reliability — Multiple DeltaSync handshake and delta-apply edge cases fixed: hash mismatches on first contact, offer/response sequencing under concurrent peers, and stale session state after a guild member relogged. Location:
libs/DeltaSync-1.0/.Transmute cooldown scan — Transmute spell IDs were scanned against the wrong API path on some client builds, causing all transmutes to report as "Ready" immediately after use. Scanner now validates expiry against
GetSpellCooldownwith a 30-day sanity cap. Location:Scanner.lua./togpm versioncommand — Addedversionsubcommand; prints the running addon version and broadcasts a version check request to online guildmates. Location:TOGProfessionMaster.lua.
[v0.0.12] (2026-04-17) - BAG_UPDATE Storm, Guild Key Migration & Online Display Fixes
Bug Fixes
BAG_UPDATE_COOLDOWNbroadcast storm — Every bag slot change was triggering a full guild broadcast. Added a 30-second coalescing debounce so rapid inventory changes collapse into a single send. Location:Scanner.lua.Guild key migration — Characters whose data was stored under the old
Faction-Realm-GuildNamekey were invisible after the key format change in v0.0.11. Added a one-time migration pass onOnEnablethat moves existing entries to the newFaction-GuildNamekey. Location:Scanner.lua.Alt online display — When a crafter's main was offline but an alt on the same account was online, the alt's name was not being shown in the crafter column. Fixed display logic to show
AltName (CrafterName)format when the online alt is detected. Location:GUI/BrowserTab.lua.
[v0.0.11] (2026-04-17) - Debug Timestamps & Guild Key Format Refactor
Improvements
- HH:MM:SS timestamps on debug output — All
addon:DebugPrint()calls now prefix output with the current wall-clock time, making it easier to correlate debug lines with in-game events. Location:TOGProfessionMaster.lua.
Internal
- Guild key format changed — Guild DB key changed from
Faction-Realm-GuildNametoFaction-GuildName. Realm is intentionally omitted so connected-realm clusters share a single key regardless of which realm a member appears on. Location:Scanner.lua,TOGProfessionMaster.lua.
[v0.0.10] (2026-04-17) - Mining Profession & Reagent Wire Payload
New Features
- Mining added to profession browser — Mining (profession ID 186) added to the profession filter dropdown and static profession list. Location:
GUI/BrowserTab.lua.
Bug Fixes
- Reagent data missing for guild peers —
itemLinkandreagentsarrays were not included in the DeltaSync wire payload, so recipients could not show item tooltips or reagent details for recipes learned by guildmates. Both fields now serialized and merged on receipt. Location:Scanner.lua.
[v0.0.9] (2026-04-17) - Alt Detection & Account Character Tracking
New Features
- Alt detection — Characters on the same account are now detected and linked. Own characters are shown as
You(brand-coloured) in the crafter list and are sorted first. When a crafter's main is offline but a known alt is online, the crafter column displaysOnlineAlt (CrafterName). Location:GUI/BrowserTab.lua,Scanner.lua.
Bug Fixes
accountCharsregistration timing — Account character list was being registered inOnInitialize, beforePLAYER_ENTERING_WORLDhad fired and guild data was available. Moved toPLAYER_ENTERING_WORLDto ensure the roster is populated before alt matching runs. Location:TOGProfessionMaster.lua.
[v0.0.8] (2026-04-17) - Connected-Realm Sender Normalization & Broadcast Storm Fix
Bug Fixes
Connected-realm sender names not normalized — Guild members appearing on connected realms were stored under their raw
Name-ConnectedRealmkey instead of the canonical normalized realm, creating duplicate entries and breaking online-status detection. All incoming sync messages now pass throughGetNormalizedRealmName()before storage. Location:Scanner.lua.Sync broadcast storm from cross-realm cluster members — Receiving a sync payload from a cross-realm cluster member was triggering a re-broadcast of the full dataset back to the guild, causing exponential message traffic. Fixed by gating re-broadcast on a "data changed" flag rather than "data received". Location:
Scanner.lua.
[v0.0.7] (2026-04-17) - AceComm Sync Fixes
Bug Fixes
AceComm handler signature mismatch — The registered
OnCommReceivedhandler had an incorrect parameter order (prefix, message, channel, sendervs the actual AceComm dispatch ofprefix, message, distribution, sender), silently discarding all incoming sync messages. Corrected signature. Location:Scanner.lua.AceComm handler parameter shift — A secondary handler registration was using a closure that shifted all parameters by one, causing the sender field to be read as the channel and vice versa. Fixed parameter binding. Location:
Scanner.lua.Broken sort indicator on Cooldowns tab headers — Column header sort arrow textures were referencing a path that doesn't exist on Classic Era, leaving a broken texture visible at all times. Removed the sort indicator until a valid asset is identified. Location:
GUI/CooldownsTab.lua.
[v0.0.6] (2026-04-17) - HashManager & DeltaSync Stability
New Features
- HashManager hierarchical hash system — New
Modules/HashManager.luaimplements a Merkle-style hash cache: per-member cooldown leaf hashes, per-profession recipe leaf hashes, and guild-level roll-ups (guild:cooldowns,guild:recipes). DeltaSync uses these hashes to skip transfers when both peers already agree. Location:Modules/HashManager.lua,Scanner.lua.
Bug Fixes
DeltaSync
Serializenil on early send — AceSerializer-3.0 was being embedded insideInitialize(), so any send that fired beforeInitializecompleted causedattempt to call Serialize (nil). Moved library embedding to load time. Location:libs/DeltaSync-1.0/DeltaSync.lua.BroadcastItemHashesnil guard — A startup timer could fire before the P2P session was fully constructed, causing a nil-access crash inBroadcastItemHashes. Added existence guard. Location:Scanner.lua.
[v0.0.5] (2026-04-17) - Cooldowns Tab UI Polish
Improvements
Cooldowns row layout — Fixed column width calculations so character name, cooldown name, reagent, and time-left columns no longer overlap at narrow window widths. Sort arrow positioning corrected. Location:
GUI/CooldownsTab.lua.Header tooltips and brand color — Cooldowns tab column headers now show descriptive tooltips on hover and use the addon brand color (
FF8000) for header text, matching the Professions tab style. Location:GUI/CooldownsTab.lua.Header bleed fix — Column header row was rendering 2 px outside the tab content frame at the bottom, causing a thin line of header background to bleed into the first data row. Fixed via explicit height clamp. Location:
GUI/CooldownsTab.lua.
[v0.0.4] (2026-04-17) - Package Metadata & TOC Fixes
Bug Fixes
Incorrect CurseForge
.pkgmetaslugs — External library slugs in.pkgmetawere pointing to wrong CurseForge project paths, preventing the packager from embedding Ace3 and companion libraries correctly on release builds. Location:.pkgmeta.TOC interface version mismatches —
TOGProfessionMaster_TBC.toc,_Wrath.toc,_Cata.toc, and_Mists.tochad incorrect## Interface:values that caused the client to flag the addon as out-of-date on those versions. Corrected to the appropriate build numbers. Location: all.tocfiles.
Internal
- Added
.gitignoreentries for legacy and copyright-encumbered source files that must not be committed to the public repository.
[v0.0.3] (2026-04-16) - Recipe Browser Tooltip Overhaul
New Features
Rich recipe tooltips — Hovering a recipe row in the Professions tab now shows a fully custom tooltip: profession name + recipe name header (WoW yellow), reagent list with quantities, and full item data (quality, stats, binding, flavor text) scraped from a hidden
GameTooltipTemplateframe without triggering other addon hooks. Location:GUI/BrowserTab.lua,Tooltip.lua.Crafter line in tooltips — Tooltip footer lists all known crafters with the current player shown as gold
Yousorted first. Online crafters are shown in white; offline in grey. Location:GUI/BrowserTab.lua.Centralized UI color palette —
addon.BrandColor(Legendary orangeFF8000),ColorYou,ColorCrafter,ColorOnline,ColorOfflinedefined once on the addon table and used throughout all GUI files and Tooltip.lua. Location:TOGProfessionMaster.lua.Smart tooltip anchoring — Tooltip anchors below the hovered row when in the top half of the screen (
ANCHOR_BOTTOMLEFT) and above when in the bottom half (ANCHOR_TOPLEFT), preventing clipping.addon.Tooltip.Owner()helper added toCompat.luafor consistent anchoring across all modules. Location:Compat.lua.
Improvements
L["You"]locale key — Added toLocale/enUS.luafor consistent localization of the self-reference label. Location:Locale/enUS.lua.
[v0.0.2] (2026-04-16) - Complete Clean-Room v1.0 Build
New Features
Profession browser —
GUI/BrowserTab.lua: virtual-scroll recipe list (35-row pool), profession dropdown filter, text search, Guild/Mine view toggle, shopping list integration. Location:GUI/BrowserTab.lua.Cooldowns tracker —
GUI/CooldownsTab.lua: displays all guild members' tracked profession cooldowns with character name, cooldown name, reagent, and time remaining. Right-click any row to whisper. Location:GUI/CooldownsTab.lua.Shopping list — Per-character shopping list with quantity controls, reagent expansion, and missing-reagents tracking. Location:
GUI/ShoppingListTab.lua,Modules/ReagentWatch.lua.P2P guild sync via DeltaSync-1.0 — Custom embedded library broadcasting profession recipes, skills, cooldowns, specializations, and alt-group data peer-to-peer over guild addon channels. Full payload on first contact; hash-based delta sync thereafter. Location:
libs/DeltaSync-1.0/,Scanner.lua.Scanner — Scans
TRADE_SKILL_SHOW,BAG_UPDATE_COOLDOWN, and related events to capture recipe and cooldown data, merges into the guild DB, and firesGUILD_DATA_UPDATEDcallbacks. Location:Scanner.lua.AceDB storage —
TOGPM_GuildDB(account-wide, guild-scoped): recipes, skills, cooldowns, specializations, altGroups, hashes.TOGPM_Settings(per-character): shopping list, reagent watch, alerts, frame positions. Location:TOGProfessionMaster.lua.Minimap button — LibDataBroker + LibDBIcon launcher. Left-click opens profession browser; right-click opens reagents; Shift+Left-click opens settings. Location:
GUI/MinimapButton.lua.Settings panel — AceConfig-3.0 options registered under ESC → Options → Addons → TOG Profession Master: minimap button toggle, persist profession filter, debug output, force re-sync, purge data, sync log viewer. Location:
GUI/Settings.lua.Sync log — Scrollable log of last 200 sync events (send/recv/request/version) with timestamps and byte counts. Location:
Modules/SyncLog.lua,GUI/Settings.lua.Multi-version TOC — Supports Vanilla (Classic Era / Anniversary), TBC, Wrath, Cata, and Mists via separate
.tocfiles. Version flags (addon.isVanilla,addon.isTBC, etc.) set at load time fromGetBuildInfo(). Location:Compat.lua, all.tocfiles.Slash commands —
/togpm,/togpm sync,/togpm debug,/togpm purge,/togpm version,/togpm minimap. Location:TOGProfessionMaster.lua.
[v0.0.1] (2026-04-16) - Initial Scaffold
Internal
- Repository initialized. Clean-room project structure established:
libs/,Data/,GUI/,Modules/,Locale/,docs/. Core addon frame (TOGProfessionMaster.lua), AceAddon skeleton, and placeholder TOC created. No functional game code.