TOGBankClassic-v1.3.2
What's new
TOGBankClassic Changelog
[v1.3.2] (2026-08-03) - ElvUI & Baganator Item Highlighting, Offline Test Suite
Bug Fixes
BAGANATOR-001: "Highlight needed items" did nothing when running Baganator — Same failure as ELVUI-001 below and from the same cause: Baganator replaces the bag UI, so the Blizzard-frame fallback found only hidden buttons and
ApplyOverlaybailed on its visibility guard. This is the path that affects the TBC install, where Baganator is the bag addon in use.Unlike ElvUI there is no public way to drive Baganator's search or its per-slot dimming —
Baganator.APIexposes no search setter, and the per-buttonSetItemFiltered/SetMatchesSearchmethods live on internal mixins. The one sanctioned integration is the corner-widget API, so the visual differs by design here: needed items get a marker in the icon's top-left corner instead of everything else going grey. Registered viaBaganator.API.RegisterCornerWidget, following the pattern of Baganator's ownequipment_set_iconand CanIMogIt widgets inAPI/ItemButton.lua(lines 316-355), and refreshed throughBaganator.API.RequestItemButtonsRefresh({Baganator.Constants.RefreshReason.ItemWidgets}).Three details worth recording. The
onUpdatecontract is tri-state — true shows, false hides, and nil means "item data isn't available yet" — so a cold item cache returns nil rather than false, which would otherwise latch the marker off until the next full refresh; ID-keyed requests skip that entirely since they need no cache. The marker usesSetColorTexturerather than a texture path because the Era client is missing many icon assets and a missing file renders as a blank square with no error. AndRegisterCornerWidgetasserts on a duplicate id, so registration is latched and wrapped inpcall— a Baganator API change degrades to the previous behaviour instead of breaking highlighting for everyone. Not yet exercised in-game. Locations:Modules/ItemHighlight.lua,.luarc.json.BAGANATOR-001/ELVUI-001: integration latches were read out of scope —
SetEnabled's disable path sits above the ElvUI and Baganator implementations in the file, so its reference to thebaganatorRegisteredlatch resolved to a nil global rather than the local declared further down. The guard would never have fired, leaving markers on screen after unticking the box. Both latches now sit at the top of the file with the module's other state. Caught by the language server, not byluac -p— a bare global read is valid Lua. Location:Modules/ItemHighlight.lua.ELVUI-001: "Highlight needed items" did nothing when running ElvUI — The checkbox ticked, saved its state, and had no visible effect.
ItemHighlightonly ever supported two bag UIs: Bagnon (driven through its search string) and Blizzard's defaultContainerFrameNItemNbuttons. ElvUI replaces the bag UI wholesale, so the Bagnon branch found noBagnon/BagBrotherglobal, fell through to the Blizzard branch, and every button lookup landed on a frame ElvUI keeps hidden — whereApplyOverlay'sif not button:IsVisible() then return endguard bailed silently. No error, no message, nothing dimmed.Added a dedicated ElvUI path that reuses ElvUI's own per-slot
searchOverlaytexture — the dark overlay it already shows over items filtered out by its search box — rather than writing icon vertex colours that ElvUI overwrites on its next rebuild. Both writers of that overlay are hooked withhooksecurefunc(B:UpdateSlot, which sets it during a per-slot rebuild, andB:InventorySearchUpdate, which re-asserts it on search events) so our pass runs after ElvUI's and wins. We only ever turn the overlay on for unneeded items and never off, so a slot ElvUI is already hiding for its own search stays hidden and its search keeps working. Disabling the checkbox callsB:UpdateAllBagSlots()withenabledalready false, making the hook a no-op so ElvUI reasserts its own state. ElvUI's bank is covered for free, sinceUpdateSlotis invoked withB.BankFrametoo.Verified against
ElvUI/Game/Shared/Modules/Bags/Bags.luaintukui-org/ElvUI:B:UpdateSlot(frame, bagID, slotID)(line 678),slot.searchOverlay:SetShown(info.isFiltered)(lines 734, 742),SetColorTexture(0, 0, 0, 0.6)(line 2734),B:InventorySearchUpdate(line 824),frame.Bags[bagID][slotID](lines 679-680),B.BagFrame/B.BankFrame(lines 3757-3758),B:UpdateAllBagSlots()(line 507), andE:NewModule('Bags', ...)inGame/Shared/General/Initialize.lua. That module is shared across flavours and branches internally onE.Classic/E.Retail, so one path serves Classic Era and TBC with no flavour-specific code on our side. Not yet exercised in-game on either flavour — ElvUI is not installed on the development machine. Every lookup fails safe: if any assumption is wrong the path returns nil and falls through to the previous behaviour rather than erroring. Locations:Modules/ItemHighlight.lua,.luarc.json.ELVUI-001: ElvUI is only claimed when it is actually drawing the bags — The
Bagsmodule object exists even when the user has switched ElvUI's bag replacement off (running Bagnon underneath it, for instance). Detecting the module alone would have let the ElvUI path claim a UI it wasn't driving and skip the Bagnon integration entirely. The check now also requiresB.BagFrame, which is only assigned inB:Initialize(), so a disabled bag module falls through to the Bagnon and Blizzard paths as before. Location:Modules/ItemHighlight.lua.
Internal
TEST-001: offline test suite — The addon now carries a unit-test suite that runs against a fake WoW client with no game and no LuaRocks, matching the layout used by the other addons in the tree.
Tests/wowapiis the shared WoWAPITesting harness as a git submodule;Tests/env_togbank.luaadds the environment this addon needs on top of it (a controllable clock and timer queue, the container/bag API, the guild roster API, the item andItemMixinAPIs, and a.toc-ordered module loader);Tests/coverage.luais the zero-dependency line-coverage tool, copied from GuildRoster. Run withlua Tests/wowapi/run.luafrom the addon root — Lua 5.1 is the only requirement.Testsis in.pkgmeta's ignore list so none of it ships. New:.busted,.luacheckrc,Tests/README.md,Tests/HARNESS_CONTRACT.md.211 specs across 8 files: 189 pass, 22 fail. The 22 failures are deliberate — each one asserts correct behaviour against a defect recorded in
docs/AUDIT_2026-08-03.mdand names the audit ID in its failure message. They are the audit made executable and go green as the fixes land in v1.4.0; none should be made to pass by weakening an assertion.One environment decision is worth recording because it is load-bearing:
C_Timer.Afterreturns nothing in the harness, exactly like the real API. OnlyNewTimer/NewTickerreturn a cancellable handle. The convenient stub — returning a handle fromAftertoo — would have made all eightTIMER-001sites pass and hidden the entire bug class, since every one of those cancels sits behind anif timer thenguard that makes a broken cancel indistinguishable from a working one.AUDIT-001: full-codebase audit recorded —
docs/AUDIT_2026-08-03.mdis the working document for the v1.4.0 overhaul: 2 critical, 4 high, 12 medium and 9 low findings, each with a greppable ticket ID, a location, a stated fix and a checkbox. It also records what was verified healthy (TOC lockstep,.pkgmetacorrectness, comm-prefix registration, the three-way debug-category consistency) so a later pass does not re-litigate settled ground, and it is explicit about which modules were line-read and which were only mechanically scanned. Roughly half the codebase —Chat,RequestLog,DeltaComms,Mail,Optionsand the UI modules — still needs a second read pass, tracked asAUDIT-PASS2.Nothing in this release fixes any of those findings. The suite and the document exist so v1.4.0 can fix them against a safety net rather than by inspection.
DEBUG-001: five further mis-categorised debug calls found by the suite —
Tests/constants_spec.luascans everyDebug()call site in every file and validates the category/tag pair againstDEBUG_CATEGORYandDEBUG_TAGS. It caught fiveDebug("FULFILL", …)calls inModules/RequestLog.lua(lines 2070, 2075, 2077, 2153, 2158) that the manual read missed —RequestLog.luais one of the modules not yet line-read. An unregistered category is not cosmetic:Output:Debugfalls through to the category-only branch, the string becomes the format string, every argument shifts, and the line renders with a raw%din it. Recorded in the audit, not yet fixed.
[v1.3.1] (2026-08-02) - Banker Inventory Never Scanned
Bug Fixes
SCAN-001: The per-character "enable scanning" flag could read as unset, silently disabling all scanning — Found while investigating a report of a banker's Inventory tab staying empty. This is a real latent defect but it was not the cause of that report, and it is not TBC-specific.
Bank:Scan()andMail:Scan()both gate onOptions:GetBankEnabled(), which readsdb.char.bank.enabled. That key was missing from the AceDBchardefaults inOptions:Init()(the table declared onlydonations = true), so on any profile where it had not been explicitly written it readnil— falsy — and every scan returned early. The single place that ever wrote it wasOptions:InitGuild(), which was reachable only from insideif TOGBankClassic_Guild:Init(guild) thenin theGUILD_RANKS_UPDATEhandler.Guild:Initreturnsfalseas soon asInfo.namematches the current guild, soInitGuildgot exactly one attempt per session — and that attempt fires before the guild roster carries public/officer notes. WithmemberRosterstill empty (it is built byRefreshOnlineCachebehind aC_Timer.After(0.5)),IsBank()fell through toGetBanks(), which found nogbanknotes, returnednil, and madeInitGuildbail at its ownIsBankguard. Nothing retried it, soenabledwould staynilin SavedVariables for the life of that character.InitGuildis also what registers the Bank options panel, so losing that race additionally left the "Enable for<character>" tick box absent from the options tree.Hardened in three parts: (1)
enabled = trueis now a declared default in thecharscope, so the flag can never readnil— safe for non-bankers because both scan paths already gate onIsBank()independently; (2)Options:InitGuild()now latches on success viaself.guildInitializedinstead of relying onGuild:Init's once-per-guild return, so it is safe to call repeatedly and retries until banker status is actually known —AddToBlizOptionsstill runs exactly once, so no duplicate Bank panels; (3) it is now called unconditionally onGUILD_RANKS_UPDATEand from the deferred block inGUILD_ROSTER_UPDATEimmediately afterRebuildBankerRoster(), which is the first momentIsBank()can answer correctly —GUILD_RANKS_UPDATEalone is not a reliable retry hook because it may not fire again after the roster loads. Locations:Modules/Options.lua,Modules/Events.lua.SCAN-001: An empty Inventory tab showed "Loading items..." forever — When a character's aggregated item list was empty,
OnGroupSelectedadded the loading label and then skipped the entireif items and #items > 0block, so thescroll:ReleaseChildren()that clears the label — which lives inside theItem:GetItemscallback — never ran. An empty record was therefore indistinguishable from a stalled load, which is what disguised the scan bug above as a hang. Empty tabs now clear the label and state the real situation, with different wording for your own character (which tells you to open the bank or run/togbank share) versus another banker's (which is waiting on them to share). Location:Modules/UI/Inventory.lua.
Improvements
- SCAN-001:
Bank:Scan()now logs why it declined to scan — All five early returns (nothing marked dirty,Guild.Infonot loaded, no bankers found in guild notes, this character not in the banker list, scanning disabled for this character) previously returned in silence, and the function's first debug line sat well past all of them. Diagnosing a non-scanning banker meant reading the source and guessing which precondition was unmet. Each gate now emits aBANK.GATEline naming the precondition and, where useful, the remedy; a matchingBANK.SCANline on the success path reports the item and slot totals and whether the bank vault was included (it is skipped away from a bank NPC, which is expected and previously invisible). Enable with the BANK category in the debug options. Location:Modules/Bank.lua.
Internal
- Wired up the
BANKdebug category, which was declared but unreachable —DEBUG_CATEGORY.BANKhad existed inModules/Constants.luasince the category system was introduced, but it had noCATEGORY_METArow (so no toggle appeared in the debug options), no entry inDatabase:Init()'sdebugCategoriesdefaults, and noDEBUG_TAGSblock — and not one line ofModules/Bank.luaever wrote to it. Added all three, withGATEandSCANtags.ITEMwas likewise missing from thedebugCategoriesdefaults (it did have an options row) and has been added alongside, restoring the invariant inCLAUDE.mdthat the category list, the defaults table, andCATEGORY_METAstay in sync. Locations:Modules/Constants.lua,Modules/Database.lua,Modules/Options.lua.
[v1.3.0] (2026-08-02) - TBC Client Support
New Features
- TBC-001: Added a TBC TOC so the addon loads on Burning Crusade clients — TOGBankClassic previously shipped a single
TOGBankClassic.tocat Interface 11508, so a TBC client (2.5.x) treated it as out of date and the CurseForge listing offered no TBC build. AddedTOGBankClassic_BCC.tocat Interface 20506, matching the file list of the Era TOC exactly (same libraries, same module load order, same SavedVariables, sameAce3, VersionCheck-1.0dependencies) and differing only in the## Interfacevalue. The BigWigs packager reads every*.tocin the tree to decide which game versions to publish for, so the same source tree now produces both the Era and the TBC build..pkgmetakeepsenable-toc-creation: no— both TOCs are checked in and maintained by hand. Location:TOGBankClassic_BCC.toc.
Improvements
- Bumped the Classic Era interface to 11509 —
TOGBankClassic.tocstill declared 11508 while the live Era client is 1.15.9, so the addon showed as out of date in the character-select AddOns list until "Load out of date AddOns" was ticked. Location:TOGBankClassic.toc.
Internal
The two TOC files must be kept in lockstep. Any new module, vendored library, SavedVariable, or metadata line has to be added to both
TOGBankClassic.tocandTOGBankClassic_BCC.toc— a module added to only one silently fails to load on that flavour. Recorded inCLAUDE.md.Added the dev-sync watcher so both flavours can be tested from one working tree — ported
wow-version-replication.ps1from the FastGuildInvite repo and retargeted it at this addon. It mirrors the_classic_era_source tree into_anniversary_\Interface\AddOns\TOGBankClassic(the TBC install) on a 2-second poll. Two independent launchers start it: the developer's global Claude CodeSessionStarthook (which scans the project dir for the script) and, for editor-only sessions, afolderOpentask in.vscode/tasks.json. A per-repo named mutex plus the hook's own process scan mean whichever fires second exits cleanly instead of racing the first — confirmed in practice, the first live run logged oneLAUNCHfollowed a second later by oneSKIP already-running.$WowVersionsdeliberately lists only_classic_era_and_anniversary_—_classic_(MoP) and_retail_are excluded because there is no TOC for them and a copy there would sit permanently "out of date". The skip list is built by parsing.pkgmeta'signore:block, so the synced install mirrors the shipped zip; the repo's flavour-specific.gitpointer file is hard-skipped so it can never resolve the_anniversary_copy back at the Era git dir. Verified end to end on the first real run: 50 files landed in the TBC install, no.git, no dot-prefixed entries anywhere in the tree, nodocs/ortools/— matching the-DryRunprojection exactly. Locations:wow-version-replication.ps1,.vscode/tasks.json,.pkgmeta..pkgmetacleaned up to the documented ignore-syntax rules — removed seven dot-prefixed entries (.git,.github,.gitattributes,.gitignore,.vscode,.luarc.json,.markdownlint.json) and the"*.DS_Store"glob. All eight matched nothing: the packager'scopy_directory_tree()prunes dot-prefixed paths unconditionally, so listing them implied coverage the entries weren't providing. Rather than couple the packager config to the dev-sync script (the script had been reading those entries to build its skip list),wow-version-replication.ps1now mirrors the packager's prune directly in its own$AlwaysSkip— one dot-segment pattern replacing the.gitignore/.gitattributes/.gitmodules/.pkgmetaspecial cases. Verified equivalent with-DryRun: the same 50 files copy and the same 31 skip as before the change. Also documented the full ignore-syntax ruleset in a comment block (bare folder names, single-star quoted globs, no dot-prefixed entries, and the trailing-comment trap that ships empty zips), and corrected the staleenable-toc-creationcomment which read "Enable …" above ano. Locations:.pkgmeta,wow-version-replication.ps1.
[v1.2.1] (2026-07-30) - Settings Panel Open Fix
Bug Fixes
- SETTINGS-002: Opening the settings panel errored (
bad argument #1 to 'OpenSettingsPanel') — Clicking the gear icon in the Inventory window or choosing Settings from the minimap button threwBlizzard_Settings.lua:144: bad argument #1 to 'OpenSettingsPanel' (outside of expected range ...)and the panel never opened.Options:OpencalledSettings.OpenToCategory("TOGBankClassic")— a name, which only ever worked because AceConfigDialog-3.0 overwrote the registered category'sIDfield with the category name. The current Ace3 build stops doing that override on any client exposingC_SettingsUtil.OpenSettingsPanel(which Classic Era now does), becauseOpenSettingsPanelrequires a numeric category ID, so our name string reached it and was rejected. Fixed by capturing the real category ID fromAddToBlizOptions' second return value at registration time and passing that toSettings.OpenToCategory. AddedOptions:GetBlizCategoryID(), which falls back to AceConfigDialog'sBlizOptionsIDMapand finally to the bare name so older clients (where the ID is the name) keep working, plus a guard that reports an error instead of throwing ifSettings.OpenToCategoryis missing entirely. Also removed a stale comment claiming the call had to be made twice. Location:Modules/Options.lua.
[v1.2.0] (2026-07-22) - API Compatibility Sweep, Manual-Fill & UI Fixes
Bug Fixes
STATICPOPUP-001: Manual "Mark Filled" quantity was never recorded (order stayed open) — Reported by multiple users in Classic: opening the Complete (hand-off) prompt, entering a quantity, and clicking Mark Filled did nothing — the amount never reached the Sent column and the order was never marked filled. Root cause is unrelated to the global-removal wave: the Era client's earlier StaticPopup refactor (the
Blizzard_StaticPopupmixin rewrite) replaced the dialog's.editBox/.button1fields with:GetEditBox()/:GetButton1()accessor methods. The prompt readself.editBox, which is nownil, so the typed quantity read asniland the code silently bailed (no error). Fixed with smallpopupEditBox()/popupButton1()helpers that read through the methods (falling back to the legacy fields), used in the prompt'sOnShow,OnAccept, andEditBoxOnEnterPressed. The quantity is now captured and recorded viaFulfillRequestByIdas designed. Location:Modules/UI/Requests.lua.WRAP-001: Re-open button wrapped on top of the fulfill/mail button on smaller windows — The per-row Actions column holds a 5-button group (fulfill + complete + cancel + delete + re-open) whose Flow layout reserves all 120px of buttons + 20px of spacers = exactly 140px even when some buttons are hidden. With the column pinned at 140px, the last button (re-open) sat exactly on the content-equals-column boundary and tipped into a second row — rendering on top of the fulfill (mail) button — whenever sub-pixel rounding shrank the usable width on a smaller window. Fixed by widening the Actions column to 152px so the button row always has headroom and can't wrap. Location:
Modules/UI/Requests.lua.ROSTER-001: Login/reload error
attempt to call a nil value 'GuildRoster'— A recent Classic Era client update removed the globalGuildRoster()function, migrating it toC_GuildInfo.GuildRoster(). The addon called the old global from five places, so it threw onPLAYER_ENTERING_WORLD(login/reload), on guild join/leave system messages, on banker-roster fallback scans, and on the delta online-banker fallback — leaving the guild roster refresh dead. Fixed centrally by the new compatibility shim (see COMPAT-001); call sites keep callingGuildRoster().METADATA-001: Opening a window errored with
attempt to call a nil value 'GetAddOnMetadata'— Same removed-global class of bug: the client moved the globalGetAddOnMetadata()toC_AddOns.GetAddOnMetadata(). The addon called the old global from seven places (the Inventory window title,/togbank version, version handshakes, and roster/request version stamps), so opening the Inventory window from the minimap icon threw atInventory.lua:85. Fixed centrally by COMPAT-001.OFFNOTE-001:
CanViewOfficerNote()was removed — officer features silently disabled — The client removed the globalCanViewOfficerNote()(moved toC_GuildInfo.CanViewOfficerNote()), and unlike the item APIs below it has no deprecation fallback, so the bare global is alreadynil. One unguarded call (Modules/Guild.lua:3229) threwattempt to call a nil value; the other eight sites used the defensiveCanViewOfficerNote and CanViewOfficerNote()idiom and so didn't crash but always evaluated tofalse— quietly turning off every officer-gated feature (the Requests Settings tab, cancel-reason editor, help-note editing, officer options). Fixed centrally by COMPAT-001, which restores the global so all nine sites — guarded and unguarded — work again. Found via a full audit of the addon's global usage against the current Era source (v1.15.9).ITEMAPI-001 / COINAPI-001: Proactively migrated shim-gated item & currency globals —
GetItemInfo,GetItemInfoInstant,GetItemQualityColor,PickupItem(→C_Item.*) andGetCoinTextureString(→C_CurrencyInfo.*) still work today, but only through Blizzard'sBlizzard_DeprecatedItemScript/Blizzard_DeprecatedCurrencyScriptaddons, which are gated behind theloadDeprecationFallbacksCVar and explicitly slated for removal.GetItemInfoalone has ~20 call sites across the addon. These would break the moment that CVar is flipped (the same removal wave that already took the three globals above), so they are pre-emptively covered by COMPAT-001. No user-visible change today; this is insurance against the next client update.FONT-001:
SetFonterror on opening a window (bad argument #3 ... ITALIC) — The status bar's centre text calledSetFont(font, size, "ITALIC"), but"ITALIC"was never a validSetFontflag (onlyOUTLINE/THICKOUTLINE/MONOCHROME/etc. are). Older clients silently ignored the bad flag; the current client validates strictly and errors, which broke opening the Inventory/Requests windows atStatusBar.lua:325. The text had always rendered non-italic anyway (the flag was ignored), so the fix re-applies the font with no flags — same appearance, no error. Location:Modules/UI/StatusBar.lua.
Internal
PKGMETA-001: Fixed the packager ignore list so dev files stop shipping — Several
ignoreentries used forms the BigWigs CurseForge packager silently doesn't match against files: trailing-slash directory entries (docs/,tools/,.vscode/) becomedocs//*after the packager appends its own/*, matching nothing, so the wholedocs/tree andtools/were bleeding into the released zip; and**/*.ps1/**/.DS_Storemiss root-level files. Rewrote the list in canonical packager syntax — directory entries with no trailing slash (the packager appends/*itself) and plain*.extglobs (verified that*matches/in the packager'scase-based matcher, so a single*covers all depths) — and dropped the now-redundant inline comments. Location:.pkgmeta.COMPAT-001: Central API compatibility shim (
Modules/Compat.lua) — Added a single, load-first module that re-establishes every bare global the client removed by aliasing it to itsC_*namespace equivalent (if _G[name] == nil and C_Foo and C_Foo[fn] then _G[name] = C_Foo[fn] end), exactly as Blizzard's own deprecation addons do but unconditionally (independent of theloadDeprecationFallbacksCVar), so the addon works whether or not the fallback shim is loaded. This keeps ~35 call sites clean (noC_*.rewrites, guarded idioms keep working) and gives one authoritative map of "APIs Blizzard removed and where they went." Currently coversGuildRoster,CanViewOfficerNote(→C_GuildInfo),GetAddOnMetadata(→C_AddOns),GetItemInfo/GetItemInfoInstant/GetItemQualityColor/PickupItem(→C_Item), andGetCoinTextureString(→C_CurrencyInfo). Added toTOGBankClassic.tocas the first module;C_GuildInfoandC_CurrencyInfoadded to.luarc.json. A full cross-reference of the addon's ~50 API globals against the Era v1.15.9 source confirmed all others (guild roster reads, mail APIs, chat/FCF helpers, memory profiling) are still called bare by Blizzard and remain safe.
[v1.1.4] (2026-05-30) - Re-open Orders, Multi-Order Mail & Fulfillment Fixes
New Features
- REOPEN-001: Re-open a finished order — A banker, officer, or GM can now re-open a completed order (filled, manually-completed, or cancelled) from the Requests window, in case it was marked done by mistake. A re-open icon appears on finished rows for those roles; confirming resets the order to
open, clears its Sent count, and drops any cancel reason. NewGuild:ReopenRequest(requestId, actor)(gated byCanManageRequests= banker/officer/GM), areopenmutation type, and an optionalreopenedAtfield appended to the request record and thetogbank-rd2wire format (slot 14, append-only). The request log normally ratchets terminal statuses — it refuses to un-cancel/un-complete an order during sync so a stale fulfillment can't revert a finished order — so the re-open carries areopenedAttimestamp and a narrow exception inmergeRequestlets a re-open stamped after the terminal defeat the ratchet, so it survives sync instead of snapping back to done. Older clients (pre-REOPEN-001) keep the order terminal until they update. Locations:Modules/RequestLog.lua(ReopenRequest,mergeRequestratchet exception,ApplyRequestMutationreopenauth, wire serialize/deserialize/sanitize),Modules/UI/Requests.lua(re-open button + confirm dialog).
Bug Fixes
COMPLETEQTY-002: Manual "Mark Filled" hand-off didn't complete the order — The row check-mark button (manual completion, for items handed over in person or mailed yourself) opened a quantity prompt whose confirm button read "Mark Sent", but entering the amount and confirming often left the order unchanged. The prompt routed through the by-name
FulfillRequest(bank, requester, item, …)path, which re-matches the request on bank + requester + item and silently no-ops if any field doesn't compare equal — so nothing was recorded and the order stayed open (only a quiet "Unable to record that quantity." status line). It now completes the request by its id via the newGuild:FulfillRequestById(requestId, count, actor): the amount is recorded into the Sent column and, once Sent reaches the requested quantity, the order is closed outright (statuscomplete, broadcast as a full snapshot so peers replicate the Sent total and terminal status); a partial amount records the Sent total and leaves the order open. The confirm button is relabelled "Mark Filled" (covers both an in-person hand-off and mail you sent yourself) and the prompt wording generalised to match. Locations:Modules/RequestLog.lua(FulfillRequestById),Modules/UI/Requests.lua(prompt text, button label, OnAccept).HITBOX-002: Bottom-row icons still dead in their bottom half (HITBOX-001 follow-up) — The HITBOX-001 lift (v1.1.2) raised each bottom-row icon to
window.frame:GetFrameLevel() + 10once, at construction time. But the Inventory/Search/Requests windows areFULLSCREEN_DIALOGAceGUI frames whose frame level jumps to a much higher value when they are shown; AceGUI'ssizer_sresize strip tracks the parent up toparentLevel + 1, while the icons stayed pinned at the stale construction-time level — so once shown, the sizer sat back above the icons and swallowed clicks/hover across the bottom ~half of every bottom-row control (and AceGUI's own Close button, which HITBOX-001 never lifted at all). Fixed with a shared helperTOGBankClassic_UI:KeepAboveResizeSizers(window, buttons)that re-asserts the lift against the live parent level on everyOnShow(plus a next-frame pass, since the final level lands just after OnShow), and that also locates and lifts AceGUI's Close button. The button set is stored on the frame and theOnShowhook is attached once per frame, so the Requests window's release/reacquire (banker-status change) and AceGUI's frame pooling neither stack hooks nor lift recycled buttons. Locations:Modules/UI.lua,Modules/UI/Inventory.lua,Modules/UI/Search.lua,Modules/UI/Requests.lua.
Improvements
- MULTIORDER-001: One mail can close several of a person's orders — When a banker mails items to a guild member, the addon now credits every matching open order that member has with that banker, instead of only one. A fully manual mail already spread across multiple orders (matched by item name); this extends the same behaviour to addon-generated mails: if the banker uses the Fulfill button and then hand-attaches extra items to "save a mail,"
OnSendMaildiffs the actual attachments against what the addon attached and records the surplus aspending.extraItems, andApplyPendingSendcredits the button's targeted order via the request's own stored item name (locale-safe, byrequestId), then spills the hand-added extras across the recipient's other open orders by name. The addon'spending.itemsis deliberately not overwritten withGetSendMailItemnames (the banker's client locale), which would break the targeted match in a mixed-locale guild. Orders assigned to a different banker are left untouched. Locations:Modules/Mail.lua(OnSendMail,ApplyPendingSend).
[v1.1.3] (2026-05-30) - Cancel-Stale Broom Icon Hotfix
Bug Fixes
- BROOM-001: Cancel-Stale button was invisible — The broom icon shipped in v1.1.2 used
Interface\Icons\INV_Broom_01, which does not exist in the Classic Era client (it rendered as the blue missing-texture box, so the bulk-cancel button appeared blank).INV_Misc_Broom_01andINV_Pet_Broomare likewise absent from the Era texture set — Classic Era ships no broom icon at all. Fixed by bundling a custom broom texture with the addon (Textures/broom.tga, a 64×64 32-bit TGA with alpha) and pointing the Cancel-Stale button at it via addon path (Interface\AddOns\TOGBankClassic\Textures\broom), so the icon renders regardless of which icons the client happens to include. The newTextures/folder ships in the build; its spec note (Textures/README.md) is excluded via.pkgmeta. Location:Modules/UI/Requests.lua,Textures/broom.tga,.pkgmeta.
[v1.1.2] (2026-05-30) - Requests Tabs, Custom Cancel Reasons & Armor Slot Filter
New Features
REQUI-001: Officer-only Settings tab in the Requests window — Added a third tab,
Settings, to the Requests window, visible only to the GM and officers (gated onCanViewOfficerNote()). It renders as an opaque overlay panel over the request list with three editable numeric fields — Archive threshold (days), Auto-cancel stale (days), and Maximum request amount (%) — mirroring the three controls previously reachable only via the Blizzard options panel. Each field commits on Enter or focus-loss and only acts when the value actually changed, so unchanged focus-loss no longer re-broadcasts. The two guild-synced settings reuse the existingTOGBankClassic_Guild:BroadcastSettings("ALERT")path (SETTINGS-001), so changes propagate guild-wide exactly as the options panel does. New methodsBuildSettingsPanel,PopulateSettings,ShowSettings. Location:Modules/UI/Requests.lua.CANCELREASON-001: Custom guild cancel reasons (officer-authored, guild-synced) — The officer Settings tab now includes a cancel-reason editor styled after the FastGuildInvite Filters tab: a
[Member] [Banker] [reason text] [Save]strip over a banded, scrolling list. Officers add custom reasons and tick Member and/or Banker to choose whether each appears in the member self-cancel dropdown, the banker-cancel dropdown, or both. The built-in flavor presets also appear in the list, greyed/read-only (no edit, no delete), each with a single native-role tick officers can clear to stop offering that preset. Custom rows are click-to-edit and have a deleteX. The whole config lives inInfo.settings.cancelReasons({ custom = { {text, member, banker} }, presetDisabled = { banker = {key=true}, member = {key=true} } }) and rides the existingBroadcastSettingspath, so every member's cancel dialog offers the same reasons. Non-officers never see the editor (Settings tab is officer-only) but consume the synced reasons. The cancel dialog now builds its list frombuildPresetReasons(role)minuspresetDisabled, plus enabled customs for that role, and always offers at least one option. New methodsBuildReasonsEditor,RefreshReasonsList,_BuildReasonRow,_ConfigureReasonRow,_OnReasonToggle,_OnReasonDelete,_OnReasonEdit,_EnsureReasonConfig. Locations:Modules/UI/Requests.lua,Modules/Guild.lua,Modules/Database.lua.FILLALL-001: "Fulfill Oldest Order" stepped button (spam-to-fill) — A new envelope icon in the Requests window's bottom-right cluster (bankers only) walks the oldest order you can fully fill from your bags through one action per click: select (sets the recipient, switches to the Send Mail tab) → split (only if a stack split is needed) → attach → send, then the next click picks the next-oldest. One WoW action per frame deliberately — the earlier single-click version raced the send ahead of the async split; stepping it lets the cursor/bag state settle between actions. The split commits into a free bag slot as its own stack (like the manual split), and ATTACH waits for it to land before grabbing it. Oldest-first (FIFO by
date, with a stable request-id tiebreak so same-second orders pick deterministically instead of appearing to jump around the list) so item contention favours whoever asked first; only orders assigned to your own character are eligible (that's the constraint for fulfillment credit). Mail collect: if the oldest serviceable order's items are sitting in your mail inbox (not bags), each click first pulls one matching item into your bags (TakeOneInboxItemFor, gated on free bag space) until enough is collected, then it selects + fulfills — so the flow now spans bags and mail, only matching your own open orders. Orders you can't cover from bags + mail are skipped. After a send, abatchInFlightguard blocks re-selecting that order until the send confirms (cleared onMAIL_SEND_SUCCESS/ApplyPendingSend, mail error/UI_ERROR_MESSAGE, or a 5s safety timer); the step state resets onMAIL_CLOSED. The status bar shows the next step at each click. NewTOGBankClassic_Mail:FulfillStep/FindOldestServiceableOrder/TakeOneInboxItemFor/ResetFulfillStep(+ inbox-match helpers), reusingCalculateFulfillmentPlan+ the existingpendingSend→FulfillRequestpath.SendMailadded to.luarc.json. Locations:Modules/Mail.lua,Modules/UI/Requests.lua,Modules/Events.lua.COMPLETEQTY-001: "Complete" now asks how much was handed over — The row's check-mark button (for items given directly, not mailed) used to silently mark the whole request complete. It now opens a quantity prompt; the number you enter is recorded in the Sent column via
Guild:FulfillRequest(request.bank, …), and the order only flips to fulfilled once Sent reaches the amount requested — so partial hand-offs are tracked correctly. NewTOGBankClassic_CompleteQtystatic popup (hasEditBox, numeric) +showCompleteQtyPrompt/ensureCompleteQtyDialog; applied against the request's own bank so it works whoever clicks (button visibility still gated byCanCompleteRequest). Location:Modules/UI/Requests.lua.HELPNOTE-001: Officer help notes on the help (?) tooltips — GM/officers can now add a custom note that appends to the bottom of the help "?" tooltip on each of the three windows (Inventory, Search, Requests) — e.g. how to submit a request and expected turnaround time. Edited in the Blizzard options panel (Esc → Options → AddOns → TOGBankClassic → Requests → "Guild Help Notes"), per window, as multi-line inputs gated to
CanViewOfficerNote(). Stored inInfo.settings.helpNotes = { inventory, search, requests }, synced guild-wide over the existingBroadcastSettingspath (sanitized on receive bySanitizeHelpNotes, clamped to 400 chars/window). The tooltips read the note at hover time via a sharedTOGBankClassic_UI:AppendGuildHelpNote(windowKey)(Inventory/Requests call it directly; Search passes a note key toAttachTooltip). NewTOGBankClassic_Guild:GetHelpNote,TOGBankClassic_Options:SetHelpNote. Locations:Modules/Guild.lua,Modules/Database.lua,Modules/Options.lua,Modules/UI.lua,Modules/UI/Inventory.lua,Modules/UI/Search.lua,Modules/UI/Requests.lua.VIEWBANK-001: View-only bank toons (visible but not requestable) — A bank character can now be flagged "view only" so its stock stays visible everywhere (inventory, search, item tooltips) while guild members are blocked from sending requests for it — e.g. a raid bank. Officers flag it by adding a view-only marker to the toon's guild note alongside the usual
gbanktag:gbank viewonly(also accepted:view-only,readonly,read-only, or the compactgbankro). NewTOGBankClassic_Guild:IsViewOnlyBank(name)(O(1) via aviewOnlyflag stored onmemberRoster, computed from notes inRefreshOnlineCache/RebuildBankerRoster, with a roster-scan fallback). Enforced in three places:Guild:AddRequesthard-rejects any request whose targetbankis view-only; the Search request dialog (ShowRequestDialog) refuses to open for a view-only banker and prints a reason; and Search result rows tag view-only banks with a(view only)marker. Items on both a normal and a view-only banker stay requestable from the normal one (requests are per-banker). Locations:Modules/Guild.lua,Modules/RequestLog.lua,Modules/UI/Search.lua.
Bug Fixes
- HITBOX-001: Bottom-row icons only clickable in a center sliver (clicks and tooltips) — The gear, help
?,</>page arrows, broom, and fulfill envelope icons that sit along the bottom edge of the Inventory, Search, and Requests windows responded to clicks and hover only in a tiny center spot. Cause: AceGUI'sFramewidget lays an invisible, mouse-enabled resize strip (sizer_s, full bottom width, 25px tall) plus a corner sizer across that whole row for the drag-to-resize handle. The parent frame is at frame level 100, so the sizers — and any icon added as a child of the same frame — all default to level 101; two mouse-enabled frames overlapping at the same level produce ambiguous hit-testing, so the sizer swallowed most of each icon's input. Fixed by lifting every bottom-row icon towindow.frame:GetFrameLevel() + 10(level 110) so it sits above the sizers and the full icon is live for both clicking and mouseover. Not a texture/SetSize/SetHitRectInsetsissue (the gear's hit-rect was already expanded and still failed). Locations:Modules/UI/Inventory.lua,Modules/UI/Search.lua,Modules/UI/Requests.lua. - FILLALL-002: Mail collect over-pulled stackable items — The "Fulfill Oldest Order" mail-collect step counted attachments pulled rather than items, so for a stackable item (where one mail attachment can be a whole stack) it kept pulling past the amount needed and filled your bags.
TakeOneInboxItemFornow returns the quantity taken (viaGetInboxItem) and the collector tracks items pulled against the deficit, stopping once enough is in your bags — correct for both single items (1 = 1) and stacks. Location:Modules/Mail.lua. - REQUI-005: Pagination "snapped back" to the first page — Clicking the
</>page arrows would jump back to page 1 a split second later. A background request sync (RefreshRequestsUI→DrawContent) was unconditionally resettingcurrentPage = 1on every redraw.DrawContentnow resets the page only when the active tab actually changed (tracked via_lastDrawnTab), andDrawRowsclampscurrentPageto the valid range so a shrinking data set can't strand the view on an empty page. Location:Modules/UI/Requests.lua.
Improvements
- OFFICERTAB-001: Options "Requests" group renamed to "Officer" and gated to officers only — The Blizzard options group (Esc → Options → AddOns → TOGBankClassic) holding the request thresholds + help notes is now titled Officer and its
hiddenfunction isnot CanViewOfficerNote(), so only the GM and officers can see or change those settings (previously bankers could too). Location:Modules/Options.lua. - REQUI-002: Requests window top strip decluttered + real tab widget — The top strip now holds only the tabs (
Requests | Archive | Settings), and they are now an AceGUITabGroup(the proper WoW tab-shaped tabs, matching FastGuildInvite) instead of redUIPanelButtons. The widget is used purely as a tab bar — its content box backdrop is removed so only the tab row shows; the request list and Settings panel still render below as separate window children. Tab selection drivescurrentTabviaOnGroupSelected; the oldUpdateTabButtonstext-prefix highlighting was removed (the tab widget shows the active tab itself). Per-tab hover tooltips useOnTabEnter/OnTabLeave. TheCancel Stalebutton and the full-width< Prev/Next >pagination buttons no longer crowd the top. Location:Modules/UI/Requests.lua. - REQUI-003: Pagination and Cancel Stale moved to compact status-bar icons — The
< Prev/Next >buttons are now compact page-turn arrow icons (UI-SpellbookIcon-PrevPage/NextPage) next to the bottom-right help?icon, dimming automatically at the first/last page. TheCancel Staleaction is now a small broom icon (Interface\Icons\INV_Broom_01, the Hallow's End Magic Broom texture) in the same cluster, shown only to officers/bankers. The status bar's right edge auto-shrinks to clear the icon cluster (wider when the broom is present). On the Settings tab these icons are hidden since they don't apply. Location:Modules/UI/Requests.lua. - REQUI-004: Settings panel compacted + tooltip cleanup — The three numeric settings (Archive, Auto-cancel, Max request %) now sit on a single compact row instead of three stacked label+description blocks, freeing ~110px the cancel-reason list now uses. The redundant "Request Settings" title was removed (the tab already says Settings). Field descriptions now live on the label's hover tooltip rather than the edit box, so the tooltip no longer covers the field while typing. The cancel-reason
Mbr/Bnk/Reasoncolumn headers and the "Custom Cancel Reasons" heading now have hover tooltips (the heading's how-to text was moved off-screen into its tooltip), via a newattachLabelTooltiphelper that overlays a hit frame on a FontString. Location:Modules/UI/Requests.lua. - REQUI-006: Bottom status-bar row tidied — The status bar now extends right to meet the icon cluster instead of stopping ~22px short, and every bottom-row icon (help
?,</>page arrows, broom, fulfill envelope) is the same size (22px) with equal 8px gaps. The cluster sits in the gap left of the AceGUI Close button (which occupies x -127..-27), with the help icon at -133 so it never overlaps Close. Rather than a hardcoded right-edge inset, the status bar'sBOTTOMRIGHTis anchored 6px to the left of whichever icon is actually leftmost (self.FulfillOldestBtn or self.CancelStaleBtn or prevPageBtn), so the bar always meets the cluster with even spacing regardless of which icons a given user has — fixing the large gap that appeared when the old fixed offsets didn't match the real cluster width. Location:Modules/UI/Requests.lua. - REQUI-007: Clickable text column headers (no more red buttons) — The request-table column headers were red
UIPanelButtons whose centered text didn't line up with the data cells below. They are now plainInteractiveLabelsort headers — gold text with a hover glow, click to sort, sort arrow appended — each justified to match its column's data so headers and rows align. Mirrors the FastGuildInvite RowList header style. A per-columnheaderAlignoverride centers theItemheader, and aheaderSuffix(trailing space) nudges the right-justified#header in by one character so it sits over the first digit of the quantity rather than thex.EnsureHeaderRowsbuildsInteractiveLabels instead ofButtons. Location:Modules/UI/Requests.lua. - REQUI-008: Tightened vertical spacing above the request list — The tab strip's
TabGroupheight was trimmed (34 → 30) to close the gap between the tabs and the filter dropdowns, and the column-header row gained ~3px of breathing room above it (header group contentyoffset0 → -3). Location:Modules/UI/Requests.lua. - SEARCH-001: Armor equip-slot filter in the Search window — When the Filter is set to
Type → Armor, a newSlotdropdown (between the subtype and Sort dropdowns) lets you narrow results to a specific equip slot — Head, Shoulder, Chest, Wrist, Hands, Waist, Legs, Feet, Back, Neck, Finger, Trinket, Shield, Held In Off-hand, or Relic. It combines with the existing armor subclass (Cloth/Leather/Mail/Plate) filter, so e.g.Plate + Legsworks. The dropdown is disabled for non-armor types. Items' equip slot is resolved on demand fromGetItemInfo(#9) and cached on the item'sInfotable (equipSlot), so no change to the synced data schema;INVTYPE_CHEST/INVTYPE_ROBEcollapse to oneChestentry, etc. NewSLOT_LIST/SLOT_ORDER/INVTYPE_TO_SLOTtables, aresolveSlotKeyhelper, asubSlotDropdownwidget +resetSlotcascade,self.SubFilterSlotmatching inSubFilterMatches. Location:Modules/UI/Search.lua.
Internal
- Pagination buttons are now raw
Buttonframes (with normal/pushed/disabled/highlight textures) rather than AceGUI buttons; a file-localsetBtnEnabledhelper replaces the oldSetDisabledcalls at the two page-state update sites. The Settings overlay is aBackdropTemplateframe rebuilt per window; its references (SettingsOverlay, the three editboxes,SettingsTabBtn, and the cancel-reason editor widgets/row pool) are cleared in the window's reset block, andCancelStaleBtnis cleared before its conditional creation so a lost-banker-status window recreation doesn't read a stale reference. Location:Modules/UI/Requests.lua. - CANCELREASON-001 sync/storage:
cancelReasonsis added to theguild-settingsbroadcast payload and validated on receive by a newTOGBankClassic_Guild.SanitizeCancelReasonshelper (clamps to 20 custom reasons × 160 chars, coerces booleans, ignores a missing field so old clients don't wipe local state). Defaults and a migration block were added to bothInfo.settingsinit sites inModules/Database.lua. The built-in flavor presets were extracted from the cancel dialog into a sharedbuildPresetReasons(role)builder (keyed so they can be individually disabled). Astrtrimglobal was added to.luarc.json. Locations:Modules/Guild.lua,Modules/Database.lua,Modules/UI/Requests.lua,Modules/Constants.lua(SETTINGS tag description).
[v1.1.1] (2026-05-29) - Sorting Fixes & Random-Suffix Request Variants
Bug Fixes
SORT-001: "By Type" split same-material gear across equip slots — The inventory "By Type" sort ordered items by item class → equip slot → subclass, so a player's cloth pieces were broken up by slot and interleaved with leather/mail (e.g. 6 cloth, 3 leather, then 1 more cloth) instead of grouping all cloth together. Reordered the comparator to class → subclass/material → required level (see SORT-003) → equip slot → rarity → name, so all cloth groups, then all leather, then all mail. Location:
Modules/Item.luaSort(typemode). The Search window had notypesort case at all (selecting "By Type" left results in scan order); added a matching comparator there. Location:Modules/UI/Search.luaDrawContentsort block.SORT-002: "Level" sort and the Min/Max level filters used item level, not required level —
Info.levelwas populated fromGetItemInfo's item-level return (#4) but was treated everywhere as the required-to-use level — in the "Level (High/Low)" sort, the Search window's "Minimum/Maximum Required Level" filters, and the "usable by my level" filter. Because item level and required level diverge non-monotonically, the "High to Low" sort looked like it descended, jumped back up, and repeated rather than producing a clean ordering. Now captures the required-level return (#5) into a newInfo.reqLevelfield and uses it for the level sort and all three level filters. Required level is resolved from the live item cache (GetItemInfo), which is warm by the time items are on screen. Items that arrive with a pre-existingInfotable — item data synced from other players or loaded from saved data predates the field — are resolved at sort time, retrying wheneverreqLevelis unresolved (nil or 0) and only writing a positive result. This avoids the bug where a 0 written during a cold-cache window stuck permanently and broke the ordering. Locations:Modules/Item.lua(Info captures,Sortprep + level comparators,GetItemsbackfill,GetInfo),Modules/UI/Search.lua(sort-prep resolution, level comparators,SubFilterMatches).REQ-003: Requests for random-suffix items matched the wrong variant — Random-property gear such as "Spiked Club of the Tiger" and "Spiked Club of the Monkey" share a single base item ID and differ only by their random-suffix ID. Because a request stored only the numeric item ID, the requests screen tooltip and the mail fulfillment/availability checks matched the first item sharing that base ID — so a request for the Tiger variant showed (and would be fulfilled by) the Monkey variant. Requests now also capture the random-suffix ID and match on it: the tooltip resolves to the requested variant, and bag scanning / fulfillment only count the matching suffix. New optional
suffixIDfield appended to the request record and thetogbank-rd2wire format (slot 13, append-only); older clients and pre-existing requests have no suffix data and fall back to the previous item-ID matching, so there is no regression. New helperTOGBankClassic_Item:GetSuffixID(link). Locations:Modules/Item.lua,Modules/RequestLog.lua,Modules/UI/Search.lua(request creation),Modules/UI/Requests.lua(tooltip),Modules/Bank.lua(FindItemsByName/CountItemInBags),Modules/Mail.lua(CanFulfillRequest/PrepareFulfillMail).
Improvements
- SORT-003: "By Type" now orders each material by level — Within each material/subclass group, items are ordered by required-to-use level high→low (then equip slot, rarity, name as tie-breakers), so a type-sorted list reads cleanly within each group (all plate: 50, 49, 48…) instead of relying on slot/name alone. Applies to both the inventory and Search windows. Locations:
Modules/Item.luaSort(typemode),Modules/UI/Search.luaDrawContentsort block.
Internal
.luarc.json— Addedstrsplittodiagnostics.globals(used by the newGetSuffixIDhelper).
[v1.1.0] (2026-05-23) - Data Corruption Fix: Linkless Gear Ghosts & Inflated Counts
Bug Fixes
ITEM-004:
EnsureLegacyFieldswas poisoningalt.bank.itemswith mail-item references — When peer-relayed alt data arrived carrying onlyalt.items(the aggregated bank+bags+mail view) without the separatebank/bags/mailfields,EnsureLegacyFields"reconstructed"alt.bank.itemsby copying every entry fromalt.items— including mail items. Subsequent re-aggregation inApplyDeltathen ranAggregate(bank, bags)followed byAggregate(result, mail), summing mail items twice per delta application. Across many peer-relay cycles, gear item counts inflated monotonically — in real SavedVariables, "Battlefell Sabre of Power" (ID=15220) reached Count=6237 and base "Battlefell Sabre" reached Count=21 (both physically impossible for non-stacking weapons). Fix removes the copy loop entirely; the next direct delta from the actual banker repopulatesbank.itemscleanly. Location:Modules/Guild.luaEnsureLegacyFields(~line 2282). Root cause documented indocs/DELTA_BUGS.mdITEM-004.ITEM-003 guard holes on
ApplyItemDeltaupdate/fallback paths — The receive-side guard against linkless weapons/armor only fired on the new-insert paths. The ID-only fallback paths in both STEP 2 (modified) at line 904 and STEP 3 (added) at line 1010 silently mutated linkless gear ghost entries into suffixed entries viafor field, value in pairs(changes) do existingItem[field] = value endandexistingItem.Count = newItem.Count. This propagated whatever Count the inbound delta carried into a ghost that should never have existed, causing count divergence across replicas. Fix detects ID-only-fallback matches against linkless gear and DROPS the ghost before falling through to the clean-add path, where the existing ITEM-003 new-insert guard catches subsequent linkless gear payloads. Location:Modules/DeltaComms.luaApplyItemDeltaSTEP 2 and STEP 3.NeedsLink/ItemClassNeedsLinkcould strip gear links during cold-cache windows — The fallback path inNeedsLinkconsulted the item's hyperlink suffix field whenGetItemInfo's class lookup returned nil (uncached). For base/no-suffix gear items the suffix is 0, so the fallback returned false and stripped the link, producing the linkless gear ghosts that ITEM-003 / ITEM-004 then propagated. Replaced both functions with a default-deny strip policy: a link is stripped ONLY when class can be positively confirmed as non-gear (class != 2 AND != 4). Uncached, unparseable, or unknown items now preserve the link. The "Weapons (class 2) and Armor (class 4) ALWAYS keep their Link" rule documented in the file finally actually holds. Location:Modules/Item.luaNeedsLink,ItemClassNeedsLink, plus newItem:GetClass(itemID)tiered-lookup helper.
New Features
Generic tooltip helper
TOGBankClassic_UI:AttachTooltip(target, anchor, title, lines)— Single one-call API for non-item tooltips. Auto-detects AceGUI widget vs raw frame and wiresOnEnter/OnLeavevia the right API. Replaces the 5-lineGameTooltip:SetOwner/ClearLines/AddLine/Showscriptlet pattern that was sprinkled across UI modules. New Search-window tooltips use it; existing call-sites kept as-is for now (gradual migration). Location:Modules/UI.lua(~line 230).Search window: info "i" icon + Prev/Next at bottom-right — Mirrors the inventory window's bottom-right layout. The "?" help icon explains how the Search window works (input field, filters, pagination). Pagination buttons moved from a full-width "< Previous / Next >" row to compact
</>icon-sized buttons next to the close button — saves ~30px of vertical space, freeing the result list. Status bar shrunk by ~210px to leave room. Both pagination buttons keep the existing:SetDisabled(bool)API soDrawContent's page-state logic works unchanged. Location:Modules/UI/Search.luabottom-right control row.Search window: tooltips on Min lvl / Max lvl / Usable — All three filter controls now have explanatory hover tooltips wired via the new
AttachTooltiphelper. Min/Max explain that empty/0 means "no constraint" and that items without a level are hidden when a min is set. Usable explains the gating (disabled until a Type/Quality is picked).Search window: Sort tooltip moved from dropdown control to label — Previously the Sort tooltip fired when hovering the dropdown itself, which competed with the click-to-open-dropdown gesture (popped up while the user was trying to click). Now it lives on a hit frame over the "Sort" label, matching the Filter dropdown's pattern.
Search window: filter row reordered (Min lvl, Max lvl, then the rest) — The numeric inputs now lead the row so the small controls cluster densely in the top-left and don't get orphaned on their own row when the window is narrow.
Search window: Min/Max EditBox labels repositioned — Labels shifted 5px right (+5, -2) so they no longer overhang the EditBox's left edge.
Min/Max level filter in the Search window — Two new compact numeric inputs (
Min lvlandMax lvl, 60px each) let players filter results by the item's required level. Empty or non-numeric input is treated as "no constraint" so partial ranges work (just a min, just a max, or both). Cheap to compute → no gating on other filters being set first. Combines with the existing Type/Quality/Usable filters. Location:Modules/UI/Search.luaSubFilterMatchesand filter section.Compact, auto-wrapping filter row in the Search window — The filters used to be four full-width-stacked dropdowns plus an inline checkbox glued to the Filter dropdown's right edge — five tall rows that ate half the window before the results even started. They're now a single Flow-laid-out row with each control sized to its content (Filter 110px, Subtype 130, Sub-subtype 130, Sort 150, Min lvl 60, Max lvl 60, Usable 80 — total ~720px). On a wide search window everything fits on one row; resize the window narrower and they wrap onto multiple rows automatically. The "Usable by my level" checkbox is now a standalone AceGUI CheckBox (previously a raw CheckButton anchored to the Filter dropdown's frame), so it participates in the wrap layout instead of forcing the Filter dropdown to stay 165px wider than it needs to be. Location:
Modules/UI/Search.luafilter section.Settings gear icon on the main inventory window — A new ⚙ button sits next to the existing help "?" icon at the bottom-right of the inventory window. Clicking it opens the TOGBankClassic options panel directly (equivalent to Escape → Options → AddOns → TOGBankClassic), so players don't have to navigate through the game menu to change banker/scan configuration, minimap button, debug settings, etc. Hover for a tooltip. Location:
Modules/UI/Inventory.lua(~line 144).One-shot ghost-purge migration on
Database:Init— Scheduled 30 seconds after addon init (gives WoW's item cache time to warm). Walks every alt'sitems,bank.items,bags.items, andmail.itemsarrays; drops entries that have noLinkfield AND are confirmed byItemClassNeedsLinkto be class 2/4 gear. Recovers existing corruption in SavedVariables without requiring/togbank wipe. Always prints a result line so users know it ran (purged count + skipped-suspect count, even when zero). Can be manually re-run via/togbank dev purgeghosts. Location:Modules/Database.luaPurgeLinklessGearGhosts.Static item DB populated from wago.tools (
Modules/Static/ItemDB.lua+SuffixDB.lua) — Ships with ~24,000 item entries (every item in Classic Era 1.15.8) and ~2,000 random-suffix fragments.NeedsLink/ItemClassNeedsLinkconsultTOGBankClassic_ItemDBfirst via a tiered lookup (static DB →GetItemInfo→ default-deny), so strip decisions no longer depend on the volatile WoW client cache. Regenerated bytools/build-itemdb.pywhich pulls Blizzard's actual DB2 dumps (ItemSparse + Item + ItemRandomProperties + ItemRandomSuffix). Wire schema unchanged in this release; bandwidth-reduction changes (Phase 3) will land in a follow-up release.tools/build-itemdb.py— wago.tools fetch + Lua generator — Python script (no third-party deps, Python 3.9+) that fetches DB2 tables from wago.tools, joins, filters suffix junk (rejects fragments not starting with "of "), and emitsModules/Static/{ItemDB,SuffixDB}.lua. Caches downloaded CSVs undertools/wago_cache/(gitignored). Re-run when a new Classic patch ships new items. Pattern modelled on TOGProfessionMaster'stools/wago_probe.py. Excluded from packaged builds (.pkgmetatools/entry).Developer-only command namespace
/togbank dev <subcommand>— Twenty-two dev/debug commands previously listed in/togbank help(clearhistory, clearsnapshots, deltaerrors, deltahistory, deltastats, forcedelta, forcefull, perfstats, persistcheck, protocol, resetmetrics, test, versioncheck, hashupdate, hashdebug, hashdump, netq, reqscan, debugdump, debuglogsave, clear-delta-errors, plus the new purgeghosts) are now hidden from the user-facing help output and dispatched only via thedevnamespace./togbank dev helplists them for developers. Reduces the top-level command list from ~30 to ~11 entries. NewDEV_COMMAND_NAMESlookup inModules/Chat.luacontrols which commands route through the dev dispatcher — flipping a command between user-facing and dev-only is a one-line change. Full catalogue and developer workflows documented indocs/DEV_COMMANDS.md(not packaged to users —docs/is ignored in.pkgmeta)./togbank dev purgeghosts— manual ghost-purge trigger — Re-runs the linkless-gear-ghost migration on demand. With the populated staticTOGBankClassic_ItemDBshipping in this release, the purge can confidently classify almost any item without relying on the WoW client's session cache. Location:Modules/Chat.luaCOMMAND_REGISTRY.
Internal
Removed obsolete local-build pipeline — Deleted
package.bat(referenced a no-longer-existingembeds.xmland would have errored on run) and the staledist/directory (contained one orphanTOGBankClassic.TOGBankClassic-v1.3.2.zipfrom before the move to the BigWigs CurseForge packager). All release builds now flow exclusively through.pkgmeta+ the CurseForge auto-builder. Defensive**/*.batignore pattern kept in.pkgmetato catch any future leftover scripts.Removed dead
function s(a)atGuild.lua:3030— Generic table-entry counter, defined as a global (lowercase), never called from anywhere in the codebase. Eliminating it removes onelowercase-globalwarning and twounused-localhints (c,dloop variables).CLAUDE.md updated — Replaced the references to
package.bat/dist/(now gone) with notes on the current packaging pipeline. The "scratch files usetmpclaude-prefix" rule is preserved but no longer mentions the obsolete robocopy exclusion.Documentation cleanup — Removed duplicate
docs/CHANGELOG.md(the canonical changelog has always been the repo-rootCHANGELOG.md). Updated.pkgmetaignore list:docs/directory now fully excluded from packaged builds (was:*.mdonly),CLAUDE.mdexplicitly excluded,*.mdno longer blanket-ignored so root-levelCHANGELOG.mdships to CurseForge as intended.README.txt cleanup — Removed dev commands from the EXPERT COMMANDS section. Rewrote MONITORING DELTA SYNC and two TROUBLESHOOTING entries to direct players at debug logging instead of dev-only counters. Kept genuinely user-facing expert commands:
compact,debuglog/debuglogclear/debuglogstats/debugtab/debugtabremove,roster,wipe,wipeall,wipeframes,debug.Tests.lualint fixes — Suppressed twoduplicate-set-fieldwarnings on the mockedDatabase.GetGuildDeltaSupportreassignments using the established---@diagnostic disable-next-linepattern.Minor lint cleanup in
Chat.lua— Removed an unused vararg and an unused loop-variable name inProcessQueueandPrintDeltaHistoryrespectively (encountered while editing the dispatcher)..luarc.jsonglobal registration — AddedTOGBankClassic_ItemDB,TOGBankClassic_SuffixDBtodiagnostics.globals.TOC additions — new
Modules/Static/ItemDB.luaandModules/Static/SuffixDB.luaload entries (loaded early so anything that queries item class has them available).
Developer / Sync architecture follow-ups (planned, not in this release)
- Bump
PROTOCOL.VERSIONto 3 once the static DB is populated and committed. - Peer-aware
StripDeltaLinks: emit minimal{ID, Count, suffixID?, randomProperty?}payload to peers known to support the static DB; continue sending legacy{ID, Count, Link/ItemString}to old peers. Backwards-compatible per Option A in the design discussion. - Update
ApplyItemDeltaandReceiveAltDatato reconstruct items from minimal payloads usingTOGBankClassic_ItemDBandTOGBankClassic_SuffixDB. - Expected wire bandwidth reduction: 4-5x for non-gear items, 5-7x for random-suffix gear, 8-10x for fixed-roll gear once everyone is on the new protocol.
[v1.0.0] (2026-04-11) - First Stable Release: Fulfill Location Awareness & Polish
New Features
- Fulfill button location awareness — The fulfill button now shows distinct icons and contextual tooltips for three new states, making it clear why an item cannot be mailed immediately and where to find it:
- Item in mail inbox — wax letter icon (
INV_Letter_06); tooltip: "Item is in your mail inbox. Retrieve it first, then fulfill the order." - Item split across bank and mail — paired bag + letter icons; tooltip: "Item is split between your mail inbox and bank. Retrieve mail items first, then pick up the rest from the bank."
- Shortage — more available in bank/mail — contextual icon matching the location; tooltip shows exact current bag count and target quantity (e.g. "Have 125 in bags. More available in your bank and mail inbox — pick up or retrieve the rest to reach 150."). Three sub-states: bank only, mail only, or both.
- Item in mail inbox — wax letter icon (
Bug Fixes
TOOLTIP-001: Item link tooltips showed banker data for ex-guild members — The
OnTooltipSetItemhook inTooltipBankerInfo.luaiterated all database entries with no guild membership check, surfacing data from characters who had left the guild. Fixed by adding anIsInCurrentGuildRoster()check as a combined guard — only alts currently inmemberRoster(O(1) lookup) are shown. Location:Modules/TooltipBankerInfo.lua.FULFILL-001: Fulfill button icon stuck on shovel after a bag split — After splitting a stack to fulfill an order, the bag-update event called
DrawRows(), which skips non-dirty rows. The row was already drawn soDrawRows()was a no-op and the split icon never transitioned. Fixed by replacing theDrawRows()call inOnBagUpdatewith_RefreshFulfillButtons(), which re-evaluates all visible rows regardless of dirty state. Location:Modules/UI/Requests.lua.FULFILL-003: "Item in bank and mail" showed a blank red button — The combined icon used
INV_Misc_Chest_01, which does not exist in Classic Era; the engine renders a blank red placeholder for any missing texture. Fixed by replacing it with two confirmed-working icons rendered side-by-side at 14px:INV_Misc_Bag_07(bag) andINV_Letter_06(wax letter). Location:Modules/UI/Requests.lua.HIGHLIGHT-001: Bagnon bag highlighting broken for recipe and pattern items —
UpdateBagnonHighlightinginserted raw item names into the Bagnon search string. Tradeskill items whose names include a colon prefix (Pattern: Ironfeather Breastplate,Formula: Enchant Weapon, etc.) caused Bagnon's search parser to silently discard the entire term. Fixed with a sharedstripRecipePrefixhelper that strips all known Blizzard craft prefixes before appending to the search string. Location:Modules/ItemHighlight.lua.
Older releases (v0.10.10 and earlier) are archived in CHANGELOG_ARCHIVE.md.
All Relations
- All Relations
- Embedded Library
- Optional Dependency
- Required Dependency
- Tool
- Incompatible
- Include

