promotional bannermobile promotional banner

WarbandMeDowns

Recommends twinks for warbound gear
Back to Files

0.7.0

File nameWarbandMeDowns-0.7.0.zip
Uploader
sgadesgade
Uploaded
Aug 24, 2026
Downloads
15
Size
93.9 KB
Flavors
Retail
File ID
8726348
Type
R
Release
Supported game versions
  • 12.1.0

What's new

WarbandMeDowns

0.7.0 (2026-08-24)

Full Changelog Previous Releases

  • perf: stop settling the whole warband in the frame that opens the panel
    Opening the settings page or running /wmd ranks went through
    GetWarbandRanking -> GetProjectedItemLevelsForWarband -> SettleAllGroups, which
    ran the entire warband assignment in one frame - the same global settlement the
    tooltip does lazily, one slot group at a time. With Pawn installed and a scale
    resolving for every character, that asks Pawn to evaluate every warbound item in
    the warband, and Pawn's cache-miss path calls SetHyperlink on a hidden tooltip
    and parses every line. Several hundred of those in one frame is a visible stall.
    Only the Theoretical iLvl column ever needed it. The ordering, Level, iLvl and
    Max iLvl already fall out of the scan Recompute performs anyway.
    Settlement is now incremental: one group per timer tick, kicked off by the
    existing debounced background recompute so it is usually finished before
    anything asks. One group is the finest grain available - SettleGroup mutates
    claims across the pool and has to complete atomically. A Recompute mid-run
    cancels the outstanding timer and abandons the queue rather than continuing
    against a pool that has been replaced.
    Until it finishes, the theoretical number has no honest value, so it is reported
    as pending rather than computed from half the claims: the panel shows an
    ellipsis and /wmd ranks prints a note. No automatic reprint of the table - a
    second unasked copy in chat is worse than one missing column.
    Also memoizes item metadata per generation. Every fact the engine asks about an
    item is a property of the link, which never changes, yet one refresh made ~8000
    C_Item calls against ~1400 distinct links. Only resolved items are cached:
    asking is what queues the client's async load, and "unknown" is a transient
    state the engine depends on re-checking, so caching a nil would freeze an alt's
    gear as permanently unreadable. Dropped per generation rather than per session
    because heirloom item levels scale with the level of the character looking.
    Measured in the harness at this account's real volumes (22 characters, 374
    containers, 1126 items), for the frame that draws the screen: 8171 -> 2800
    C_Item calls, and 476 -> 0 Pawn tooltip parses.
    Note the contract change: theoreticalItemLevel is nil until the warband is
    fully settled, so callers must check theoreticalPending.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01LMfxdSN9SBaVJKDJnrH3ea
  • fix(assignment): only claim candidates the character can actually wear
    Two subclasses were unmapped and warned on load: Armor 5 is Cosmetic
    (transmog-only gear) and Weapon 14 is Generic, the Miscellaneous auction
    subcategory - mining picks, blacksmith hammers and other equippable oddities.
    Both are genuine Armor/Weapon items with an equip location, so they reach the
    eligibility check like anything else. They are now mapped explicitly to an
    empty class list rather than left out of the tables: a missing subclass means
    "not taught about this yet" and warns accordingly, which is a different
    statement from "nobody wears this for stats".
    Mapping them was not enough on its own. Data.SlotClassKey collapses all armor
    into one key, so the candidate union for a plate chest also holds every cloth,
    leather and cosmetic chest in the warband. SettleGroup vetted only the group's
    representative subclass, and _ClaimBestCandidate never re-checked per
    candidate - so a warrior sitting on a plate 620 upgrade and a cloth 700 had the
    cloth claimed for them, left the plate unclaimed, and was told to vendor a real
    upgrade. Cosmetics with a nominal item level did the same thing, which is how
    these two subclasses turned a latent bug into a visible wrong answer.
    Candidates and floor items are now narrowed to what each character can actually
    equip before anything is scored or claimed, reusing Characters.CanCharacterEquipItem.
    The entry tables are the same objects, so a claim still removes the entry from
    every other group it could have satisfied.
    The bug predates cosmetics - cloth versus plate triggers it with no cosmetic
    involved - so this is a correctness fix, not just warning cleanup.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01Wj3G1QpAvXbSMybXRbb2f9
  • fix(diagnostics): report the recompute once, and in packaged builds too
    /wmd refresh printed two lines: the timing from Recompute and a bare
    "recomputed." from the command handler. Dropping the redundant one would have
    left packaged builds with no feedback at all, since the timing line sits in an
    --@debug@ block that the packager strips from everything it builds - it is
    visible only when running from a git checkout.
    Recompute now takes a userRequested flag and decides in one line the packager
    can rewrite:
    local announce = userRequested
    --@debug@
    announce = true
    --@end-debug@
    so a recompute the player asked for always reports back, while the automatic
    ones - which fire on every bag update and equipment change, and would be chat
    spam - stay development-only.
    The elapsed-time capture moves out of the debug block accordingly.
    debugprofilestop is a normal retail API despite the name, present in every
    build, so nothing there depends on a debug flag.
    /wmd refresh calls Recompute(true) directly instead of MarkDirty +
    EnsureFresh. That is how the flag gets through, and it is more honest about
    what the command does: the old pairing set the dirty flag purely so EnsureFresh
    would not no-op, and left a debounce timer to fire half a second later and find
    nothing to do.
    Verified by stripping --@debug@ blocks the way the packager does and running
    both trees: /wmd refresh prints one line in each, and an automatic recompute
    prints one line in the checkout and none in the packaged copy.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01Uz4YnTwQ4X4a8sXR7ZKZoB
  • refactor(tooltip): register cache-invalidation events with AceEvent
    Tooltip.lua created its own CacheInvalidationFrame, wired an OnEvent script to
    it, looped RegisterEvent over thirteen events, unregistered them by hand in
    OnDisable, and guarded the frame's existence in two places. AceEvent does all
    of that: self:RegisterEvent(name, "OnCacheInvalidated"), and AceAddon calls
    AceEvent's OnEmbedDisable on disable, which unregisters everything.
    The pcall around each registration stays. AceEvent routes to the same
    frame:RegisterEvent underneath, so an event name a given client does not know
    still raises, and one unsupported event must not take the rest of the list
    with it.
    Costs two embedded libraries rather than one: AceEvent-3.0 depends on
    CallbackHandler-1.0. Both are copied from the same Ace3 distribution the
    already-committed AceAddon-3.0 (13) and AceConsole-3.0 (7) came from, so
    nothing is mixed. .pkgmeta already lists ace3, which covers them at package
    time.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01H8H1EnZE9DKAWMWMHc31L7
  • refactor(assignment): debounce recomputes with AceTimer instead of a token
    MarkDirty kept an incrementing _debounceToken and TryBackgroundRecompute
    compared against it, so that a C_Timer.After callback armed by an earlier
    invalidation could recognise itself as superseded and bail. AceTimer expresses
    the same thing directly: cancel the outstanding timer, schedule a new one.
    AceTimer-3.0 has no dependencies of its own, and its OnEmbedDisable cancels
    every timer when the addon is disabled, so Reset no longer has to think about
    a callback still in flight.
    The timer methods are called on the addon object rather than on Assignment,
    which is a plain namespace table and not an AceAddon module - the one
    non-obvious part, so it is commented at the call site.
    Unchanged: the dirty flag is still set immediately on every invalidation, the
    timer remains a warm-up only, EnsureFresh is still the guaranteed path, and a
    background recompute is still skipped in combat.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01H8H1EnZE9DKAWMWMHc31L7
  • refactor(diagnostics): parse /wmd arguments with AceConsole:GetArgs
    HandleChatCommand split its input with a hand-rolled
    ^%s*(%S*)%s*(.-)%s*$, and ParseItemArgument trimmed what was left. That
    survives a shift-clicked item link only because everything after the first word
    is dumped into one string - a link is not one whitespace-delimited word, so any
    command wanting a second argument after one would have broken.
    AceConsole:GetArgs is already embedded and already mixed into the addon object.
    It walks past |H...|h...|h hyperlinks and |T...|t textures explicitly, hands a
    full coloured link back as a single argument, trims for us, and returns nil
    rather than an empty string for an argument that is not there.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01H8H1EnZE9DKAWMWMHc31L7
  • refactor: use Blizzard's wipe and tContains instead of WarbandMeDowns.Util
    Util held exactly two functions, arrayContains and clearTable, which are
    Blizzard's own tContains and wipe globals under different names. Both are
    long-standing retail globals - 170 addons in a stock install call wipe
    unguarded - so there is nothing to gain by carrying private copies.
    tContains needs one bit of care. Blizzard's has historically answered 1/nil
    rather than true/false. Two of the three call sites only branch on it, where
    that is immaterial, but Data.CanItemBeSentToTwink returns the value and is
    annotated as returning a boolean, so it coerces explicitly and is correct under
    either contract rather than depending on which one the current client ships.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01H8H1EnZE9DKAWMWMHc31L7
  • refactor: share one warband ranking between the engine, the panel and /wmd
    Three places ranked the warband independently: Recompute sorted into
    _sortedCharacters, and both Settings.RefreshTable and Diagnostics.PrintRanks
    called GetSortedWarbandCharacters again for themselves. They agreed only
    because the comparator is deterministic - nothing enforced it. The panel
    re-sorted from live DataStore state, so a level-up between the recompute and
    opening it would show an order the engine never settled under, and /wmd ranks,
    whose whole job is explaining that order, was a third opinion. The two row
    builders also disagreed about missing data: PrintRanks coerced an unknown item
    level to 0, GetRowData rendered an em dash.
    Assignment:GetRankedCharacters() is now the one order - the engine's own array,
    after EnsureFresh. Characters.GetWarbandRanking() layers the numbers both
    consumers display onto it, so the panel and the console format from one record.
    GetSortedWarbandCharacters is left as the sort itself and is called only by
    Recompute. /wmd ranks gains a theo column as a result, so both views show the
    same four numbers for the same character in the same order.
    GetDisplayName walked the whole warband on every call to look for a name
    collision, which is a per-row cost in /wmd ranks and in ExplainItem's table -
    O(n^2) for one printed table, and worse once GetWarbandCharacters started
    sorting. The collision map is now built once, and the roster is memoized
    alongside it, both dropped by Recompute/Reset with the other per-generation
    memos.
    Measured in the offline harness on a 22-character warband, one panel refresh
    plus one /wmd ranks: warband sorts 3 -> 1, comparator calls 270 -> 90,
    GetWarbandCharacters rebuilds 27 -> 5.
    Also folds the settings table's four hand-synchronised column definitions (a
    widths table, two creation chains, a styling list) into one ordered descriptor,
    and extracts the collect-keys-and-sort idiom the three bucket walks in
    Assignment each repeated into a SortedKeys helper - that idiom is what the
    engine's determinism rests on and should exist once.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01FEeqXLQP4x1DcSbSPnBv8N
  • fix(itemlevel): stop counting the off hand as empty for two-handed wielders
    Max iLvl read 316 on a character equipped at 295 whose real ceiling was ~298.
    The off hand slot was scored as 0 because nothing is in it, so any weapon,
    shield or holdable in the bags looked like it was filling a naked slot and was
    awarded its full item level as gain - an entire extra item, worth about +19.
    Blizzard does not score it that way: a two-hander counts twice, once for each
    weapon slot. The stored snapshot forces that reading - an averageItemLvl of
    295.1875 is an exact multiple of 1/16 and not of 1/15, so there really are
    sixteen contributions, and if the off hand were one of them at zero then that
    character's fifteen equipped items would have to average 315 while the
    character sheet reads 295. 14 of the 22 characters in the account have a filled
    main hand and an empty off hand, so this was most of the warband.
    While a two-hander is equipped the off hand's baseline is now the main hand's
    item level; a two-handed candidate is weighed against what both slots
    contribute together and gains against both when it wins (a real two-hand
    upgrade used to be under-counted by half); and nothing may be dropped into the
    off hand on its own, since doing that really means swapping the main hand too,
    which this model does not attempt - that errs toward under-counting, the safe
    direction here.
    Ranged locations are handled as two-handed as well. INVSLOT_RANGED is dead in
    retail - a bow equips to the main hand, and slot 18 is empty on all 22
    characters - but Data.EquipLocToSlotID still points INVTYPE_RANGED and friends
    at it, so a bagged bow was invisible to the projection. Routed around locally
    rather than changing a table the assignment engine also reads; the engine side
    is noted in docs/DATA_SOURCES.md and left alone.
    Adds /wmd ilvl [name], which prints the per-slot account behind the number
    alongside Blizzard's overall item level (the second, previously unused return
    of DataStore:GetAverageItemLevel) as a cross-check. The number was otherwise
    unauditable, which is exactly how a phantom slot went unnoticed.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01UDdb9jSCzJ4iucdcstxSxs
  • feat(assignment): rank the warband by projected instead of equipped item level
    CharacterPriorityComparator broke ties on DataStore:GetAverageItemLevel, which
    under-rated exactly the character this addon exists to help: someone sitting on
    a pile of upgrades they have not put on looks weak, ranks low, and is therefore
    sent even more gear. The tiebreak moves to the Max iLvl projection - where they
    would land having equipped everything usable already in their bags, bank and
    mail.
    Not the theoretical projection: that one is derived from the engine's claims,
    which are decided by walking this very order, so it would be circular and
    self-reinforcing - gear assigned to a character would raise their priority and
    so assign them more gear.
    Recompute now runs in two passes, because the order it needs is derived from
    the scan. Pass 1 scans under a plain character-key order, which is safe because
    ScanWarband's output does not depend on its walk order (_SortBuckets
    re-normalizes every bucket and every entriesByLink list afterwards); pass 2
    computes the real priority order, after the scan and before anything settles.
    ItemLevel.GetMaxItemLevel reads the pool exactly as it stands and never calls
    EnsureFresh or SettleAllGroups, so the comparator cannot recurse into
    Recompute, and its per-generation cache is cleared alongside the other memos.
    Two determinism fixes fall out of touching this: GetWarbandCharacters now
    returns a key-sorted array rather than pairs order, and the comparator is a
    total order down to the character key - table.sort is not stable, so without
    both, characters equal on level and item level came out in an order the session
    decided.
    /wmd ranks prints max alongside ilvl, since ilvl is no longer the sort key.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Claude-Session: https://claude.ai/code/session\_01M13qUXhBjhTdb3hgnFT7GX

This mod has no additional files