promotional bannermobile promotional banner

LibItemDB

A library with all of the WoW items for use by other addons
Back to Files

ItemDB-v1.6.0

File nameItemDB-ItemDB-v1.6.0.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 17, 2026
Downloads
618
Size
35.4 MB
Flavors
MoP ClassicRetailClassic TBCClassicForever
File ID
9233325
Type
R
Release
Supported game versions
  • 12.1.0
  • 5.5.4
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.60.1
  • 1.15.9

What's new

Changelog

[v1.6.0] - 2026-10-17

The first four sections below ship (a second Vanilla BiS provider; threat and armour reduction priced in EP -- both MINOR 45; on-use rating trinkets priced from TBC on; IsBiS returning a fresh list); the rest are under tools/ or Tests/, which the packager does not ship, plus .pkgmeta (packaging only) and README.md.

Vanilla: a second BiS provider, "bisclassic", through phase 6 (MINOR 44 -> 45)

  • Data/Vanilla/_core/BiSClassic.lua registers lib:LoadBiS("bisclassic", ...): 623 sets for 9 classes, phases 0-6, including the fire / nature / shadow / frost / arcane resistance lists. The wowsims provider stops early (hunter at phase 1, mage and shadow priest at 1, rogue at 2, warlock at 0 + 2, warrior tank at 1; WoWSims master checked 2026-10-16), so nothing in the library answered "what is BiS in AQ40 / Naxx" for most classes (Dibs thread 000389037f53, the Badge of the Swarmguard report). wowsims stays the default: ItemDB.toc loads it first.
  • Source: BestInSlotClassic (GPL-2.0, github.com/Warkdev/BestInSlotClassic) at commit 302ab4ecca0c, cached by the new tools/fetch-bisclassic.py (desk itemdb-fetch, source bisclassic) and read by the new tools/build-bis-classic.py. Facts extracted, files not redistributed -- the AtlasLootClassic terms. Its model is a per-class/spec priority ranking plus each item's phase; the builder mirrors its SearchBis (an item counts from its phase on, lowest priority wins). Choices the source leaves to its UI, made here: phase 0 is phase 1 without raid or world-boss items; a fractional phase (2.5) counts from the next whole phase; PvP-rank gear is left out; random-suffix rankings collapse to the bare item id.
  • Weapons and races (Peer Review 5ba689c6 B1-B3; the first cut got all three wrong and built warrior fury phase 1-2 from the Orc main hand 17068 and the Human off hand 18832, a pairing no race's list gives). Now: ONE SET PER RACE GROUP (the races whose rankings are identical), the largest group on the plain token and the others suffixed with their race names (default_human, default_orc, hunter default_dwarf_nightelf); one-hander and two-hander priorities are never compared -- the source numbers them as separate lists -- so a spec its WEAPON_ICONS offers both setups gets the one-hand set on its token and the two-hander on <token>_2h (dropped when it ranks no two-hander); rankings are keyed by (item, OffHand), so an item the source ranks as both a main hand and an off hand (19363, 19104, 19170) keeps both. Tests/bis_classic_check.py gains fixtures for each, red-checked by restoring the merge and the item-only key (both went red, then green).
  • KNOWN, reported to Dibs: Badge of the Swarmguard (21670) is BiS here for Enhancement Shaman (phases 5-6), not for hunters -- BestInSlotClassic ranks it 12th-13th among hunter trinkets, behind Drake Fang Talisman and Jom Gabbar. IsBiS(21670) now answers for the shaman.
  • CONSUMER-VISIBLE through an unchanged API: IsBiS(itemID) with no source reads every loaded provider, so on Vanilla it now also returns bisclassic hits -- including the resistance lists. A consumer that shows "BiS for ..." from it without a source will show more. Pass "wowsims" to keep the old answer.
  • Items restricted to one faction (the source's Faction field) are not filtered: a set is faction-agnostic, like wowsims'.
  • split-seasonal.py skips BiSClassic like BiS (it is keyed by class, not item).
  • Tests/bis_classic_check.py pins the selection rule on a synthetic ranking.
  • Peer Review c9097ad3 B6-B8. New lib:GetBiSSetSpecs(classID [, source]): the sorted spec tokens a FIXED SET source holds (default: the default set source), so a consumer finds default_human or tank_2h without rebuilding the naming rule. GetBiSSpecs was the only lister and reads the EP weight scales, which carry none of those tokens; its README row now says so. Same MINOR 45, because 45 is not released yet. README IsBiS row: one hit per spec token per phase, so a warrior item in all three race groups' lists gives three a phase. build-bis-classic.py race_rankings evaluated only the races some row named, so a playable race no row named got no set at all. New PLAYABLE_RACES (the original-Classic race/class matrix): with an unrestricted row present every playable race is evaluated and an unnamed one gets the unrestricted rows, as the source's SearchBis gives it; without one, a missing race or a named race the class cannot be is refused. The real cache builds the same 623 sets. Four new fixtures, red-checked by disabling the unnamed-race path (the check went red).

Vanilla and TBC: threat reduction and target-armour reduction priced in EP (MINOR 45)

  • Operator, 2026-10-16: "threat reduction is HUGE for DPS classes". Fetish of the Sand Reaver (21647, -70% threat for 20 s every 3 min), Eye of Diminution (23001, -35% for 20 s every 2 min), Badge of the Swarmguard (21670) and, on TBC, Icon of Unyielding Courage (28121, ignore 600 armour for 20 s every 2 min) were in LoadUnscoredProcs, scored 0, and Dibs' list hid them.
  • tools/build-use-effects.py: the on-use path now emits THREAT_REDUCTION_PCT / SPELL_THREAT_REDUCTION_PCT (aura MOD_THREAT, sign flipped so a reduction is positive) and TARGET_ARMOR (aura 123 MOD_TARGET_RESISTANCE, negative, armour bit). New use_window: an on-use spell whose aura is a proc trigger (the Badge) emits the AVERAGE stack count over its window -- Poisson arrivals at the proc's CMaNGOS spell_proc_event ppm (26480: 15 per minute), capped by the triggered aura's CumulativeAura -- 661.1 armour, not the 1,200 cap. New pure helpers use_keys / average_stacks, pinned by the new Tests/use_window_check.py (18 assertions, red-checked by restoring the old sign test).
  • Scoring/Vanilla.lua / Scoring/TBC.lua: derived rules for the three keys through the spec's crit weight (1% crit taken as 1% damage, 0.5% for spell crit). Armour: (A + K) / 100 points per 1%, Vanilla A = 3731, K = 5500 (WoWSims Classic level-63 boss and armour constant); TBC A = 7685 (Magtheridon), K = 10557.5, through crit rating. Threat 1:1 with damage is an UPPER BOUND, ItemDB's own and stated as such; no community weight exists for it.
  • LibItemDB-1.0.lua effectWeight: a derived rule may carry exclude = { [classID] = { [spec] = true } }, read from lib.bisWeightsSpec. Threat reduction excludes every tank and healer spec. A scale not registered through LoadBiSWeights has no spec and is not excluded.
  • Data: Data/Vanilla/_core/UseEffects.lua and Data/TBC/_core/UseEffects.lua rebuilt at the builds their siblings carry; the diff is exactly those items leaving the unscored list. The post-build split-seasonal.py added its partition marker line to Data/Vanilla/_core/ItemPlaces.lua and TokenRewards.lua, which had not been through it before; no row changed.
  • Tests/libitemdb_spec.lua: seven specs on synthetic rows (armour and threat pricing, the tank / healer exclusion, an unregistered scale), red-checked by disabling the exclusion.
  • Peer Review 917f21ea follow-ups. The exclusion list is ONE table, lib._noThreatSpecs in the core, which both Scoring/Vanilla.lua and Scoring/TBC.lua read (each carried its own copy). New specs: both flavours' threat rules hold that table; every spec it names is a spec the shipped Vanilla, TBC and Forever scales load (red-checked with feral_tnak); and no two specs in any flavour's shipped scale share one table, since lib.bisWeightsSpec is keyed by table and a shared one would carry the last spec's exclusion. The TBC [2].default and [5].default scales are byte-identical to holy and discipline, so excluding them is correct. Data/Forever/_core/UseEffects.lua rebuilt at 1.60.1.69913 (Forever loads Scoring/Vanilla.lua): 15 -> 17 on-use rows, 480 -> 478 unscored.
  • Peer Review a7919e37. The Badge of the Swarmguard (21670) still scored 0 on Forever: use_window read its ppm from CMaNGOS only, and Forever has no CMaNGOS DB. It now falls back to the version's OWN wowsims ppm (by the on-use spell, then by the item; a per-hit chance is not a ppm and is skipped), never Vanilla's CMaNGOS rate, which build-sets.py removed as a defect. Forever rebuilt at 1.60.1.69913: 17 -> 18 on-use rows, 478 -> 477 unscored; the Badge prices at 481.3 armour (Vanilla, at CMaNGOS's 15 ppm: 661.1). Tests/use_window_check.py now also reads Forever's file (26 assertions). New spec, the reverse of the token check: every tank or healer scale in the Vanilla, TBC and Forever weights (Stamina weighted 0.5 or more, or healing 0.5 or more and above spell damage) is in lib._noThreatSpecs. Red-checked by dropping shaman tank from the list, and the shared-table spec by aliasing one Vanilla rogue scale: exactly those two failed. The _noThreatSpecs comment now names Forever.

TBC to Mists: on-use rating trinkets priced; the Badge scored on Wrath and Mists

  • build-use-effects.py's use_keys had no branch for aura 189 (MOD_RATING), so every on-use buff that grants a combat rating (haste, crit, hit, dodge, armour penetration, mastery ...) fell through to the unscored list on every flavour from TBC on. It now decodes the mask through the shared itemdb_common.rating_keys(misc, version), the table build-effects.py and build-aura-buffs.py already use, so an on-use trinket and a food carrying the same bit name the same key. Rebuilt, each at the build its siblings carry: Wrath 115 -> 166 on-use rows (940 -> 889 unscored), Cata 189 -> 273 (1421 -> 1338), Mists 264 -> 399 (1954 -> 1820); TBC now ships 83 on-use rows and 601 unscored. Vanilla and Forever are unchanged (no aura 189 in a 1.x client).
  • The Badge of the Swarmguard (21670) on Wrath: CMaNGOS wotlk has its ppm (15), but there its stacking aura is +28 armour penetration rating, not -200 target armour, so the missing rating branch was the whole cause. It prices at 92.6 armour penetration rating.
  • On Mists: no CMaNGOS DB, and wowsims/mop does not model the Badge. use_window now also reads per-spell ppm from Mists' own SkyFire 5.4.8 world DB (mistsskyfire.sqlite, the same spell_proc_event columns), Mists only, so Cata never takes Mists' number. 26480 is ppm 3 there; the stack is +28 crit rating on that client, so the Badge prices at 21 crit rating. Meteorite Crystal (46051, SkyFire ppm 5) also gained a use window: 100 spirit.
  • On Cata the Badge stays unscored: no world DB, and the cached wowsims/cata source has no Badge model (its only TBC trinket file is commented out).
  • Peer Review cc49825a U1/U2: the build log now names the tier each use window took its ppm from (Forever: wowsims/forever spell 26480, ppm 10, 2.407 stacks), and use_window's docstring notes that the by-item wowsims tier keeps an item's first proc. Tests/use_window_check.py pins Forever's m=481.3, the Wrath and Mists Badge rows, and five use_keys aura-189 cases (33 assertions); red-checked by disabling the branch, exactly the three positive decodes failed.

IsBiS returns a fresh list (Peer Review cc49825a I1)

  • lib:IsBiS(itemID) with no source returned the reverse index's own list, so a consumer that sorted or trimmed the result edited every later answer for that item until the index rebuilt. Both paths now build a new list (the hit records inside are still shared). New spec mutates one result and asserts the next call is unchanged; it failed (3 expected, 4 got) before the fix.

Tools only -- the armor-curve verify says what it skipped (Peer Review 3748bed0)

  • My TBC self-audit said shields went unchecked because wago serves no ItemArmorShield at 2.5.6.68941. Wrong: no TBC shield carries walk armour (507 of them, the TBC walk's KNOWN COST), so verify_armor skipped every one at its want <= 0 test before the table mattered, and the run printed only exact / low / OVER -- a green line that read as if shields were covered. The verify now prints a "not checked" line: shields and cloth/leather/mail/plate with no walk armour (the latter named, up to 12). TBC at 2.5.6.68941: 507 shields and 6 body pieces skipped (4728, 5091, 19837, 33073-33075 -- ilvl-0 placeholders, a shirt-slot and a ranged-slot piece), 11,362 exact, 0 OVER. The comment that claimed the unchecked count stops a TBC run is corrected: it guards only a version whose walk carries shield armour (Vanilla, Forever, Mists, Retail). verify_armor now returns a third value, the skipped counts.
  • A hidden item (test / placeholder, never served) that comes out OVER is excused, named and not failed; hidden items that match still count as exact. Forever's OVER row 16213 is "Test Quality Modifier Chest", whose armour is whatever a developer typed. (The first cut skipped every hidden row, which cut TBC's exact matches from 11,362 to 9,769 -- caught in the self-audit.) hidden_ids moved from build-core-wago.py into itemdb_common so both builders read Hidden.lua one way.
  • Forever's other OVER row, 273646 "Half-Eaten Boots" (walk 43, curve 143), is now a listed ARMOR_EXCEPTIONS entry, printed by name on every run. The row is still compared: only an OVER result is excused, and an entry whose row is no longer OVER prints as STALE so it cannot go on excusing nothing. Its cause is NOT VERIFIED: wago's Forever ItemSparse at 1.60.1.69913 has no row for it (its Item row is there). A "-100 Armor" bonus would fit the gap exactly. Forever's verify now exits 0 (7,537 exact, 592 low, 0 OVER); Vanilla passes too (6,715 exact, 12 low, 0 OVER).
  • Tests/armor_curve_check.py 13 -> 18 checks. The skipped-count check was written first and failed on the old two-value return.

Docs -- the changelog back under GitHub's release-body limit

  • CHANGELOG.md was 127,330 characters, over GitHub's 125,000-character release-body limit, so the next release would have been rejected with HTTP 422. v1.1.3, v1.1.2 and v1.1.1 moved unchanged to CHANGELOG_ARCHIVE.md; checked section by section (32 before, 32 after, no word changed).
  • markdownlint's line-length rule (MD013) stays off. Turning it on at 80 columns (Peer Review b4eb143b, M1) was tried and reverted on the operator's ruling, 2026-10-17: "there is only one line length rule, you need to move stuff from the changelog into the archive". The one length this repo keeps is the changelog's size, kept by archiving the oldest whole versions. The 80-column reflow tool written for M1 was removed with it.
  • The markdownlint suite was shown to fail (with MD013 on it exited 1, so the npm shim passes the failure through), and it now lints CHANGELOG_ARCHIVE.md too (Peer Review b4eb143b, M2).
  • CLAUDE.md: "merge- precedence" corrected to "merge-precedence", and the README rules' closing paragraph ("Write it for a dev...") moved out of the last nested bullet it had been read into.

Tools only -- derive-core-maps.py re-run on Vanilla, TBC and Mists (Peer Review d89d615b)

  • First run of read_walk_core on itemdb_common.core_rows since the v1.4.1 refactor: every InventoryType maps to one token on all three versions, matching itemdb_common.INVTYPE. Every stat id's top key matches build-core-wago.STAT_KEY, or EXTRA_ARMOR / RESIST_STAT for ids 50-56, except Mists ids 11 and 40 (2 items each, no mapping; not investigated).
  • The structural check now NAMES every item where wago and the walk disagree (the full list, not a truncated one), instead of only counting them. TBC's 2 are the WoW Tokens (122270, 122284), which the client reports as quality 8 with no subclass. Mists has 98 quality, 18 item-level and 7 subclass differences, where the shipped walk value is the client's own answer; nothing shipped changed.
  • The comment claiming the true stat key "wins by orders of magnitude" is replaced with what was measured: about 3-4 to 1 for the primary stats, with ratings among the runner-ups on TBC/Mists.
  • A stat id with five or fewer hits now lists its items. That named Mists ids 11 and 40: Test Sword Chest 13712, Test Sword Skill Trinket 33003 and Juno's Test Gem 34967, all in Data/Mists/_core/Hidden.lua. build-core-wago.py documents them as deliberately unmapped.
  • build-core-wago.py gains verify_structure(): before it writes Wrath/Cata it reports where wago's quality, subclass, equip slot and item level disagree with the TBC and Mists walks, which verify() (stats only) never compared. Its docstring said the gate compared "every emitted key on every item", which read as covering structure; it now says the gate is stats only. Report-only, because no Wrath/Cata client is on hand to say which side is right; Tests/core_wago_check.py holds the counts exactly (TBC: the two WoW Tokens; Mists: 98 / 7 / 18), so a new drift goes red. Red-checked by reading item level from the wrong field: both counts failed.
  • Forever also run: its ItemSparse carries no stat amounts, so nothing matches by value; its core is walk-built, so nothing shipped depends on that.
  • Peer Review c53c25e2 nits: verify_structure's docstring names the equip slot it compares, and the header build-core-wago.py writes into Wrath/Cata files now says stats are gated and structure only reported. The shipped Data headers pick that up at their next rebuild.

Packaging -- .pkgmeta cleaned, and its checker declared

  • .pkgmeta carried comment lines, which the no-comments rule forbids; removed. One was # curseforge-project-id: 1558208, which the packager never read: release.sh takes the id from -p or the TOC's ## X-Curse-Project-ID: 1558208 (in ItemDB.toc).
  • Dropped the "*.sh" and "*.bat" ignore entries; no tracked file matches either.
  • The harness's .pkgmeta checker is now the declared pkgmeta suite in .writ-suites.json. Its WARN [7] (no curseforge-project-id:) asked for a key release.sh does not read; reported to WoWAPITesting (inbox d446ea6d), fixed in harness f6e5fde, which reads the TOC id instead.

Docs -- GetTokenRewards named a GetSlot the library does not have (Dibs, inbox 65ade047)

  • The comment above lib:GetTokenRewards and its README.md row told consumers to read each reward's slot with GetSlot. No such method exists, so following that raised a nil-method error. Both now name GetInfo's 5th return (equipLoc). Comment and docs only.

Tests -- harness pin 407bd26 -> f6e5fde

  • Harness f5389cb runs a block's examples after its whole body, as busted does. That left Tests/pricewindow_spec.lua's file-level require("env.frames") + libs.load("LibAceGUIWidgets-1.0") in place before its two "without the widget library" examples, and both failed. The load now happens once, in the window block's first before_each. Suite 926/926.

[v1.5.0] - 2026-10-16

Added -- a Shaman tank weight scale (Dibs' contract 46c74258)

  • GetBiSWeights(7, "tank") now resolves on Vanilla, TBC and Forever, in the default hawsjon source. HawsJon publishes no Shaman tank scale and no published Classic one was found, so tools/build-bis-weights.py's new DERIVED_SPECS copies HawsJon's own Protection Paladin scale -- the one published model of a mana-using shield tank whose threat comes from spell damage. Each generated BiSWeights.lua says in its header that [7].tank is a copy, not HawsJon's, and the builder refuses to copy once HawsJon publishes the spec itself.
  • The copy drops DPS_OFFHAND (DERIVED_DROP). A paladin can never dual-wield, so that weight never applied to its own scale; a TBC shaman can (Dual Wield, 30798), and keeping it would let an off-hand weapon compete with a shield on a tank scale. Dropped on all three versions so they share one rule (Peer Review 4c1a17ee P3).
  • PARRY_PCT is kept, verified from the client data rather than the talent's name: Enhancement talent 16268 ("Parry" on Era, "Spirit Weapons" on TBC) learns spell 18848, whose SpellEffect is the same effect (type 22) as the baseline Parry spell 3127, in both the 1.15.9.70003 and 2.5.6.68941 tables. Shield Specialization backs the block weights and Anticipation the dodge weight. Forever's tree was not checked. (The first cut of this entry and the builder comment said TBC had no Talents.lua and that Anticipation gives defense; both were wrong -- Peer Review 4c1a17ee P1/P2.)
  • itemdb_common.header writes a blank header line as -- with no trailing space after it. Every builder's generated headers lose that whitespace the next time they are rebuilt.
  • No scorer change: the Vanilla/TBC scoring rules carry no spec-keyed tables, and the per-class ScoreModel default applies. No MINOR bump -- the API is unchanged and Dibs detects the scale by GetBiSWeights returning non-nil.
  • New Tests/derived_weights_check.py (declared in .writ-suites.json, red-checked by editing one weight in the shipped row, then by putting DPS_OFFHAND back) pins that each derived scale equals its source minus the dropped keys, that the header names it as a copy and each drop, and that [7].default is still elemental.

Added -- what a tier token is turned in for (MINOR 44, Dibs' contract 1c5f8e8d2faf)

  • DB:GetTokenRewards(itemID) returns the gear a boss-dropped token is exchanged for, every class's together, as a fresh table sorted by item id, or nil for an item that is not a token (never an empty table). Dibs prices a token by a substitute item level and slot, and asked for a default derived from the rewards. lib:LoadTokenRewards replaces per token. MINOR 43 -> 44.
  • New tools/build-tokens.py -> Data/<V>/_core/TokenRewards.lua for Vanilla (87 tokens), TBC (159), Wrath (226) and Mists (115), listed in those four TOCs. The client has no such table; the exchange is server-side, so it reads the same world DB build-droprates.py does: quest_template turn-ins (every Vanilla token) and npc_vendor/npc_vendor_template ExtendedCost against the client's ItemExtendedCost DB2 (TBC onward). A token is an item required by count 1, not itself equippable, dropped by an encounter in Sources.lua or DropRates.lua, and rewarding equippable gear in that version's _core.
  • Three defects found while building it, each fixed and pinned by the new Tests/tokens_check.py (declared in .writ-suites.json, red-checked):
    • Head of Onyxia and Head of Nefarian were missing: quest 7490 takes the head and rewards nothing; its NextQuestInChain 7491 needs no item and rewards the rings. A reward-less turn-in is now followed down its talk-only chain (depth 3).
    • Chestguard of the Fallen Defender listed six Gladiator pieces beside its seven T4 chests. tbc-db prices Karynna's Season 1 stock (template 555) at the same ExtendedCost 1235 the T4 vendor uses, and the 2.5.6 client reads 1235 as one T4 chest token. A vendor now counts only when most of its cost rows resolve to a token (template 554 and npc 26089 fail that, at exactly half), and a reward carrying resilience is never a token reward. Every T4, T5, T6 and Sunwell row now matches AtlasLoot's own token table (AtlasLootClassic Data/Token.lua, BCC); 29753 and 30243 are pinned against it.
    • SkyFire's quest_template has no required-item columns, so Mists refused to build; the quest half now degrades to nothing there and the vendor half carries it.
  • Not covered, stated rather than hidden: Forever, Cata and Retail ship no file: the pipeline has no world DB for any of the three. Mists carries the Wrath-era tokens only; why Pandaria's own tier tokens are absent was not investigated. Wrath's Trial of the Crusader trophies were not checked. Nothing ran in a live client.

Fixed -- the armor-curve verify hid its own failures (tools only, nothing ships)

  • tools/build-core-wowsims.py verify_armor printed low and OVER rows from one list capped at 12. Low rows (items with bonus armor, expected, often hundreds) filled the cap first, so the Forever run of 2026-10-12 reported OVER 2 and named neither item. OVER rows are the failures, so they now print in full under their own heading; low rows keep the first-12 sample.
  • That run's two OVER items are 16213 (walk 531, curve 541) and 273646 (walk 43, curve 143). 16213 is "Test Quality Modifier Chest", flagged hidden, and its Forever ItemSparse row (1.60.1.69913) carries QualityModifier=-10, which base_armor does not read; whether that field accounts for the 10-armor gap is not verified. 273646 ("Half-Eaten Boots", Forever-only) has no row in the cached Forever ItemSparse at that build, so the walk came from a newer client and the item cannot be inspected at the pin. Players are unaffected: Forever's core is walk-built, so its shipped armor is the client's own figure and this formula does not reach it. Rerun 2026-10-12 with the fix: both rows print under OVER:; Vanilla unchanged (6,715 exact, 12 low, 0 over).
  • No shipped flavour carries curve-built armor today: every Data/*/_core/Armor.lua header (and Vanilla's _seasonal/Armor.lua) names build-core.py (a walk) or build-core-wago.py, and build-core-wowsims.py refuses to overwrite either. So neither OVER cause reaches a player.
  • armor_curves treats ItemArmorShield as optional. wago answers HTTP 400 for it at TBC 2.5.6.68941 while serving the other three curve tables, and the TBC verify died there before checking a single piece. Shields are now "not computable" (base_armor returns None) and --verify counts them as unchecked, never as low (low means "carries bonus armor"). A 504 or any other error still raises. The TBC verify itself has not run yet: wago answered 504 at both 2.5.6.68941 and 2.5.5.67511 the same day.
  • Peer Review (ad843502): unchecked rows first sat outside the total, so --verify exited 0 with every shield skipped, and a build at such a pin would have shipped every shield with no base armor. Now unchecked rows count toward the total and never toward a pass, so --verify exits 1 while any remain; and the new require_shield_curve makes the build refuse when the shield table is absent and any shield would ship. A wrong 400 then costs a rerun, never bad data.
  • New Tests/armor_curve_check.py (declared in .writ-suites.json, 13 cases) pins that path, the 504 re-raise, the build refusal, the failing partial verify and the robe-reads-chest row. Red-checked three ways: the None guard replaced with an empty table (got 0), the old return restored (got (1, 1)), and the refusal short-circuited (got False).
  • New repo desk tools: itemdb-verify-pinned (a builder's --verify at an exact --wago-build, so the check reads the build the shipped files carry -- wago.tools answered 504 for the installed 1.15.9.70003 while 1.15.9.69722 is cached) and itemdb-derive-maps (tools/derive-core-maps.py --version V --wago-build B, read-only; itemdb-build cannot pass its version flag).

[v1.4.1] - 2026-10-12

Fixed -- where-to-get places filed in the wrong zone (Dibs' DIBSREQ-IDB, Searing Whelp)

Dibs' report (inbox 2c393671f8ee): Dibs showed Small Flame Sac dropping from Searing Whelps in Thousand Needles on Classic Era. The whelps live in the Wyrmbog, Dustwallow Marsh.

  • Root cause: tools/build-item-places.py files each spawn under the zone whose UiMapAssignment rectangle holds it. Those rectangles are map-image bounding boxes and overlap far past the real borders: every Searing Whelp (4324) spawn is inside the Barrens, Thousand Needles AND Dustwallow rectangles, and the tie-break ("the smallest rectangle") chose Thousand Needles. No rule over the rectangles alone gets it right (smallest, deepest-inside and nested were all tried), and the CMaNGOS creature_zone / gameobject_zone tables are empty.
  • New tools/fetch-questie.py (desk itemdb-fetch, source questie) caches QuestieDB's (github.com/Questie/QuestieDB) NPC and object spawn tables for Classic, TBC, Wotlk, Cata, MoP and Forever at a pinned commit (cd4ac424, tools/wago_cache/questie.source.json), each file verified against its git blob id, and reads them: per entity, {AreaTable zone: [(x, y)]}. It shares fetch-att.py's GitHub helpers (resolve / fetch_one now take the repo's API, cache and raw roots, defaulting to AllTheThings').
  • Placer.places takes that as a hint: a spawn inside several rectangles goes to the zone QuestieDB files the entity under (nearest recorded point when it lists several candidates), then the zone the entity's unambiguous spawns use, then the smallest rectangle. The AreaTable id becomes a UiMap through the client's own UiMapAssignment.AreaID (zone_areas, a SET of UiMaps per area, so an area on two zone maps hints both). The coordinates shipped are still the CMaNGOS spawn's; QuestieDB only picks the zone, and none of its data is shipped. QuestieDB carries no licence file (GitHub's licence API answers 404, recorded in questie.source.json); it is a build-time input only.
  • A hint whose nearest QuestieDB point is more than HINT_MAX_YARDS (250) from the spawn gives way to the entity's clear spawns, but still beats the smallest rectangle. Measured on shipped spawns before choosing it: Vanilla, the same world as QuestieDB, median 0 yards, 99th percentile 49, max 4,644; Cata, whose places are SkyFire 5.4.8 spawns against QuestieDB's 4.x points, median 0, 90th 50, 99th 560. It moved four entities on Cata and Mists, none elsewhere: Coral Shark (5434) drops The Cape of Stranglethorn, Winterfall Runner (10916) drops Felwood, and the Firefin Snapper and Oily Blackmouth schools (180683 / 180682) drop Thousand Needles. Which side is right for those four is not verified.
  • tools/fetch-questie.py refuses a line that opens a row ([<id>]) but is not one whole [id] = {...}, line, instead of skipping it, and looks the licence up before its already-pinned early return so a cache pinned offline does not keep "not reached".
  • The builder reports which rule settled the overlapping spawns -- summed over the entities it ships, not every entity it placed (a vendor weighed for the zone spread and not chosen used to count) -- the distance spread above, any QuestieDB area that sits on more than one zone map (none on any flavour; its points would be measured in a frame that may not be theirs), and how many places changed zone against the file it replaces AND against git HEAD. Rebuilt every flavour QuestieDB covers; places that changed zone against HEAD, the cap included: Vanilla 775 of 4,725, TBC 916 of 6,510, Wrath 1,213 of 8,559, Cata 1,253 of 9,601, Mists 1,351 of 10,216. Vanilla's shipped overlapping spawns: 36,450 settled by QuestieDB, 344 by the entity's clear spawns, 96 by clear spawns over a far hint, 14 by a far hint, 33 by the smallest rectangle. Checked in the built file, not in a client: Searing Whelp to Dustwallow Marsh, Skeletal Warrior (48) and Gavin Gnarltree (225) from Deadwind Pass to Duskwood, Gruff Swiftbite (100) from Duskwood to Elwynn Forest, Mine Spider (43) to Elwynn Forest. Vendor choice shifts slightly where a vendor's zone changed (vendors are spread over zones), so a few translated names enter or leave PlaceNames.lua. Retail is unchanged: QuestieDB stops at Mists. Vanilla is pinned to 1.15.9.69722, the build its siblings carry.
  • Tests/item_places_check.py: the hint against both old rules, nearest-point choice, an area on two zone maps, the distance cap both ways, the shipped-only report, a split row refused, the move report, and the real case on real data -- Searing Whelp places in 1445 only, and without the hint it still comes out 1441, so the check can fail. A missing cache fails it rather than skipping.

Changed -- Peer Review f4bc13b5 follow-ups

  • Tests/itemdb_common_check.py's every-[id]= assertion now red-checks itself on every run: a scratch tree with one row core_rows cannot read must come out unequal.
  • Data/Vanilla/_core/EquipStats.lua (hand-edited for the earlier red-check) is rebuilt by the builder at 1.15.9.69722 and re-split, rather than restored by hand.
  • New desk tool itemdb-verify (repo scope) runs a builder's read-only --verify, which itemdb-build cannot pass.

Fixed -- gear school resistances on Vanilla, TBC and Wrath (MINOR 43, Dibs' DIBSREQ-IDB)

Dibs' contract (inbox f93b32b37ec4): a frost-resistance sort in Dibs' Planner found almost nothing on Classic Era, because GetStats returned no school resistance for most gear. Glacial Cloak (22658) had stamina and armor but not its 24 frost; Polar Tunic (22661) was missing 40 frost and Arcanist Robes (16798) 10 fire.

  • Root cause: the Classic clients' GetItemStats never reports ItemSparse.Resistances_1..6 (0 items in the Vanilla and TBC walks, as build-core-wago.py already recorded), so the walk-built cores carried only resistance that comes from an on-equip aura (16061: column 0, walk fire 9). The Wrath core reproduces the TBC walk and so lacked them too.
  • tools/build-equip-stats.py now reads Resistances_1..6 in its existing ItemSparse pass and emits RESISTANCE<n>_NAME (1 holy, 2 fire, 3 nature, 4 frost, 5 shadow, 6 arcane) into EquipStats.lua, which GetStats merges. An item whose core ALREADY carries a school (an on-equip aura the walk reported) and whose ItemSparse column names the same school would need the two summed, which GetStats' core-wins merge cannot do -- so the builder REFUSES and names the item (Peer Review 13334356 F1; the first cut only counted it on stderr). None exists on any version today.
  • Rebuilt: Vanilla 1,371 items (pinned to 1.15.9.69722, the build its siblings carry, then split-seasonal.py), TBC 667, Wrath 723. Forever gains none: its ItemSparse columns are empty and its walk already reports resistances. Mists, Cata and Retail store resistances as stat slots, which their cores already read; not rebuilt.
  • itemdb_common.core_rows is now THE reader of the shipped class files (CORE_ROW + the lib:LoadCore( test), and core_ids is a view over it. It replaces four private copies of the row regex and file walk in build-equip-stats.py, build-core-wago.py, build-core-wowsims.py and derive-core-maps.py (whose hand-kept NON_ITEM side-table list is gone). Found through it: core_resist_keys assigned per row, so an id with a base AND a seasonal row kept only the last file's schools (Peer Review 13334356 F1b); it now takes the union.
  • Tests/data_baseline.json re-recorded. The 14 shrinks it accepted were all already in the released v1.4.0 tree, HEAD being that release: Data/Retail/_core/Effects.lua 1,186 -> 1,182 (-4), Data/Vanilla/_seasonal/Quest.lua 451 -> 450 (-1) and the twelve Data/Vanilla/_seasonal/<locale>/Names.lua 6,523 -> 6,522 (-1 each; the same item moved to the base Quest.lua / Names.lua, both +1). Also adopted: Retail Hidden.lua (new, 13,923) and the grown Hidden.lua on Cata, Forever, Mists, TBC and Wrath.
  • MINOR 42 -> 43 so a consumer can gate on the data (select(2, LibStub("LibItemDB-1.0")) >= 43); no API change. CLAUDE.md's MINOR rule now says so: it moves for an API change OR when a consumer needs to detect new data (Peer Review 13334356 F2).
  • Tests/equip_resist_check.py red-checked by removing Glacial Cloak's row from the Vanilla file: it failed on that item, and passed again once restored.

[v1.4.0] - 2026-10-10

Dibs' DIBSREQ-IDB-010 (inbox d7219561224e): on a rogue, Dibs' Buffs tab offered five foods and priced every sharpening stone at 0.

New -- sharpening stones and weightstones score (MINOR 42)

  • lib.StatScorers.weaponDamage prices WEAPON_DAMAGE (flat damage per hit) as damage / swing speed x the hand's weapon-DPS weight, the weight the weapon's own DPS uses. The hand comes from opts.equipLoc through the rules' dpsSlot; the speed from opts.attack.weaponSpeed / offhandSpeed (the latter falls back to the former). No speed or a ranged slot answers 0 rather than a guessed number. Registered by Scoring/Vanilla.lua and Scoring/TBC.lua; WEAPON_DAMAGE joins both SKIP_KEYS so the flat walk never prices it. Capability flag lib.SCORE_WEAPON_DAMAGE. Until now no code read the key, so Dense Sharpening Stone scored 0 for every class.

New -- which weapons a stone fits (MINOR 42, Dibs' DIBSREQ-IDB-011)

  • DB:GetImbueWeapons(id) and a weapons field on every GetConsumableBuffs row: the GetItemType names a sharpening stone (Axe, Dagger, Polearm, Sword) or weightstone (Fist Weapon, Mace, Staff) can be applied to; nil for an oil. build-effects.py reads it from the SpellEquippedItems row of the spell carrying the temporary enchant (Dense Sharpening Stone's 16138: subclass mask 33219; Dense Weightstone's 16622: 9264; Brilliant Wizard Oil's 25122: mask 0, inventory types only) and writes it as a third table in Effects.lua, lib:LoadImbueWeapons. Vanilla 14 stones (12 base, 2 seasonal), TBC 14.
  • WEAPON_TYPE / weapons_from_mask moved from build-talents.py into itemdb_common, so the talents' weapons tags and the stones' cannot disagree.
  • The other half of DIBSREQ-IDB-011 needed no code: melee swing speed has shipped since v1.2.0 as MELEE_WEAPON_SPEED on every flavour (Thunderfury 1.9, Gorehowl 3.6). The README only ever mentioned it under Search, which is why it read as ranged-only; it is now documented beside the stone scorer and in the stat vocabulary table.

New -- Retail consumable buffs (Data/Retail/_core/Effects.lua)

  • Retail ships Effects for the first time: 584 consumables (381 food, 147 other, 18 potions, 12 flasks, 9 scrolls, 3 elixirs, 14 weapon imbues), loaded by ItemDB_Mainline.toc. A trial build on 2026-10-09 resolved only 50 items and was discarded, because Retail stores almost every buff amount as Coefficient x a game-table column with EffectBasePointsF 0.
  • itemdb_common.spell_effect_scaled_value resolves Retail scaling. The scaling class lives on the SpellEffect row (ScalingClass), not on SpellScaling as on Cata / Mists, and the game table names its columns (Item, Consumable, Gem1, ..., Mana Consumable). The formula is TrinityCore master's SpellEffectInfo::CalcBaseValue: the level cap (90) clamped to the spell's SpellScaling Min/MaxScalingLevel (or its SpellLevels.BaseLevel under SPELL_ATTR10 0x1000), the class column x Coefficient, raised to 1 when in (0, 1), rounded half away from zero. Class -7 is read as the Item column with its item-level multiplier taken as 1. Class -6 and SPELL_ATTR11 0x4 (item-level scaled) stay unresolved rather than ship a guess.
  • Amounts are the level-90 value, like Cata (85) and Mists (90).
  • The Retail path is chosen by the build's major, not by the row having a ScalingClass column (Peer Review c27e8a07 M4): Forever's SpellEffect carries that column too, and Mists 5.5.4 has an unnamed one in the same place. A zero game-table column now answers "no scaling" and keeps the stored base instead of shipping 0 (L1; every per-class column of the 12.1.0 table is 0).
  • Both scaling paths now round through one itemdb_common.c_round (half away from zero, as the client does); the Cata / Mists path used Python's half-to-even round(), so an exact n.5 would have shipped one low. Pinned in Tests/spell_scaling_check.py (2.5 -> 3). Rebuilding Cata and Mists Effects, UseEffects and AuraBuffs and Retail Effects with it moved no line.
  • Verified against Wowhead's Retail tooltips: Well Fed 19706 "Stamina and Versatility increased by 1" (Goretusk Liver Pie 724 ships Stamina 1, Versatility 1); Hearty Meals 222740 "4 Stamina and 2 Agility". Both are pinned in Tests/spell_scaling_check.py (Retail cases) and Tests/effects_check.py.
  • Aura 189's Versatility bits (CR 29-31, mask 0x70000000) map to ITEM_MOD_VERSATILITY on Retail (the rating table, now itemdb_common.rating_groups). They were reported unmapped on 154 Retail rows.
  • Retail spells carrying SPELL_ATTR12 0x00200000 (USE_FLOAT_VALUES_FOR_SCALING_AMOUNTS, SpellMisc.Attributes_12) keep their fractional scaled amount instead of being rounded, as TrinityCore master's CalcBaseValue does (itemdb_common.retail_finish_scaled; build-effects.py writes such a float to 4 decimals). No shipped Retail row is fractional today, so no data moved; Tests/spell_scaling_check.py pins both halves of the rule (Peer Review c27e8a07 L2/F1).
  • Rune of Darkness 139461 and Blessing of the Light 139459 (Legion consumables that save you from a fatal blow) shipped ABSORB0..6=100000000, the client's "absorb everything" amount. Every Scoring file prices ABSORBn as health, so the first Retail scorer would have ranked them above every real item. build-effects.py skips exactly that amount (ABSORB_ALL, reported as unresolved 69:absorb-all) rather than a ">= N" cutoff, so a new large legitimate absorb still ships (Peer Review, thread c5c4048f). Retail Effects now has 584 rows; Tests/effects_check.py asserts no shipped Effects file carries the amount (red on 139459 / 139461 before the rebuild).
  • Not done: Retail has no Scoring file, so nothing prices these rows yet, and no Retail ConsumableGroups.
  • Two stale builder comments fixed (Peer Review c6500056 D7). build-consumable-groups.py now says 13506 is subclass 0 on Vanilla. build-effects.py now gives Trogg Brew as HEALTH=30.

New -- Retail hidden items; Forever's level cap; AllTheThings read on Retail and Forever

  • Retail ships Data/Retail/_core/Hidden.lua for the first time, loaded by ItemDB_Mainline.toc (Peer Review c27e8a07 L6). Before it, no Retail item was ever skipped by rankings or search.
  • build-hidden.py never read AllTheThings on Retail or Forever: _att_categories looked for db/Retail and db/Forever, but ATT's TOC loads db\[Game]\, which the client fills with Standard (Retail) and Camelot (Forever). ATT_DB_DIR maps both. With no installed ATT, att_cache_ni falls back to the fetched source tree (tools/wago_cache/att/, Retail only, never-implemented only), and every Retail build cross-checks that reader against the installed list.
  • Retail's NeverImplemented.lua lists unreleased APPEARANCES (a source id with a bonus id), and the same item id can be live loot: Manaforged Displacement Chassis 237528 is a 24% Plexus Sentinel drop (Wowhead tooltip) and ATT lists it under both. att_extract.lua now prints both for a never-implemented id it also finds in content, and build-hidden.py does not hide those. att_extract.lua also reports a failing data file or handler on stderr instead of swallowing it in a pcall, which silently truncated the tree.
  • Forever gets LEVEL_CAP 60 (WoW Forever stays at 60): 18 items above it are hidden, all unusable at 60 (the Mesh / Strapped / Sterling Chain sets Vanilla already hides, the Azzinoth blades at 70). Retail gets no cap: 0 shipped items require more than Midnight's 90, so an entry would hide nothing today and real gear once the cap moves. LEVEL_CAP_PROBE keeps Retail's count in every build's output.
  • Stormproof Sloop 217272 is hand-hidden (tools/curated_hidden.py): it only works inside Plunderstorm, and its 9999 all-school absorb is a "negate the first hit" effect, not a health pool. The curated list's docstring now admits mode-only items.
  • Forever: 1,641 -> 1,664 hidden (18 by level cap, 6 by ATT).
  • att_extract.lua crashed on line 5 of ATT's Instances.lua and Holidays.lua in every classic flavour that reported it (Wrath, Cata, Mists; TBC's ten new rows below are holiday items, which points to the same loss there, not confirmed from a TBC error line), dropping the rest of each file: every instance drop and every holiday item. Instances post-processes constructor results inline ((function(t) t.objectID = t.providers[...][2] ... end)(e(1626, {providers = ...}))) and the stub constructors returned a bare {}; Holidays compares _.WOWAPI.GetItemCount(...) < 1, and the stub returned a table. Constructors now return their options table (missing fields resolve to the permissive stub) and every _.WOWAPI function answers 0. No file reports an error after the fix.
  • Rebuilt every flavour's Hidden under it. Shipped -> now: Vanilla 2,110 -> 2,110, TBC 4,707 -> 4,717, Retail (new) 13,923, Forever 1,664. Wrath, Cata and Mists rose by 26, 66 and 90 over the crashing build: removed-with-patch items inside instances and holidays that the truncated tree never reached. TBC's ten are Lord Ahune's original Midsummer drops (35494-35497, 35507-35509, 35511, 35514), which ATT records as removed in 2.5.5 (35514: awp=20003, rwp=20505, u=2, beside a 2.5.5 replacement set), and the Yellow Brewfest Stein 32912 (its ATT entry not read).
  • Returning the options table also changed Vanilla's era set, which split-seasonal.py reads (self-audit 3d5d5d3f A3): a parent's argument scan now reached item nodes' own providers, so 190307 (named only in i(190308, {providers={{"i",190307}, ...}}), the same quest as 190308 and 190309, both already in base) became era content and moved from _seasonal/ to base -- one row in Quest and each Names file. That is the header's stated rule, so it is kept, but now on purpose: recItem scans an item node's own options rather than relying on the parent call's shape. Tests/seasonal_ids_check.py pins 184937 and 190307 as era and in base (red on 190307 with the bare {} and no item-node scan put back). A second split-seasonal run moves nothing.

Changed -- flat % buffs price by attack family on Wrath / Cata / Mists (Peer Review c6500056 D2-D6)

  • The scoring rules' derived entries replace pair with family (physical / melee / ranged / spell) in Scoring/Wrath.lua, Cata.lua and Mists.lua. LibItemDB-1.0.lua familyShare scales the rating conversion by the spec's share of that family, read off the scale's weights by spellShare: spell power / (spell power + attack power), else Intellect / (Intellect + the better of Strength / Agility), else 0.5. Melee vs ranged follows the ranged-DPS weight beating the main-hand one, as the speed scorer reads it. An approximation for hybrids, exact at both ends.
  • Tanks and hybrids get a stated share (Peer Review c27e8a07 M3). The ratio gave Wrath protection paladin a 0.75 spell share (spell power 0.4 vs AP 0.13), so its melee crit / hit % priced at a quarter, and enhancement 0.47. The rules take an optional spellShare = { [classID] = { [spec] = 0..1 } }, consulted before the ratio; LoadBiSWeights records each weight table's class / spec in the weak-keyed lib.bisWeightsSpec, so no scoring call has to pass them. Every tank and retribution is 0 (melee); enhancement is 0.3, an estimate (shocks and Maelstrom bolts are real spell damage), not a simulation result. SPELL_SHARE in each of the three Scoring files.
  • The derived conversion is written once (S1): scoreStats prices a derived key through effectWeight, which now also returns the rule, base weight and share; weightFactors turns them into breakdown factors, so a use or proc row shows the conversion and share the same way a stat row does (L7) and its factors multiply to its EP.
  • scoreProcEffects keeps the higher of melee and ranged AP, as scoreStats and scoreUseEffects do (S2). The omission was a double-count, not a choice: build-use-effects.py writes a generic "+X attack power" spell as both a melee and a ranged row, and the same builder reads proc spells. No shipped proc carries the pair today (the one ranged-AP proc, Dragonstalker's 8-piece, is ranged-only), so no shipped score moves.
  • New Tests/scoring_rules_agree_spec.lua (S3): the three Scoring files' derived tables must tag the same keys with the same family and rating, each family must match its key's prefix, and every stated share must be in 0..1 and agree wherever a spec appears in more than one flavour. cata_scoring_spec and mists_scoring_spec gained hybrid-share, mixed-sign and use / proc cases (L8); wrath_scoring_spec moved its ratio cases to a synthetic hybrid spec, since enhancement is now stated.
  • D2: a spell-only % (Destruction Potion 22839's SPELL_CRIT_PCT=2) priced at full crit-rating weight for an arms warrior; it is now 0 for a physical spec. cata_scoring_spec and mists_scoring_spec asserted the old price and were rewritten. The two test rings 18968 / 18982 (SPELL_CRIT_PCT=50) were already hidden on Cata and Mists, so needed no change.
  • D4: the pair's "better member" gave a caster the melee hit conversion; each spelling now prices only for its own family.
  • D5 / F2: the pair kept the larger contribution, so a mixed-sign or negative pair dropped its penalty. Shares sum to 1 across a stat's spellings, so the spellings are now summed and the sign survives. Burning Blossom 23247 (CRIT_PCT=3,SPELL_CRIT_PCT=3) still sums to one 3% crit.
  • D3: scoreUseEffects and scoreProcEffects looked up w[alias[k] or k] only, so a % buff inside an effect (Felstriker 12590's CRIT_PCT proc, Battlegear of Wrath 8-piece's PARRY_PCT) scored 0 from TBC on with no breakdown row. Both now go through effectWeight, the same derived conversion and family share as the stat walk (derivedRule is shared with scoreStats).
  • D6: the derived % keys carry labels in the three Scoring files (Crit %, Spell Haste %, ...); the breakdown showed the raw key. A derived row adds a <family> share factor when the share is not 1.
  • The melee / ranged AP pair is settled once (Peer Review 4eaaa2da M1, L1): apPairEp / apPairClose serve scoreStats, scoreUseEffects and scoreProcEffects. The on-use scorer summed every same-side AP effect but itemised one merged row from the LAST effect's factors, so with two effects on one side the row's factors did not multiply to its EP; it now emits a row per effect. The pair is sign-correct (L2): same-sign sides keep the larger magnitude, opposite signs sum, so a lone AP penalty is no longer dropped by max(-x, 0). No shipped item is known to carry either case. Also L3 (the Mists SPELL_SHARE comment names the AP-vs-SP ratio for the specs that weight AP), L5 (Cata / Mists tank-copy default scales get stated shares) and L7 (hp5 is no longer described as unpriced in Scoring/Mists.lua and mists_scoring_spec).
  • D8: no capability flag; it ships inside MINOR 42, and the README says so.
  • Suite 917/0 (re-run 2026-10-10 after every change above); 100% line coverage on LibItemDB-1.0.lua and the three Scoring files. buffs-ep-vanilla / buffs-ep-tbc red only on Sayge's Dark Fortune of Damage, the known Dibs-side row.

Fixed -- the seasonal overlay kept rows a rebuild had dropped

  • tools/split-seasonal.py re-partitioned the UNION of the base file and the existing overlay, so a seasonal row the builder stopped emitting stayed in _seasonal/ for ever. The Vanilla Effects overlay still shipped 227813 (mp5 1104) and 231778 (mp5 851), drink ticks the decoder fix above removed, and 232433 (MANA=426) with no category. The base file the split writes now starts with a -- partitioned by tools/split-seasonal.py line; a base without it that still holds a seasonal row is a builder's fresh output, and its old overlay is replaced instead of merged. Files split before the line existed hold no seasonal row, so they are merged as before and marked. Every Vanilla _core and locale Names.lua file gains that line. Pinned in Tests/split_seasonal_check.py (red with the replacement disabled). The 679 stale ids the docstring records in the non-enUS Vanilla Names.lua overlays leave on the next locale rebuild.

Fixed -- Well Fed foods, drinks and TBC rating buffs in Effects

  • tools/build-effects.py never followed SpellEffect Effect 64 (TRIGGER_SPELL), which is how most Well Fed foods reach their buff (Blessed Sunfruit 18124 -> 18125, Runn Tum Tuber Surprise, the Mightfish / Bass / Sagefish). Those foods had no Effects row and no food group. It follows the trigger now.
  • Every drink shipped its drinking tick as mp5: the restore while seated and real mp5 are the same aura 85. Spells whose aura ends on standing up (SpellInterrupts.AuraInterruptFlags_0 & 0x40000) are now the eat / drink itself and contribute no stat. Refreshing Spring Water lost its bogus 42 mp5; Blessed Sunfruit Juice 13813 and Bottled Alterac Spring Water 19318 went from 735 mp5 to their real Well Fed +10 Spirit. Vanilla: 55 such rows, TBC: 96.
  • Inside a Well Fed buff (reached from an eat / drink spell) the periodic-mana and periodic-heal auras 24 / 8 are regen, not a restore, and are emitted per 5 seconds as ITEM_MOD_MANA_REGENERATION / ITEM_MOD_HEALTH_REGEN_SHORT: Nightfin Soup 8 mp5, Smoked Sagefish 3, Sagefish Delight 6, Poached Sunscale Salmon 6 hp5. Elsewhere they stay unmapped.
  • Aura 189 (MOD_RATING) was unmapped, so TBC's rating foods and elixirs carried only their other stats (Spicy Hot Talbuk had its +20 Spirit and not its +20 hit rating). The misc value is a bitmask over the client's CR_* indices minus one (read from the classic_anniversary tree's Blizzard_CharacterFrame/TBC/PaperDollFrame.lua); each group maps to the TBC gear key. TBC Effects rows 379 -> 395.
  • The "only mp5 / max pool, so it is food" category fallback is gone. After the seated fix it caught only Trogg Brew 4953, a drink buff that stacks with Well Fed; it is other now, and food means the Well Fed path only.
  • build-effects.py now reports every item whose use chain reached a trigger and still produced no stat, with the chain -- that report is how the missing foods were found.
  • The same decoder now builds Wrath, Cata, Mists and Forever, each pinned to the build its other _core files carry (3.4.3.58936, 4.4.2.60895, 5.5.4.68806, 1.60.1.69913), with build-consumable-groups.py rerun after. Effects rows now: Wrath 518 (446 before), Cata 551, Mists 662, Forever 386. Every flavour also gained its LoadImbueWeapons table (Wrath 15 stones, the rest 14).
  • Rating bits 24 and 25 (CR_ARMOR_PENETRATION 25, CR_MASTERY 26, from the classic tree's Blizzard_CharacterFrame/Cata/PaperDollFrame.lua) map to ITEM_MOD_ARMOR_PENETRATION_RATING and ITEM_MOD_MASTERY_RATING_SHORT, the keys Wrath / Cata / Mists gear and Scoring already use. Mists reported 12 mastery rating auras as 189:mask33554432 and gained 10 rows (652 -> 662); Wrath gained 2 (516 -> 518), Cata 1 (550 -> 551).
  • Aura 227 (PERIODIC_TRIGGER_SPELL_WITH_VALUE) is followed like aura 23, with the triggered spell's amount taken from the 227 row. WoW Forever files its Well Fed foods this way: Goretusk Liver Pie's 1248384 carries 227 (amount 3) -> 1248422 "Well Fed" (MOD_STAT Strength, its own points a placeholder 1), and the higher foods hand 5 / 10 / 15 / 20 to the same three Well Fed spells. 101 Forever foods gained a row (285 -> 386) (Goretusk Liver Pie +3 Strength, Succulent Pork Ribs +5). Not verified in a live Forever client.
  • Retail was built later in this release, once its level-scaled amounts resolved (see "Retail consumable buffs" above).

Fixed -- the food exclusion group

  • tools/build-consumable-groups.py groups every food-typed item in the version's _core/Effects.lua as food where WoWSims / CMaNGOS named no slot. WoWSims types only the foods its sim offers: Vanilla food 11 -> 67, TBC 134 (it already had CMaNGOS's well-fed mask). Run build-effects before build-consumable-groups.
  • Vanilla Effects rebuilt at the core's own pin, 1.15.9.69722 (itemdb-build-pinned); a first rebuild at the installed client's newer 1.15.9.70003 failed the one-build-per-kind spec.

Fixed -- hp5 scores as a heal

  • ITEM_MOD_HEALTH_REGEN_SHORT had no pricing route in any weight scale, so Poached Sunscale Salmon 13932 (+6 hp5) was the one Well Fed food at 0 EP for every spec. Scoring/Vanilla.lua and Scoring/TBC.lua now price it through derived the way the proc heal bucket prices a heal: hp5 / 5 = HPS, x epPerHPS (3.5) = healing power, x the healing weight (per the operator's ruling that a heal has EP). Read off the rules MODEL, so a per-class LoadScoreModel override of epPerHPS does not reach it.

Fixed -- protection potions score for tanks

  • A school protection potion's shield (ABSORB0..ABSORB6) had no pricing route, so all eleven TBC Major Protection Potions scored 0 EP for every spec. Absorbed damage is health not lost, so it is now priced as a health pool (the operator's ruling, 2026-10-09; resistance stays unpriced): Scoring/Vanilla.lua derives it through Stamina at 10 HP per point, like HEALTH, with breakdown labels "Absorb (Fire)" etc.; Scoring/TBC.lua and Scoring/Wrath.lua alias it onto HEALTH, which their scales weight directly; Scoring/Cata.lua and Scoring/Mists.lua derive it through Stamina at 14 HP, like their HEALTH. Mists' header, which said no flavour priced it, is corrected. Pinned by a spec in each of the five flavours; the Vanilla and TBC specs were red-checked by removing the rule (both failed).

Fixed -- a shield against several schools was priced once per school

  • build-effects.py writes one ABSORBn key per bit of a shield's school mask, so a shield that absorbs any school (mask 127) ships ABSORB0..ABSORB6, and every flavour's scorer added them up: the Greater Ward of Shielding 23576 scored as 28,000 health instead of 4,000, Potion of Petrification on Wrath as 42,000. scoreStats now runs collapseAbsorbs first, keeping only the largest ABSORBn on a row, for every flavour and on both the score and the breakdown routes. The data keeps one key per school (Dibs reads the raw keys). Peer Review inbox 85dbb074 I1. Pinned by a seven-school spec in libitemdb_spec.lua and the TBC absorb spec, both red with the collapse removed.

Fixed -- rolled spell amounts shipped at their lowest roll

  • itemdb_common.spell_effect_base_points ignored EffectDieSides, so an amount that rolls shipped its floor: Sayge's Dark Fortune of Damage 23768 (base 0, 10 sides, "1-10%") read PCT_DAMAGE=1 on TBC, Wrath and Mists, and every protection potion its minimum (Greater Fire Protection 13457 ABSORB2=1950, rolling 1950-3250). A roll now returns its expected value, base + (sides + 1) / 2, the number Retail itself stores in EffectBasePointsF (5.5 for 23768); an integral average stays an integer. Classic Era's fixed 10% is unchanged. The data written through the helper was rebuilt: Vanilla's protection potions now read 2600 (matching Forever, which already shipped the average), the lesser ones 400-1800, TBC's Sayge 5.5. Pinned in Tests/spell_scaling_check.py. Peer Review inbox 85dbb074 I2.

  • The same helper also fixed an off-by-one in Season of Discovery data: those spells store a fixed amount with EffectDieSides 0, and the old code always added 1. _seasonal/Effects.lua values drop by one, mostly to round numbers (not checked against an in-game tooltip) (233964 spell power 181 -> 180, 233965 Stamina 101 -> 100, 233969 armor 451 -> 450). That file loses three rows -- drinks 227813 and 231778 (the seated-drink fix) and 232433's stale mana row -- and gains Scroll 228227.

  • Mists' Greater Fire Protection 13457 ships ABSORB2=2607, not 2600: that is the Mists client's own value (spell 17543 Coefficient 0.79 x the spell-scaling table's 3300 at its level-48 cap), not a decoder error. Peer Review inbox 58162a3b.

Fixed -- rating consumables scored twice from Wrath on; Forever foods missing stats

  • build-effects.py split each rating aura's bits into a melee key and a _SPELL_ key, which is right for Classic Era and TBC (separate ratings) and wrong from Wrath on, where hit, crit and haste are one rating each: an elixir carrying both keys scored twice. rating_groups(version) now gives Wrath, Cata, Mists and Retail one key per rating; Classic Era, TBC and Forever keep the split. Rows merged: Wrath 43 (14 spell-only rows renamed, e.g. 41169), Cata 50, Mists 76; TBC is byte-identical. Resilience reads bits 14-15 only from Cata on (bit 16 is CR_LIFESTEAL there; no shipped row moved). Peer Review inbox fa38856e I1, I5.
  • Forever: aura 290 (crit chance) now writes CRIT_PCT and SPELL_CRIT_PCT -- assuming Forever's crit is unified, not verified -- and aura 465 writes armor (RESISTANCE0_NAME). New rows: Grilled Squid 13928 and Hot Smoked Bass 13929 (1% crit), Plated Armorfish 286152 (150 armor, now grouped food); Elixir of the Mongoose 13452 and four more elixirs gain their crit. Run speed (aura 129) stays unmapped. A 227 trigger's amount is now collected before decoding, so a spell reached first another way still gets it; it still feeds every effect of the spell, because eight Forever attack-power foods store AP and RAP both as the placeholder 1. Peer Review inbox fa38856e I3, I4, I7.
  • The same double count survived for flat percentages: Burning Blossom 23247 ships CRIT_PCT=3,SPELL_CRIT_PCT=3 (two auras, 52 and 57), and Wrath / Cata / Mists derive both onto ITEM_MOD_CRIT_RATING, so it priced as 6% crit. Fixed in scoring, not the data, because these are two real auras: a derived entry may now carry pair = "<name>", and scoreStats prices each pair as its best member (ties to the lower key) with one breakdown row. Scoring/Wrath.lua, Cata.lua and Mists.lua pair crit, hit and haste %. Vanilla and TBC are unchanged -- they weight melee and spell apart. Specs in the three *_scoring_spec.lua, red-checked by removing Wrath's SPELL_CRIT_PCT pair (1 failed). Peer Review inbox fa38856e, reopen 0f50301a.

Fixed -- protection potions get an exclusion group, one per school

  • wowsims/classic ships its Greater Protection Potion configs commented out, so none of Classic Era's eleven school protection potions had an exclusion group and the Buffs-tab audit (and, presumably, Dibs' grouped picker) skipped them. build-consumable-groups.py now reads each item's cooldown category (ItemEffect.SpellCategoryID, at the build Effects.lua records) and groups untyped items as potions when they carry the category at least two thirds of the wowsims-typed potions items share, with no tie -- otherwise it adds nothing and prints the vote. It never overrides a slot wowsims or CMaNGOS gave. The first cut demanded a unanimous category, which TBC's 20004 and 30793 (not on category 4) silently defeated. Classic Era: category 4, 13 of 13; 12 items added (6048-6052, 13456-13461, Potion of Fervor 1450). TBC: 36 of 38; 14 added. The Flame Deflector, Ice Deflector and Frozen Rune are not on the potion cooldown and stay ungrouped. Peer Review inbox 85dbb074 I3.
  • A potions group means "only one active", and the potions only share a cooldown: protection potions of different schools can be drunk before a pull and run together (inferred from the shared cooldown and pre-combat use, not verified in game). So any potions item carrying a school absorb (ABSORBn) is filed as protection-<school> (protection-all for all seven schools) instead, on every flavour -- ItemDB's own rule, overriding wowsims' typing on TBC. Classic Era 11 items moved, TBC 22. The generated header now names both passes and the client build the category was read at. Peer Review inbox 62329f9d P2-P5.
  • "On every flavour" was not true: the pass only re-filed items already in potions, and Wrath's wowsims types no potion while Forever's cooldown vote fails (4 of 8), so both shipped no protection-* row and Dibs dropped every protection potion there. An ungrouped item whose Effects type is potion and which carries a school absorb now qualifies too. Wrath gains 28, Forever 11; Classic Era, TBC, Cata and Mists are unchanged. Flask of Petrification 13506 stays flask on TBC and protection-all from Wrath: the client renames it Potion of Petrification and moves it to the potion subclass there. Tests/effects_check.py asserts every flavour's absorb potions carry a protection-* group (red-checked: Wrath 6048 set to potions fails it). README's group paragraph, Forever count (26 -> 159) and stone-scoring scope corrected. Peer Review inbox 62329f9d, reopen c4974c0e.

Fixed -- hp5 on Wrath, Cata and Mists, and missing breakdown labels

  • No Wrath, Cata or Mists weight scale weights ITEM_MOD_HEALTH_REGEN_SHORT, so hp5 scored 0 for everyone there. Those rules now price it through spell power by default, as Classic Era and TBC do (hp5 / 5 x epPerHPS x the healing weight), under a new derived-rule flag scaleWins: a scale that weights hp5 itself takes precedence -- including an explicit 0, which the first cut read as "no weight" and overrode (LibItemDB-1.0.lua now tests ~= nil; Peer Review inbox 58162a3b N1). Specs in wrath_scoring_spec.lua, both hp5 spellings.
  • Sharpening stones and weightstones scored 0 on Wrath, Cata and Mists: only TBC registered the weaponDamage scorer. Each of Scoring/Wrath.lua, Cata.lua and Mists.lua now skips WEAPON_DAMAGE in the flat walk, registers the scorer (damage / weapon speed x DPS_MAINHAND or DPS_OFFHAND; 0 in a ranged slot or with no speed) and labels it "Weapon damage (stone)". A spec with no weapon-DPS weight (Mists protection warrior) still scores a stone 0. Specs in each flavour's scoring spec, red-checked. Peer Review inbox fa38856e I2.
  • Shield rows on Cata and Mists showed the raw key (ABSORB2) in the score breakdown; they now read "Absorb (Fire)" etc. as on Classic Era. Classic Era and TBC gain a "Health per 5 sec" label.

Fixed -- the 11 languages' seasonal item names were never rebuilt

  • build-locales.py took its item ids from core_ids(), which reads the base _core class files only, so it never wrote a seasonal (Season of Discovery / Fresh) id. split-seasonal.py then carried each language's old _seasonal/<locale>/Names.lua forward unchanged and only moved its header's build number, which claimed a rebuild that had not happened. core_ids and read_names take include_seasonal, which build-locales.py now passes. Rebuilt at 1.15.9.69722: 24,127 names per language (15 English fallbacks), and the split regenerated all eleven overlays from them (6,523 each, the same count as the enUS walk's overlay). The "679 stale ids" the split's comment recorded were measured against an older client build; at this build no overlay shrank.
  • The same base-only read froze four more overlays: build-classes.py, build-equip-stats.py, build-sets.py and build-item-sources.py now read seasonal ids too. Rebuilt and re-split at 1.15.9.69722: _seasonal/Classes.lua 10 -> 2,894 rows, EquipStats 2,667, ItemLocations 676, Sets 287 -> 284 (1571, 1664 and 1955 now have one shipped member, below the two a set needs). itemdb_common.shipped_ids now reuses read_names(..., include_seasonal=True) instead of its own pattern. Peer Review inbox 62329f9d P1, P6.
  • build-locales.py left the walk locale out of its default, so enUS's RandomProps stayed at the previous build while the other eleven moved on (provenance_spec red). The default is now every locale; the walk locale's names are still never rewritten. Suffix tables are now written only for a flavour whose TOC loads them (Classic Era, TBC, Mists): Forever's client has none, so every Forever run used to fail 12 of 12 and could not exit 0. Peer Review inbox bc500fc0 B1, B3.

Changed -- dev tooling

  • Tests/itemdb_common_check.py asserts that the Python WEAPON_TYPE table (tools/itemdb_common.py) and its Lua copy in LibItemDB-1.0.lua agree; nothing did before. tools/build-score-model.py's docstring no longer says healing is worth 0.
  • New repo-scoped desk tool itemdb-split-seasonal (runs tools/split-seasonal.py <version>): itemdb-build only runs build-*.py, so the post-build seasonal split had no desk route.
  • New audit Tests/buffs_ep_audit.lua <toc>, declared as buffs-ep-vanilla and buffs-ep-tbc: loads one TOC's library, Data and Scoring files and prices every row Dibs' Buffs tab can show (GetConsumableBuffs rows with an exclGroup; raid / world / totem auras at max rank) for all 37 class/specs, failing on any row at 0 EP that carries no percentage Dibs prices. A standalone process, not a spec, because LibStub refuses a second library load in one state. Rows carrying only magic resistance are listed as a known exception and do not fail it (Classic Era 10, TBC 13). It stays RED on one row in each flavour: Sayge's Dark Fortune of Damage (PCT_DAMAGE, which Dibs drops; reported to Dibs on inbox 058b4bc68b2e).
  • New Tests/seasonal_ids_check.py: finds every builder that writes a _seasonal overlay (from the overlay's own "Auto-generated by" header) and fails if it reads its version's ids without include_seasonal=True. Red-checked by restoring the base-only call in build-classes.py.
  • Tests/aura_buffs_check.py parsed values as integers and crashed on TBC's 5.5; it now reads fractional values, and pins Sayge 23768 per flavour (Classic Era 10, TBC 5.5, Forever 10). It also checked that build-effects.py's mastery bit agreed with build-aura-buffs.py's RATING_BITS; that second table is now gone. Both builders read one table, itemdb_common.rating_groups / rating_keys, and the check compares every bit 0-31 on all seven versions in both directions, so a bit only one side knows (Retail Versatility 28-30) fails it (Peer Review c27e8a07 L5/S4). Wrath, Cata and Mists AuraBuffs rebuilt byte-identical.
  • New Tests/effects_check.py (29 checks): per-version rating groups, resilience bits, Forever's 290/465 mapping, and that no Wrath/Cata/Mists row carries a rating and its _SPELL_ twin. Red-checked by putting Wrath back on the split groups (5 failed, the twin rule on 43 rows).
  • The Mists compile spec is split by locale: Tests/data_compile_mists_spec.lua compiles the TOC's non-locale files and the new Tests/data_compile_mists_locales_spec.lua its Data/Mists/<locale>/ files, through a part argument on Tests/data_compile.lua (one side takes isLocaleFile, the other its complement, so every file is compiled exactly once). The single file hit the harness's 60 s per-file limit in one full run and passed alone; that overrun's cause is unknown (load from a concurrent run is the likeliest, not verified). Measured after the split in one full green run (919 passed, 0 failed, --times): 0.69 s and 2.25 s CPU, about 3 s together, so the unsplit file would likely fit too -- the split makes each half smaller; it is not shown to be what fixed the timeout. The 60 s limit was not raised.

[v1.3.0] - 2026-10-07

New -- full-scan progress events (MINOR 41)

  • LibItemDB_ScanProgress("full", scanned, total) now fires after every 500-listing batch of a full Auction House scan, item nil. Asked by TOGBankClassic (inbox 8cc455f1df85, the one gap left on LIBREQ-PRICE-005): full-scan progress was poll-only. Price/Scanner.lua's two batch loops (fullProcessLegacy / fullProcessModern, identical apart from the row read) are now one fullProcess(n, readRow), so the progress lives in one place.
  • The /itemdb Scan tab shows "Scanning the whole Auction House... 500/1203" once the list arrives (BuildScanStatus).

Fixed

  • GetScanState() reported the LAST TARGETED scan's scanned / total while a full scan ran, so the polling route the README offered gave stale numbers. Full scans now keep their own S.fullScanned / S.fullTotal (listings), reset by StartFullScan; GetScanState reads them when kind == "full". Pinned in Tests/pricescan_spec.lua and pricewindow_spec.lua.

Changed -- dev tooling

  • Removed the spec-times suite from .writ-suites.json. It was the Lua suite re-declared with --quiet --times only because the desk's test call took no runner flags. Writ added args (inbox bb110f037d31, writ CHANGELOG 779), so a timing run is now {"tool":"test","args":"--quiet --times"} -- verified 2026-10-05, it prints one TIME line per spec file and exits 0.
  • .vscode/settings.json is now tracked (commit bda440a): Lua 5.1 runtime, WoW API annotations and the diagnostics globals, so the editor's Lua checks match on any clone. Dotfile, so the packager prunes it.
  • LibItemDB-1.0.lua Search: annotated loose as table|nil (the a and b and c form typed it as false, a cast-local-type warning). No behaviour change.
  • docs/Curseforge_Description.html: dropped the v0.4.6 and v0.4.7 Recent Updates blocks for one "older notes are in the changelog" line, taking the page from 87,124 to 82,606 characters -- below the largest page CurseForge has been seen to accept (85,514).

[v1.2.0] - 2026-10-04

New -- data for Wrath, Cataclysm and Retail

Requested by SmexyMats (inbox 926a233876bf), which generates its tooltip tables from ours and fell back to a hand-made list on these three flavours. The operator chose "Ask ItemDB to build it".

  • Cores. No Wrath or Cata client is installed, so neither has a walk; Retail has one since 2026-10-04 (below). Wrath 45,155 items, Cata 65,797, Retail 176,130.
    • Wrath and Cata carry real stats from the new tools/build-core-wago.py, which reads wago ItemSparse (StatModifier_bonusStat/bonusAmount, Resistances_0) and REFUSES to write unless it first reproduces the shipped walk-built stats of TBC (classic profile, applied to Wrath: 39,426 exact key matches, 0 unexplained) and Mists (modern profile, applied to Cata: 173,121, 0 unexplained). Every remaining mismatch is a listed reason (on-equip auras, which build-equip-stats.py covers; 11 named "Communal" items). Wrath 23,944 items with stats, Cata 30,403. Left out because no walk can confirm them: weapon DPS on both (ItemSparse damage reads about half the Mists walk's), all armor on Cata (the Mists walk disagrees with its own build's Resistances_0 on 18,598 items), shield armor and school resistances on Wrath (the TBC walk never reports either). Tests/core_wago_check.py (45 checks, red-checked) pins the gate and the shipped blobs; build-core-wowsims.py now refuses to overwrite a core this builder wrote. WoWSims wotlk/cata were the alternative and were not used: their db.json is a filtered subset and their stat enums would need a new key map and rating-divisor rework.
    • Retail is walk-built, with stats (build-core.py Retail, from the operator's TOG Tools walk of the Retail client, 2026-10-04): 176,130 items -- 901 more than ItemSparse's 175,229, the items the client carries outside that table -- across 19 class files, names from the walk. Stats are GetItemStats on the link GetItemInfo(id) returns for the bare id, so no bonus or upgrade ids: the base item (TOG Tools Modules/ItemDB/ItemDB.lua:266-275; Spirit Tender's Cuffs 175675: Intellect 4, Mastery 4, Stamina 6, Versatility 3, armor 3). It replaced the structure-only core build-core-wowsims.py wrote (ItemSparse holds only StatPercentEditor allocations and scaling curves, no flat amounts); that builder now refuses to overwrite it, by the existing walk-built header check. Reagents, Expansion, ItemLocations (121,347 items) and ItemPlaces were rebuilt on the new item set. No Retail scoring model yet. The walk also settles the "~38,000 missing items" question: TOG Tools walks to id 320,000 and keeps only ids the client has data for (GetItemInfoInstant), so 176,130 is every item the Retail client knows; the rest of the client's 213,415 Item rows have no data behind them.
  • Pins. FROZEN_BUILD gains Wrath 3.4.3.58936 (the last GLOBAL Wrath build; wago's 3.4.5 is the China Titan-Reforged line), Cata 4.4.2.60895 and Retail 12.1.0.69933. PRODUCT_LABEL maps the product wow to Retail, so the folder does not move when 12.x becomes 13.x. KNOWN COST: when the Anniversary client reaches 3.x the Wrath pin wins over its live build; move it then.
  • Retail slot tokens. INVTYPE gains 27 and 29..34 (quiver, profession tool/gear, the four equipable-spell slots), read from the live client's Enum.InventoryType and GlobalStrings.
  • Reagents for all three: Wrath 766 items, Cata 842, Retail 3,088 across 13 professions.
  • Expansion for Wrath, Cata, Mists (new) and Retail. See Changed for the rule.
  • ItemLocations (SmexyMats' follow-up, inbox e9aefe20d418):
    • Wrath from CMaNGOS wotlk-db (23,256 items; the DB is now cached).
    • Cata from SkyFire 5.4.8 (mistsskyfire.sqlite, the post-Cataclysm world) through the full world-DB path: 31,975 items (gathered 826, drop 9,185, quest 8,476, vendor 9,986, pvp 271, plus crafted and reputation). Vendor stock is 5.4.8's, so a Cataclysm item whose vendor changed in Mists shows the Mists answer.
    • Retail from TrinityCore's world database, TDB 1210.26091 (client 12.1.0; GPL-2.0), fetched and converted by the new tools/fetch-trinitycore.py (desk tool itemdb-fetch), through the full world-DB path, then the AllTheThings passes below: 87,178 items in all. SkyFire stays a gathering-only supplement (GATHER_DB) for 170 old-world items TrinityCore links to no loot. TrinityCore's schema is detected by column throughout: loot ItemType, per-difficulty LootID / SkinLootID / TypeFlags in creature_template_difficulty, QuestSortID, gathering-node gameobject type 50, and per-expansion gathering LockTypes read by name ("Khaz Algar Mining 25"). The client-only mode this replaced was removed the same day.
    • Modern gathering from AllTheThings (MIT, pinned commit c84276b52158, cached by the new tools/fetch-att.py through itemdb-fetch): TrinityCore has spawns and loot for few Dragonflight-onward nodes, so a further pass reads what ATT records under each gathering spell (Mining 2575, Herb Gathering 2366, the skinning spells, Fishing) and under prospecting and milling (salvagerecipe as well as header), for items no world DB gave a gathered row. 642 items; per profession, Dragonflight 119, The War Within 84, Midnight 117, older 324 (a few items count under two). Never removes a row; prints any spell header under a gathering profession it does not read. Tests/att_gather_check.py (23 checks, red-checked). A rule from item category alone was measured first and rejected: only herbs matched the real source 98% of the time (metal and stone 76%, leather 72%, gems 78%). STILL UNCOVERED, Dragonflight / The War Within / Midnight reagents with no gathered or crafted row: gems 12 / 6 / 8, pigments 4 / 2 / 3, cooking meats 14 / 12 / 11 (mob drops TrinityCore lacks), cloth 6 / 15 / 8, metal and stone 4 / 2 / 2, plus 184 others.
    • Drops from AllTheThings (Retail): an item with no drop, gathered, vendor or quest row gets a drop row from ATT's own records -- world drops (03 - World Drops), zone drops inside a map header (02 - Outdoor Zones, named from the client's UiMap) and instance loot (01 - Dungeons Raids, through the same instance-name aliases). 10,443 items (Dragonflight 794, The War Within 328, Midnight 537, older 8,784); ATT names the creature for 2,746, the rest are ATT world or zone drops with no creature named. Vendor stock and an item's own contents are never read as drops, and an item the Encounter Journal already places by a boss is skipped (journal_boss_items, which ignores Cata's items-nil merge call whose first table is the instance map). fetch-att.py now stays on the recorded commit unless --update, and caches ATT's Maps.lua for the map constants. Tests/att_drops_check.py (14 checks, red-checked). This gave 24 of the 37 uncovered Dragonflight-onward cooking meats and 15 of the 29 cloths a source. The later passes in the next bullet took the rest down to 12.
    • Every other origin AllTheThings and the client record (Retail; for SmexyMats' request d95142e3, all additive, each record shape pinned in Tests/att_drops_check.py or att_gather_check.py and red-checked):
      • salvage as a gathered row: disenchanting -> Enchanting, crushing -> Jewelcrafting, scrapping -> Engineering;
      • crafted: SpellEffect 294 ("craft loot") and 296 ("craft salvage item", names from TrinityCore's SpellEffects.cpp) besides 24 and 288 -- Chopped Mycobloom, Fresh Fillet and the Transmutagens are these -- plus ATT's crafted-items lists (2,893 items; the Wilderthread / Duskthread / Dawnthread / Weaverthread spools have no client recipe row at all, only ATT's);
      • vendor stock, including profession supply vendors filed under a profession header in a zone (rule checked against one real file);
      • quest REWARDS and reward headers -> the zone, else "Quest" ("Crafting Orders" for patron rewards). Quest-objective items, which ATT tags "(QI!)" in three shapes, never get a quest row: that removed 4,632 items' rows;
      • treasures, objects and container contents (Airy Soul -> "Docile Airy Soul"), and instance loot ATT keeps outside the Encounter Journal;
      • Midnight housing lumber as object drops per map (ATT names no profession for it).
      • item constants: ATT writes some items by name, i(ARTISANS_ACUITY); fetch-att.py now also caches ATT's Constants/Items.lua and att_source_rows(..., items=) resolves them through it. The pass also reads 21 - Holidays, and a holiday file's row with no map names the holiday ("Darkmoon Faire" for Artisan's Acuity, a Darkmoon profession-quest reward).
      • Fixed: a Companion Pet, Mount or Mount Equipment item (Miscellaneous subclass 2 / 5 / 6) is no longer read as a container, so items nested under one in ATT no longer "drop from Reins of the Quantum Courser" (ATT_NOT_CONTAINER; 3 items' rows).
      • Blizzard's own collectable sources (client_source_pass, before ATT's drop pass, so an item they place keeps ATT out): the client tables CollectableSourceInfo + CollectableSourceVendorSparse / QuestSparse / EncounterSparse record a vendor, quest or Encounter Journal boss for every collectable appearance (96,900 / 15,172 / 47,320 rows at 12.1.0.69933). Vendor rows for 12,629 items, quest rows for 7,189, boss-drop rows for 2,121; Retail locates 96,447 items, up from 88,842. A vendor or quest in an instance's map reads that instance's name -- which for an open-world map is its Journal world-boss tier, so a Valdrakken vendor reads "Dragon Isles" -- else "Vendor" / "Quest". Each item comes from its APPEARANCE (ItemModifiedAppearance.ItemID), never from the vendor table's VendorItemID: on the Mining Pick's trade-supplier rows that column names Lasherweave Raiment, which Wowhead lists as not available to players. Tests/att_drops_check.py pins the join.
      • The same vendor rows feed Retail's "Where to get it" (build-item-places.py client_vendor_spawns / add_client_vendors, Retail only): 15,960 items gained a vendor, and 1,281 vendors TrinityCore never spawns are placed at the client's own position (Provisioner Thom, Valdrakken's Blacksmithing Supplies, at 2112: 36.6,50.6). A creature TrinityCore spawns keeps its spawns; one with no name there is skipped. Tests/item_places_check.py pins the merge.
      • Quest givers in "Where to get it" (MINOR 40): a new place token q<creature>:<questID> from CollectableSourceQuestSparse; GetItemPlaces returns it as kind = "quest" with questID (no chance), after objects, at most 4 per item (MAX_QUESTS). 7,453 Retail items gained a quest giver, 583 givers placed at the client's position alone. Checked: quest 58166 "Unknown Assailants" (Tirna Vaal) rewards 175675-175677, which carry q160439:58166, the giver on Ardenweald's map; Wowhead did not name the starter, so the giver is not verified. BuildWhereRows shows it as a "Quest" row, applies the vendor faction filter to it, and no longer repeats the generic "Quest" location line when a giver row covers it. An older library skips the token. Specs: libitemdb_spec.lua, where_spec.lua, item_places_check.py. lib.PLACE_KIND (the token letters) is now public, read-only, so a consumer decoding tokens itself need not keep a copy that misses the next letter -- SmexyMats kept one (Peer Review, thread 8acd9f23).
      • The compile spec is one file per TOC (Tests/data_compile_<flavour>_spec.lua, body in Tests/data_compile.lua). Measured with the new spec-times suite (run.lua --times, NOT A GATE): the single data_compile_spec.lua took 69 s CPU and hit the harness's 60 s per-file limit part-way through Mists, so the TOCs after it never compiled -- a pass that depended on the machine's speed. Now the slowest, Mists (12 large Names files), takes 18-26 s. provenance_spec.lua read its -- source and -- client build stamps by walking every line of every Data file; it now reads only the comment header they live in (22 s -> 11 s).
      • Every Retail item, measured (report_unplaced, printed by every Retail build): of 175,229 shipped items, 135,314 (77%) now have a source -- ItemLocations or an Encounter Journal boss drop (19,049, Sources.lua, which an earlier count wrongly left out). Of the 39,915 without one, AllTheThings files 10,971 as never implemented and does not list 18,854 at all; 10,090 it lists and no pass reads yet (class-trial / boost / starting gear 1,844 is left out on purpose; the rest is listed by folder in the build output).
      • Folder sources (att_folder_pass, ATT_FOLDER_SOURCES): an item no record-shape pass placed takes its source from the ATT folder it is filed in. 08 - PvP was never read at all -- 12,396 items, whole season files (Legion Season 3 Cruel.lua:1121, Cruel Gladiator's Ring -> "PvP") -- plus Trading Post 951, instance files (the file's own name) 928, holidays 784, Delves 470, Group Finder 439, In-Game Shop 258, Black Market 57. A file named for something that is not a place (an Achievements.lua inside these folders) is skipped, so achievement rewards do not read "Achievements" as where they drop. Zones, events, expansion features and character files go by their PATH (att_path_row: the folder is the place, the file name below it the kind -- Quests, Vendors, Gear Conversion, Rares, Dungeons Raids; achievements, boosts and files naming no kind are left alone): events 2,335, zones 963, expansion features 548, Heritage 210. Only i(<id>) counts, never a { "i", id, n } cost; quest objectives and never-implemented items are skipped.
      • Quest objectives (ATT's "(QI!)") with no other source now read their quest's zone (add_objective_rows), 4,064 items -- e.g. V-300 Serial Plate -> Tiragarde Sound. Earlier today they were given no row because "rewarded by a quest" is wrong for them; for an item nothing else places, "got during a quest in that zone" is the true answer, and an objective that also drops or is sold keeps only those rows.
      • Items missing from the database itself: the client's Item table has 213,415 rows and ItemSparse (names, details -- what Retail ships) 175,229, so 38,186 client items are not shipped. Tests/item_coverage_report.py (run_check) breaks them down: AllTheThings marks 17,262 never implemented and does not list 19,890; nine Wowhead samples were all 404s or placeholders ("[PH] Plate Chestguard of the Rising Dawn"). 1,034 it lists as real items (quest 377, armor 372, miscellaneous 149) -- not shipped yet, because the client carries no name or details for them. The Dragonflight-onward reagents with no row at all went 152 -> 11, each with a reason: Fang Adornments is never-implemented; two Shadowflame crests were a fragment exchange removed in 10.2.0; three Amirdrassil prototypes are quest objectives; Smudged Lens, Soul Sigil I and II and Spark of Tides are listed by ATT with no source and made by no client spell; Temporal Vestigial has only a description. The client's QuestPackageItem (quest reward packages) names none of the 11, and CurrencyContainer holds currencies, not items. itemdb-schema gained a row filter for that, Table:Column=v1|v2, inside its table field so its command and approval stay unchanged.
    • Prospecting and milling are gathered rows now, like disenchanting ("Enchanting"): prospecting_loot_template -> Jewelcrafting and milling_loot_template -> Inscription, read wherever the world DB has the table. TBC 32 / 0, Wrath 51 / 15, Cata 58 / 15, Mists 71 / 17, Retail 166 / 57 (with ATT); Vanilla has neither table. fetch-trinitycore.py converts both now, and re-converts when its cache lacks a listed table: the skip used to compare only the release tag, so a cache built before a table was added counted as current.
    • Vanilla ItemLocations was rebuilt for the first time since v1.1.4 and gains that release's fixes, which had only reached TBC and Mists: reagents such as Arcane Powder, Ankh and Symbol of Kings read their PvP Barracks and ordinary vendors instead of "Alterac Valley", and the four battleground healing draughts read Vendor. An orphan row for item id 1 (in no Vanilla item file) dropped out.
  • Sources (instance drops):
    • Wrath from AtlasLoot (5,522 items).
    • Cata: AtlasLoot up to Wrath (5,511 items) plus 20 instances from the client's Encounter Journal through the new tools/build-sources-journal.py: 16 new Cataclysm instances, and the 4 instances Cata revamped (Deadmines, Shadowfang Keep, Zul'Gurub, Zul'Aman) folded into the AtlasLoot entry instead of listed twice. ORDER MATTERS: build-sources.py Cata rewrites both files with AtlasLoot only, so build-sources-journal.py Cata must run after it.
    • Retail, all from the Journal: 213 instances (142 dungeons, 70 raids), 1,138 encounters, 19,049 items. 14 bosses the Journal lists twice in one instance are merged into one; same-named instances get a suffix (Blackrock Depths, Magisters' Terrace).
  • DropRates for Wrath (7,956 items) and Mists (7,561). Mists reads SkyFire, whose loot rules differ from CMaNGOS: a reference row rolls its own chance and processes only its group, and rows without lootmode bit 1 are hard-mode only (skipped). The builder picks those rules from the table's columns, not the version name. Wrath also merges DifficultyEntry1..3 heroic loot, which the old HeroicEntry-only read missed.
  • ItemPlaces ("where to get it") for Wrath (17,264 items, CMaNGOS) and Cata (21,078, SkyFire), with 8 locales of place names each, and Retail (23,802 items) from TrinityCore's world database with 8 locales of place names. SkyFire was ruled out for Retail first: its world is pre-Legion and Retail has rebuilt much of it (Undercity, Teldrassil, Silithus).
  • RepLoot for Wrath (964).
  • TOCs. ItemDB_Wrath.toc and ItemDB_Cata.toc now load their data (they were deliberately empty placeholders). New ItemDB_Mainline.toc (Interface 120100) loads the whole addon -- Integrations, Tooltip, Price, Where and the bootstrap, with the same dependencies, OptionalDeps and SavedVariables as every other flavour -- and no Scoring file, since Retail has no stats. It shipped data-only at first; the operator asked for the full addon the same day ("we also need to create a retail .toc for you, so you work in retail"). Every module already feature-detects the modern client (data-driven tooltips via C_TooltipInfo, C_AuctionHouse, C_MerchantFrame; HandleModifiedItemClick exists in the live UI source). Loaded on a Retail client with TOGProfessionMaster, no errors, after the Names.lua fix (operator, 2026-10-04: "togpm is running with no errors, and before it had a bunch"); the tooltip lines, price window and "Where to get it" window have not each been exercised there yet. Tests/toc_spec.lua now treats all seven manifests as full. wow-version-replication.ps1 replicates to _retail_ too. ItemDB_Mists.toc loads its new Expansion.lua.

New -- scoring and side tables for Mists, Wrath and Cata (parity with TBC)

The operator asked for parity so addons like Dibs work on every flavour ("lets get to parity").

  • Scoring. New Scoring/Mists.lua, Scoring/Wrath.lua, Scoring/Cata.lua, each loaded by its own TOC only (Tests/toc_spec.lua now expects them). Rating conversions at each level cap from WoWSims' base_stats_auto_gen.go (Wrath 80, Cata 85, Mists 90). Spell power is counted once (the data stores "+N spell power" as an equal damage and healing pair). Cata and Mists skip the stats patch 4.0.1 removed (defense, armor penetration, block value, ammo, feral AP); only Wrath registers the ammo scorer. Death Knight (6) and Monk (10) score through their weights. Specs: Tests/{wrath,cata,mists}_scoring_spec.lua (48 tests); 100% coverage on all three.
  • BiSWeights for Mists (45 specs, 11 classes incl. Monk), Wrath and Cata (41 specs, 10 classes incl. Death Knight), from the Wrath/Cata/Mists branches of HawsJon's Pawn scale. The builder now refuses any Pawn stat that is neither mapped nor explicitly ignored. Not priced: weapon Speed (Marksmanship's top Mists weight), resistances, school spell damage, Hp5, feral AP.
  • Side tables. Mists: Factions, Mounts, Sets, ReqLevels, VendorPrices, SellPrices, Classes, EquipStats, UseEffects, BiS, ScoreModel, BiSWeights, DropRates. Wrath and Cata: Effects, Talents, TalentEffects, EquipStats, Sets, Factions, Classes, Races, ReqLevels, VendorPrices, SellPrices, UseEffects, ScoreModel, BiSWeights, Mounts, Hidden; Wrath also SpellSchools.
  • AuraBuffs for Wrath (44 buffs), Cata (35) and Mists (31), each from that expansion's OWN raid buffs: a curated spell-id list from WoWSims wotlk / cata / mop, every amount read from that build's SpellEffect, and the build refuses when a curated id's name does not match. The first cut emitted the Classic list (Wrath's Warchief's Blessing read as +500% damage, Trueshot Aura as a flat 300 AP) and was not shipped. Cata's resistance buffs ship at 195 / 97 from the game's own spell tooltips. A buff that fills TWO raid-buff categories (Mists Arcane Brilliance: spell power AND crit) now competes in both, through the new GetAuraSlots and the slots field on GetAuraBuff / GetAuraBuffs ranks (MINOR 39, additive): Arcane Brilliance with Leader of the Pack is 5% crit, not 10%. Tests/aura_buffs_check.py pins the values per version.
  • Gems, ConsumableGroups and BiS sets for Wrath and Cata; ConsumableGroups for Mists. BiS now covers all 11 Mists classes, and an earlier expansion's gear is no longer listed as BiS for a later one. ScoreModel gains Death Knight and Monk constants.
  • Not shipped, on purpose: Mists Talents (tiers and specialisations, not trees).
  • New desk tools (all repo-scoped): itemdb-worlddb (tools/world-db-query.py, one read-only SELECT against a cached world DB, SQLite mode=ro), itemdb-tarball (tools/tarball-read.py, reads inside a cached WoWSims tarball without extracting), itemdb-fetch (tools/fetch-trinitycore.py) and itemdb-build-pinned (a builder at an explicit --wago-build).

Fixed -- found while building the new flavours

  • Mists spell effects were wrong, some badly (shipped). Two defects of mine in itemdb_common.spell_effect_base_points:
    • Every builder adds +1 to a SpellEffect's base (Classic, EffectDieSides 1). Cata 4.4.2 and Mists 5.5.4 store EffectDieSides 0, so every value read one high (Flask of Endless Rage 181 on Cata against 180 on Wrath).
    • MoP stores many amounts as Coefficient times the client's spell-scaling game table with a base of 0. Flask of Spring Blossoms shipped as ITEM_MOD_AGILITY_SHORT=1; it is 1000. The multiplier comes from gametables/spellscaling.txt (wago CASC file 1391660) at the version's cap, clamped to the spell's MinScalingLevel..MaxScalingLevel (Spring Blossoms caps at 85: 1.9646 x 509 = 1000). Spells scaled by ITEM level (SpellMisc.Attributes_11 & 0x4, e.g. Relic of Xuen) keep their stored base. Tests/spell_scaling_check.py pins four flasks, Battle Shout and Relic of Xuen (red-checked twice). Mists Effects lost 41 items whose only "effect" was that bogus =1. Vanilla, TBC, Wrath and Forever resolve nothing and do not change.
  • Wrath item procs never got their CMaNGOS rates. build-proc-rates.py wrote proc_rates_wotlk.json; build-use-effects.py read proc_rates_wrath.json. Both names now come from one helper in build-proc-rates.py. Wrath scored procs 111 -> 313.
  • WoWSims proc rates were found only for Forever. build-use-effects.py and build-sets.py derived the repo from the version name ("tbc", "mists"; real: tbc-new, mop); they now read WOWSIMS_REPO. No shipped count moved.
  • build-talents.py dropped Death Knights (a Vanilla class filter), read Cata's dead talent tabs (negative OrderIndex: "(4.0 OLD)") and wrote header-only files for a version with no trees. All three fixed; it now refuses rather than writing an empty file.
  • build-races.py read only the AllowableRace_0/_1 halves, so Wrath and Cata (one signed 32-bit column) read every item as unrestricted.
  • Retail loaded no item names at all. Data/Retail/enUS/Names.lua put 175,229 names in one function, past Lua 5.1's 262,143 constants per function, and the client refused it ("constant table overflow", the operator's in-game report). itemdb_common.write_names now splits any table over 60,000 names into blocks, each in its own local function (each gets its own constant table; the [id]="name" lines are unchanged, so every reader still parses them). Files at or under that size are written exactly as before. New Tests/data_compile_spec.lua compiles every file each TOC loads under Lua 5.1, the client's own limit; it was red on exactly that file.
  • Retail counted no Dragonflight-onward recipe as crafting. build-item-sources.py knew only SpellEffect 24 (create item); modern recipes use effect 288, whose misc value is a CraftingData id (2,450 rows at 12.1.0; its items come from CraftedItemID and one CraftingDataItemQuality row per quality tier). Effect 301 is enchanting (CraftingData Type 3, no item) and is not read. Retail ItemLocations 34,641 -> 38,048 items.
  • Weapon haste procs were never scored, on every flavour (found by Peer Review). build-use-effects.py read an ItemSparse column named Delay, which no build has; the column is ItemDelay. It exited 0 with no swing speed for any weapon. It now reads ItemDelay and refuses to write when weapons carry damage but no speed. Scored procs: Vanilla 187 -> 190, TBC 235 -> 237, Wrath 313 -> 315; Cata, Mists and Forever rebuilt the same way.
  • Socket scoring was copied into four Scoring files and is now one shared scorer (lib.StatScorers.sockets), which also fixes gem prices going stale after new gem data loaded. The meta gem bonus (META_SOCKET_EFFECT) is now priced on TBC, Wrath, Cata and Mists.
  • Weapon speed is scored on Mists: Marksmanship values ranged speed, and Retribution and Assassination value melee speed, now recorded as MELEE_WEAPON_SPEED by build-equip-stats.py (ranged keeps WEAPON_SPEED). A hunter's speed weight never counts a melee weapon and a melee spec's never counts a bow. Search { stat = "WEAPON_SPEED" } matches both keys, as stat always matched a fragment (README).
  • build-hidden.py missed QATest... / QAEnchant... names (its ^QA\b pattern needed a word break after "QA"); about 100 test items per version were served. Vanilla hides 57 more.
  • "Where to get it" placed Dalaran's vendors in Crystalsong Forest on Wrath, Cata and Mists (Dalaran is a dungeon-style map at 3.4.3); they now show on the Dalaran map. build-item-places.py also reads the conditions table's negation flag now; no shipped "conditional" tag changed, because every affected drop already carried one.
  • Cata Sources: build-sources.py Cata now runs the Encounter Journal merge itself after it writes, so rebuilding the AtlasLoot half can no longer wipe the Journal half (Tests/sources_order_check.py). Nine Cata bosses listed twice in a folded instance are merged, and Zul'Gurub and Zul'Aman are labelled dungeons. KNOWN COSTS: the Cata Journal loot of Venoxis and Mandokir sits under their Vanilla NPC ids (14507 / 11382); the Vanilla AtlasLoot bosses still list beside the Cata ones in the four folded instances; Retail's Jadefire Masters keeps one row for its two faction versions, since a boss row has no faction field.

Changed -- one profession resolver, which understands Retail's expansion child lines

Retail splits each profession into per-expansion skill lines ("Midnight Alchemy" 2906 is a child of Alchemy 171) and hangs recipes off the child, and its CategoryID 11 also holds minigame lines ("Abominable Stitching", "Supply Shipments"). The first Retail run reported 29 "professions". itemdb_common.profession_lines(build) now walks ParentSkillLineID to the root and, on a build that gives any profession a SpellBookSpellID, keeps only roots that have one (Classic Era sets it to 0 everywhere, so there the filter is off). build-reagents.py and build-item-sources.py (craft_professions) both read it, so "used by" and "made by" cannot disagree; they were two copies that had already diverged.

build-reagents.py also reads the ModifiedCrafting* tables on 9.x+ builds: modern recipes take quality materials and optional reagents through crafting SLOTS, not SpellReagents. Before this, Bismuth (210930) listed only Mining; it now lists Blacksmithing, Engineering, Jewelcrafting and Cooking, all from the client's own slot data (Cooking recipes 445482 and 1245993-1245995 take the "Common Ore" category; not confirmed in game).

build-expansions.py runs the first-ships-it chain through Vanilla, TBC, Wrath, Cata and Mists; Retail continues with its own ItemSparse.ExpansionID (5 Warlords .. 11 Midnight; the numbering is the live Encounter Journal's tier table, Blizzard_EncounterJournal.lua:106-122) and lists items where that is unset too under -1. GetExpansion returns nil for -1 rather than a number that is not an expansion; the tooltip's Expansion line now names Wrath through Shadowlands in SmexyMats' colours (SmexyMats/Utilities/Utilities.lua:29-38). The default bucket is now the largest one, which keeps Vanilla's and TBC's files unchanged in shape.

Instance names in Sources come from the client's Map table (via the block's InstanceID and the same INSTANCE_NAME_ALIAS item-sources uses) when the key is not in SOURCE_INSTANCE_NAMES, a map no other block shares, and the name does not collide with another instance.

Fixed

  • Malformed instance names on TBC and Mists. Every AtlasLoot key outside the hand-kept Vanilla table was humanized from its CamelCase key, which shipped "Hallsof Lightning", "Trialofthe Champion", "Ahn Kahet", "Gruuls Lair", "Zul Aman" and "Magisters Terrace". They now carry the client's names ("Halls of Lightning", "Ahn'kahet: The Old Kingdom", "Gruul's Lair"). Consumer- visible: several TBC instances now read with the client's prefixes ("Coilfang: The Slave Pens", "Hellfire Citadel: Ramparts", "The Sunwell"). Vanilla is unchanged.
  • Mists listed Pandaren cooking specialisations as professions. "Way of the Grill" and its five siblings shipped as six separate professions in Data/Mists/_core/Reagents.lua; they are now folded into Cooking (same 969 items, 11 professions instead of 17).
  • TBC Expansion.lua was stale against the current Vanilla and TBC cores (17,568 / 12,516); regenerated with unchanged logic: 17,490 Classic / 12,542 TBC.
  • Expansion.lua stamped a build it never read. Every flavour's file carried the live client's client build, though only Retail reads a DB2 (the rest derive from the shipped item sets). Once the Era client moved to 1.15.9.70003 that put a second build into Vanilla's core group and provenance_spec went red. Non-Retail files now say client build: none read and carry a source tools/build-expansions.py sha256: line (the rule lives in the builder, as with ScoreModel.lua); Retail keeps its build stamp too. Vanilla's Reagents.lua, regenerated only to prove the shared profession resolver left it unchanged, went back to HEAD for the same stamp. Vanilla's count now reads 17,604 (was 17,682), matching the class cores today; which change moved it was not traced.
  • Three builders called a validating helper on a branch (build-expansions.py's shipped if ... else core_ids(v), build-item-sources.py's client-only path, build-sources.py's comprehension), which itemdb_common_check refuses. Each call is now unconditional.
  • build-rep-loot.py wrote an empty file when it had nothing (Cata); it now writes nothing and removes a stale one.

Tooling

  • tools/wago-schema.py + the itemdb-schema desk tool (approved 2026-10-04): print a wago DB2 table's columns, row count and first row at a version's pinned build. It replaces throwaway shell probes; the operator's directive the same day is that tool gaps are fixed, not shelled around.
  • The itemdb-build desk tool now lists the new versions in its help, through Writ's tools update action (shipped on request, inbox 8aa52bc3e697), with a 600-second limit.
  • New Tests/constant_headroom_check.py: fails when any shipped Data file's busiest function passes 80% of Lua 5.1's 262,143-constant limit -- the warning before data_compile_spec, which only fails once the game already refuses the file. Measured today: the largest unsplit files are Mists' Names.lua at 62% (they split on their next rebuild, being over 60,000 names), then Retail's Expansion.lua at 57%. Peer Review suggested one shared chunked writer for every builder; not done, because no other file is near the limit and the builders cannot all be re-run here to prove an unchanged output -- the check names the writer to split when one gets close.
  • Harness pin b1d9bd5 -> 407bd26: an error escaping the runner is now a named FAIL with a traceback instead of a bare (error object is not a string), which is what data_compile_spec.lua hit. Adoption asks only for the pin; the suite is green on it.
  • itemdb-schema takes its table list separated by commas OR spaces; a space-separated list used to reach the fetch as one table name and crash on the space in the URL.
  • CLAUDE.md: builders run through itemdb-build, not the shell; that paragraph used to say the opposite.

[v1.1.4] - 2026-09-29

Fixed -- ordinary vendor goods read "PvP" on TBC and Mists

Refreshing Spring Water (159) showed only "Alterac Valley / PvP" as its source on TBC, with none of its innkeepers, vendors or quests (SmexyMats peer review 60e304d70cab, found while generating its tooltip data from ours). The cause was in tools/build-item-sources.py: the battleground reward-vendor pass REPLACED an item's whole location set with the battleground whenever a BG_VENDORS NPC sold it, exempting only ammo. TBC's Stormpike Quartermaster (12096) stocks common food, water, reagents and flasks, so every one of those lost its real sources. The pass now skips any item a vendor outside a battleground also sells (sold_outside_bg).

  • TBC: about 60 items corrected (water, bread, flasks, reagents, arrows), e.g. 159 now reads Vendor and Quest. Item 1 (a placeholder id, "World Drop") also left the file on this rebuild.
  • Mists: about 70 items corrected, the same pattern.
  • Vanilla: unaffected -- the classic DB's quartermaster stocks none of them, and 159 already read Vendor and Quest. Not rebuilt: .build.info no longer lists the Era product, so the builder needs --wago-build, which the desk's itemdb-build tool cannot pass.
  • A trade-off, stated: the battleground-only healing draughts (17348, 17349, 17351, 17352) on TBC and Mists, and the Warsong tabards (19505, 19506) on Mists, now read Vendor instead of their battleground, because the CMaNGOS / SkyFire DB has a vendor outside a battleground selling them.

Changed -- the "Which click" chooser is LibAceGUIWidgets' capture widget

The /itemdb Tooltip tab's "Which click" control is now LAGW-ClickBinding (LibAceGUIWidgets MINOR 38, built for this at TOGProfessionMaster's request, contract e0f3923e3548): the player clicks it, then performs the combination. It has a Default button that restores Alt + Left Click, read from the tooltip defaults through a new private lib:_GetTooltipDefault(key). It also allows the middle and extra mouse buttons, which ParseClickBinding already accepted. On an older LibAceGUIWidgets the dropdown of 14 combinations stays. Both store the same canonical string, so no saved value changes. win.clickDropdown is renamed win.clickChooser.

Changed -- test harness pinned at b1d9bd5 (was 20453ac)

From 9c6c874 on, the harness's libs.load("LibAceGUIWidgets-1.0") includes the ClickBinding file, so the spec no longer loads it itself. No other Adoption entry in that range needed a change here; the suite is 777/777 at the new pin. The new pkgmeta.lua checker (a desk tool from 6eb30e0, asked for on inbox d6eb270e8b4f) reports no FAIL on .pkgmeta. Its three WARNs were left as they are: the "*.sh" and "*.bat" ignores match no tracked file today, but they are guards against a future script shipping; and curseforge-project-id: is not needed in .pkgmeta because every TOC carries X-Curse-Project-ID: 1558208.

Changed -- release docs

README: the source-locations pipeline paragraph states the battleground-vendor rule, and the tooltip-settings row says whereClick is set on the LAGW-ClickBinding control (dropdown on an older LibAceGUIWidgets). The CurseForge page gains the v1.1.4 notes. Checked against both files for this release's changes: no item count, build number or feature row they quote moved.

Fixed -- a stale comment about VersionCheck-1.0

ItemDB.lua and CLAUDE.md said VersionCheck-1.0 is Classic-only. Its TOC declares Retail too (120005, 120007, 120100); both comments now say so (SmexyMats peer review 34ee062fd221).


Older releases (v1.1.3 and earlier) are in CHANGELOG_ARCHIVE.md. Nothing was edited or dropped, only moved.