ItemDB-v0.4.2
What's new
Changelog
[v0.4.2] (2026-07-18) - Item sources, tank gear, weapon skill & more on-equip stats
New Features
Item source locations — every item now resolves to a real origin instead of a consumer falling back to "Other".
GetSourcespreviously knew only AtlasLoot boss drops; quest rewards, vendor items, crafted items, world drops, and PvP / reputation gear all read "Other".tools/build-item-sources.pyreads the CMaNGOS vanilla server DB (the same SQLite the proc rates come from) plus wagoMap/AreaTable/Factionnames and derives, keyed by item ID:- quest — reward item ←
quest_template←creature_(questrelation|involvedrelation)NPC → the NPC's spawn map (an instance name), else the quest'sZoneOrSortzone. So Striker's Diadem (21366) resolves to Ahn'Qiraj Temple, not "Other". - vendor —
npc_vendor→ the NPC's instance, else generic Vendor. - crafted — a create-item spell (
spell_templateEffect == 24) → its product → Crafted. - drop —
creature_loot_template→ the creature's instance map, else World Drop. - pvp — wago
ItemSparse.RequiredPVPRank > 0→ PvP, overriding other origins (the rank is the source, so High Warlord's / Grand Marshal's read PvP). - reputation — wago
ItemSparse.MinFactionID > 0→ the faction name (Argent Dawn, Cenarion Circle, The Defilers, …), added alongside the vendor zone. Continent maps (0/1/530/571) are skipped so an instance map always wins; battleground maps resolve by name (AV → Alterac Valley, WSG → Warsong Gulch, AB → Arathi Basin). Instance names are normalized to the AtlasLootSourceNamesspelling (NAME_ALIAS: wago's Ahn'Qiraj Temple→Temple of Ahn'Qiraj, Ahn'Qiraj→Ruins of Ahn'Qiraj, Deadmines→The Deadmines, Stormwind Stockade→The Stockade) so the location and boss-drop graphs agree — a consumer needs no aliases andGetSourcescan dedup. Emitted to_core/ItemLocations.lua(lib:LoadItemLocations,{ [id] = {{loc, kind}, …} }) — 8588 items on Vanilla: crafted 1669, drop 3470, quest 1987, vendor 1652, pvp 461, reputation 260. Source facts are from CMaNGOS (GPL-2.0), credited. Location:tools/build-item-sources.py.
- quest — reward item ←
GetSourcesenriched — merges the CMaNGOS locations under the AtlasLoot boss graph. Boss drops keep their full{ instanceKey, instance, boss, encounterID, source = "atlasloot" }detail; each location adds{ instance = <place>, source = <kind> }(no boss) for kindsquest/vendor/crafted/pvp/reputation/drop. Each place shows once: a location whose instance is already shown — by the boss graph, or by an earlier location (e.g. a ZG item that both drops and is a reputation reward in ZG) — is collapsed, while the boss graph's legitimate multiple-bosses-per-instance rows stand.HasSourcesis true when either source exists. New loaderlib:LoadItemLocations, storagelib.itemLoc; wired intoItemDB.tocandcore_ids' non-item skip list. Location:LibItemDB-1.0.lua,tools/itemdb_common.py.Hand-curated "do not serve" list — some items have perfectly normal client data (a real name, a sane required level, real stats) yet were never made obtainable in Classic, so no
build-hidden.pyheuristic catches them and they'd rank as unreachable top-tier BiS.tools/curated_hidden.py(MANUAL_HIDDEN = { id: reason }) is the hand table for these;build-hidden.pymerges it into each version'sHidden.luaon top of the automated name/level flags, so the items stay fully queryable for automation (GetInfo/GetLink/GetStats) and only ranking/search skip them — reusing the existingIsHidden/opts.includeHiddenpath, no lib or TOC change. First entries: Ashbringer (13262 — the original, never-released; Corrupted Ashbringer 22691 stays served) and Shard of the Defiler (17142). Only Vanilla currently loads aHidden.lua(the ranking / search features are Vanilla-only), so that's where the flags take effect. Location:tools/curated_hidden.py,tools/build-hidden.py.Life-steal & haste proc EP — two more proc effect types now score, extending the v0.4.1 proc pass. A life-steal proc ("Chance on hit: Steals 185 life",
SPELL_EFFECT_HEALTH_LEECH) is enemy damage plus an equal self-heal, so it folds into thedamage+healbuckets; a melee-haste proc (+X% attack speed for D s,MOD_MELEE_RANGED_HASTE) folds intodamageas X% more white damage over the buff. Both use the weapon's own damage/speed and score through the existing DPS→AP path — no new lib buckets. Corrupted Ashbringer's leech now adds ~126 EP for a fury warrior instead of reading nothing.build-use-effects.pynow also capturesDelay(weapon speed) for the haste math. Of the procs still unscored, ~284 are genuinely un-scoreable (enemy debuffs / stuns / utility with no proc rate) and stay flagged for the BiS backstop. Location:tools/build-use-effects.py.On-equip stats the walk missed now captured — tank avoidance, weapon skill, spell penetration, ranged haste. Vanilla delivers a whole class of stats as on-equip spell auras, not
ItemSparsestat fields, soGetItemStats(and the walk) never saw them — every tank item was missing its defining stats, and famous items like Edgemaster's Handguards showed nothing.build-equip-stats.pynow recovers them from the same on-equip aura chain it already used for hit/crit:- defense / dodge / parry / block — aura 30 (
MOD_SKILL, misc 95) →DEFENSE, aura 49 →DODGE_PCT, 47 →PARRY_PCT, 51 →BLOCK_PCT. - weapon skill (endgame: +weapon skill vs a level-63 boss cuts miss/dodge/glancing and adds
crit) — aura 30 with the 15 combat skill lines →
WEAPON_SKILL_SWORD/_AXE/_DAGGER/_MACE/_2H_*/_POLEARM/_STAFF/_FIST/_UNARMED/_BOW/_GUN/_CROSSBOW/_THROWN. Profession lines (fishing/mining/…) are excluded. - spell penetration — aura 123 (stored negative) →
ITEM_MOD_SPELL_PENETRATION(a key HawsJon already weights, so it scores for casters immediately — it was just never captured). - ranged haste — aura 557 (quivers / ammo pouches) →
RANGED_HASTE_PCT.
+309 items now carry these (EquipStats 760 → 1069), verified against Elementium Reinforced Bulwark (+7 defense), Dreadnaught Legplates (+13 defense / +1% dodge), Stronghold Gauntlets (+1% parry), Edgemaster's Handguards (+7 sword/axe/dagger), Trueaim Gauntlets (+8 bow/gun/crossbow), Enigma Robes (+20 spell pen). All merge into
GetStats/Searchand joinWEIGHT_STATS(GetWeightStats, now 40 keys). The aurasGetItemStatsalready surfaces (AP / spell power / mp5 / healing / resistance) stay excluded to avoid double-counting; non-stat auras (procs, movement, "+X vs Undead", damage shields) are skipped. Weapon skill is per-type and only helps the weapon you wield (a paperdoll applies the equipped line); HawsJon ships no weapon-skill weight, so it reads 0 until a scale provides one. Location:tools/build-equip-stats.py,LibItemDB-1.0.lua.- defense / dodge / parry / block — aura 30 (
Weapon-skill EP scoring (formula-based). Capturing weapon skill isn't enough to rank it, and no data source ships a per-point weight — the value is non-linear and target-dependent (checked HawsJon, WoWSims presets, CMaNGOS, wago: none has one). So
GetItemScorevalues it by porting the Classic attack-table math the way WoWSims implements it (sim/core/target.go): an item's+Nskill for the weapon you wield converts to fewer misses (0.05 + skillDiff×0.002above the 10-skill gap, else×0.001), fewer dodges (×0.001), and stronger glancing blows (the1.3 − 0.05×skillDiff/1.2 − 0.03×skillDiffglance multipliers, whose clamp is the ~308 soft cap) — summed over current→current+N skill as a white-DPS gain, then DPS → AP → weight. It's context-aware viaopts.attack = { weaponType, weaponSkill (current, default 300), weaponDPS (default 90), targetDefense (default 315 = a level-63 boss) }, and only counts the equipped weapon's type. Verified: Edgemaster's Handguards (+7) scores ~96 EP for a sword or dagger user at 300 skill, tapering to ~37 at 305 and ~9 at the 308 cap, and 0 for a mace user — a pure weapon-skill item that used to score ~1 EP now ranks like the BiS-tier item it is. Weapon-skill keys are formula-scored, soscoreStatsskips them (no flat double-count); they stay inWEIGHT_STATSonly for display/breakdown labels. Location:LibItemDB-1.0.lua(scoreWeaponSkill). Formula credited to WoWSims (MIT).Block value captured; tank gear can now be EP-ranked. Added block value (aura 564 — Aegis of the Blood God +30, Drillborer Disk +23, the Dreadnaught set), completing the tank stat set (defense / dodge / parry / block% / block-value; 21 items). And since the shipped HawsJon scale is DPS/healer-only and weights none of those, tank gear couldn't be ranked at all — so a new curated "tank" weight scale (
tools/build-tank-weights.py→_core/TankWeights.lua,LoadBiSWeights("tank", …)) makes it rankable viaGetItemScore(id, class, spec, {source="tank"}): warrior/paladin protection + druid feral-tank, survival-focused (Stamina-referenced). Verified: Elementium Reinforced Bulwark 80.9 (HawsJon) → 156.8 (tank). These weights are an APPROXIMATE curated default, not sim-derived — no source ships static tank weights — so tune them viaLoadBiSWeights. Location:tools/build-equip-stats.py,tools/build-tank-weights.py.Curated legendary sources — Thunderfury and Sulfuras (assembled from multiple pieces, so CMaNGOS's quest/loot graph can't place them) now resolve to Molten Core instead of "Other", via a
CURATED_SOURCESoverride inbuild-item-sources.py. Extend it as more surface.More test/placeholder items hidden —
build-hidden.py's name filter now catches "test"/"Test"/"TEST" as a whole word anywhere (case-insensitive), not just all-caps or at the start, so generated junk like "2200 Test sword 80 purple", "Fast Test Polearm" and "Level 60 Test Gear …" (~190 more on Vanilla) no longer ranks or searches. Excludes the one real "High Test Eternium Fishing Line" (high-test = a genuine fishing-line term). Location:tools/build-hidden.py.
Notes
- ~53% of core items now carry a location (up from boss drops alone). The rest is almost entirely low-level starter / vendor-trash gear (Worn Shortsword, Recruit's set) and hidden / unobtainable items — none of which surface in ranking or planning.
- Vanilla only (CMaNGOS is a Vanilla server DB); TBC / Mists keep the AtlasLoot boss graph.
- Feral attack power is now captured too. It's tooltip-only in the client (not a DB2 aura — an
aura-99 scan finds nothing, which is why WoWSims scrapes it), and it's just 5 items in the whole
game (Atiesh +420, The End of Dreams +305, Blessed Qiraji War Hammer +280, Hammer of Bestial Fury
+154, Mace of Unending Life +140). Their values are lifted from WoWSims' parsed
db.json(facts, credited) intoFERAL_ATTACK_POWER, and — since a feral druid is always in form — aliased toITEM_MOD_ATTACK_POWER_SHORTso it scores exactly like melee AP (verified: Blessed Qiraji War Hammer's +280 folds into the Attack Power row for a feral druid). Location:tools/build-equip-stats.py,LibItemDB-1.0.lua. - One stat stays uncaptured, as an honest follow-up: "+X damage vs creature type" (aura 102/131 — the captured value doesn't reconcile with the item tooltips, and 102 pairs with 131, so shipping it risks wrong/double-counted data).
- Cleaned two
unbalanced-assignmentslint hints inscoreStats/scoreUseEffectsby splitting the AP-accumulator declarations onto their own line (no behaviour change).
[v0.4.1] (2026-07-15) - On-use + proc EP scoring, EP breakdown & set-bonus API
New Features
On-use effect EP — a trinket's "Use: +280 Attack Power for 20s (2 min cd)" is a cast spell, not item stats, so a static-stat EP scored it ~0 even when it's the best trinket in the game (Earthstrike sorted to the bottom and got hidden by "upgrades only").
tools/build-use-effects.pyextracts on-use effects from the DB2 chain (ItemEffectTriggerType 0 →SpellEffectdirect stat auras →SpellMisc.DurationIndex/SpellDurationfor length +ItemEffect.CoolDownMSec) →_core/UseEffects.lua(lib:LoadUseEffects,{k, m, d, cd}per item — 17 scored on Vanilla).GetItemScorefolds in an uptime-averaged term,Σ magnitude × min(1, d/cd) × premium × weight(optsuseEffectsdefault on,usePremiumdefault 1.3 for the controllable-burst bump). New reads:lib:GetUseEffect(id)(drives a "Use: +280 AP (20s / 2m)" display) andlib:HasUnscoredProc(id). Location:tools/build-use-effects.py,LibItemDB-1.0.lua.Chance-on-hit & extra-attack proc EP — procs now score too. The missing piece — the proc rate — isn't in the client (
ProcChanceis a100/101sentinel), so it comes from the CMaNGOS vanilla server DB.tools/build-proc-rates.pyreads CMaNGOS's SQLite world DB and resolves each item's rate the way the server does:item_template.spellppmRate(tuned PPM — Thunderfury 6, Ironfoe 0.8), else a realspell_template.ProcChance, elseGetWeaponProcChance's default of 1.08 PPM for un-tuned "chance on hit" weapons (they all normalize to one constant PPM). Joined by item ID onto the effect read from the client →_core/UseEffects.lua(lib:LoadProcEffects/lib:GetProcEffect).GetItemScoreprices each proc by bucket: buff (stat × uptime), damage (DPS → attack power at 14 AP = 1 DPS → weight; extra-attack procs like Ironfoe/Flurry Axe fold in asextraSwings × avg weapon damage), heal (HPS → healing weight), mana (→ mp5); rate isppm/60(weapon-speed-independent) orchance × hitsPerSec. 181 procs + 17 on-use score on Vanilla. Proc rates are facts from CMaNGOS (GPL-2.0), credited. Location:tools/build-proc-rates.py,build-use-effects.py,LibItemDB-1.0.lua.GetSetBonusEP—lib:GetSetBonusEP(setID, classID, spec)→{ [threshold] = ep }, the EP of each set-bonus stage (only the flat-stat ones — proc bonuses score 0 and are omitted). A consumer that knows how many pieces are equipped sums the stages it's crossed, which is the real set contribution — vs. the flat per-piece amortizationGetItemScore(opts.setBonus)uses as a rough ranking proxy. Location:LibItemDB-1.0.lua.GetItemScoreBreakdown— the itemized version ofGetItemScorefor a "why this EP" tooltip:{ total, parts = { {kind, key, label, value, weight, ep}, … } }. Each contributing stat is its own row (value × weight = ep); set bonus, and every on-use and proc effect, are their own rows too. Guaranteed by construction — it calls the same scorers with the sameopts— sototal == GetItemScore(…)andΣ parts.ep == total. Generic Attack Power is one row (the winning melee/ranged, never both). The hot path (GetItemScore/ranking) is untouched — the scorers only allocate rows when a collector is passed. Location:LibItemDB-1.0.lua.Scoring-model table — a separate
_core/ScoreModel.lua(lib:LoadScoreModel/lib:GetScoreModel,tools/build-score-model.py) holds our MIT modeling constants — the on-use burst premium (moved out of hardcode), the per-class proc trigger rate (hitsPerSec; a paperdoll overrides at score time), andepPerHPS(heal-per-second → healing power) — kept separate from the licensed, immutable HawsJon weights inBiSWeights.luaso their data is never touched. Per-class overrides merge over a bakedDEFAULT_MODEL. (Damage procs need no constant — DPS is priced as attack power via the fixed 14 AP = 1 DPS.)
Bug Fixes
- Attack Power double-counted on generic-AP effects — a generic "+X Attack Power"
is stored in DB2 as two auras (99 melee + 124 ranged), because in-game generic
AP raises both attack types. Emitting both keys and summing scored the boost twice
for any class whose melee and ranged AP weights are equal (a hunter), so Earthstrike
read 52 instead of its true 26 and wrongly outranked always-on trinkets like Drake
Fang Talisman.
scoreStats/scoreUseEffectsnow takemax(meleeAP, rangedAP)instead of summing — it auto-picks the attack type each class actually uses (ranged for a hunter, melee for a warrior). PassiveGetItemStatsreports generic AP once, so the walk path was never affected (0_coreitems carry both keys). Location:LibItemDB-1.0.lua.
Known Gaps
- Some procs stay flagged, by design — enemy debuffs (an armor/stat reduction on
the target), pure-utility procs (stun, snare, dispel), and the rare proc with no rate
in any CMaNGOS tier aren't scored; they're flagged (
lib:HasUnscoredProc) so a consumer can caveat them and never hide a known-BiS item on a low score. - Extra-attack EP is a floor — it counts the weapon's base swing damage; a real extra swing also gets AP/crit scaling and can chain-proc, so the true value is higher.
- Caster flat-damage procs price via attack power (14 AP = 1 DPS), not spell power — fine for physical classes, an approximation for casters. A follow-up.
[v0.4.0] (2026-07-05) - Drop sources, Best-in-Slot / EP, class & faction filtering
New Features
Drop sources — each item now carries the instance + boss that drops it, built offline from AtlasLootClassic and re-expressed in LibItemDB's own schema. The Classic client ships no Dungeon Journal, and the obvious client-DB2 fallback (the Retail Journal, read from wago.tools) was audited against AtlasLoot and found ~40% incomplete for Classic — missing whole Cata-revamped/restructured instances (Scholomance, Upper Blackrock Spire, Scarlet Monastery, Ragefire Chasm) and 10–25% of drops even on the un-revamped raids (Molten Core 77%, Blackwing Lair 91%) — and flat wrong-era for Onyxia / Zul'Gurub / Naxxramas. So AtlasLoot is the authoritative source.
tools/build-sources.pyparsesAtlasLootClassic_DungeonsAndRaids/data.lua(data["Key"].items[].npcID+{slot, itemID}rows), intersects each itemID with the version's built_core(so a version only carries drops for content whose items it ships), and emits_core/Sources.lua(item→boss graph, boss→instance) +_core/SourceNames.lua(instance + boss display names). Coverage per version: 33 instances — every raid and dungeon plus a synthetic World Bosses instance (Kazzak, Azuregos, the four Emerald dragons): Vanilla 2,227 items / 306 bosses, TBC 2,240 / 308, Mists 2,248 / 307.We do not ship AtlasLoot's file — which-item-drops-from-which-boss is fact, not copyrightable expression, so only the facts are extracted; AtlasLootClassic (GPL-2.0, github.com/Hoizame/AtlasLootClassic) is credited in every generated header and in the README. Instance key is AtlasLoot's stable data key (a string, e.g.
"MoltenCore"); encounter key is the boss npcID.core_ids()skipsSources*files (they key instances by["Name"]=, not item IDs).LibItemDB API — loaders
lib:LoadSources(source, items, instances)andlib:LoadSourceNames({ inst=, enc= })(instance keys may be strings); readerslib:GetSources(itemID)→{ { instanceKey, instance, boss, encounterID, source }, … },lib:GetInstanceItems(instanceKey),lib:GetInstances(),lib:GetInstanceEncounters(instanceKey), pluslib:HasSources(id)/lib:HasSourceData(). An item that drops off several bosses/instances lists them all. Library MINOR bumped 5 → 6 (additive). Location:LibItemDB-1.0.lua.Source audit tool —
tools/audit-sources.pycross-checks the shipped AtlasLoot data against the wago Retail Journal (an independent second opinion, not shipped) and reports where they disagree — candidate omissions to review. This is how the ~40% Journal-incompleteness above was measured.Best-in-Slot (provider model) — the fixed BiS gear set per class/spec/phase, built from WoWSims (MIT, github.com/wowsims)
ui/<spec>/gear_sets/*.gear.jsonbytools/build-sources.py's siblingtools/build-bis.py(positional 17-slotitemsarray → slot key; phase from filename). Shipped as the"wowsims"provider;lib:LoadBiS(source, data)lets any addon or the user register/replace a list under another source name and pick it withlib:SetDefaultBiSSource. API:lib:GetBiS(classID, spec, phase),GetBiSItem,GetBiSSpecs,GetBiSPhases,IsBiS(itemID)(reverse). Vanilla: 42 sets across 8 classes._core/BiS.lua.EP ranking + per-raid BiS — the payoff of combining drops + weights + stats.
tools/build-bis-weights.pyextracts HawsJon's Classic stat weights (the community-standard set Pawn auto-installs on Classic) fromClassicHawsJon.lua(github.com/VgerMods/Pawn) — resolving each<Rating>Perlevel-60 conversion to a per-1% weight and mapping HawsJon's stat names to our keys → 37 specs / 9 classes in_core/BiSWeights.lua(lib:LoadBiSWeights, provider model). Values are used unchanged (immutable read-only"hawsjon"scale; players layer their own viaLoadBiSWeights); HawsJon / Pawn / Vger credited (CC BY-NC-ND). (WoWSims' hardcodedepWeightswere dropped — they're miscalibrated for Classic, e.g. Hunter Agility 0.64 vs Ranged AP 1.0 inverted, which sank agility gear like Ossirian's Binding to ~#30; with HawsJon it correctly ranks #2 of 701 belts.) EP weights now carry their own default ("hawsjon"), separate from the fixed-set default ("wowsims");Get/SetDefaultBiSSource,GetBiSSources,GetBiSSpecsare weight-scale-scoped (what a player's in-game weight editor picks). Runtime scoresΣ(stat × weight)overGetStats:lib:GetItemScore(itemID, classID, spec, opts),lib:RankItems(ids, …),lib:GetRaidBiS(instanceKey, classID, spec)(an instance's drops ranked — "what to want from this raid",opts.perSlotgroups by slot), andlib:GetSlotRanking(classID, spec, slot)("top N helms, in order"). Weapon DPS is weighted per hand (DPS_MAINHAND/OFFHAND/RANGED) resolved by equip slot; fragmented AP/mp5 keys are aliased. Verified: Warrior MC ranking leads with Bonereaver's Edge; Mage MC ranking is entirely different and correct.On-equip stat enrichment — vanilla implements hit/crit/spell-hit/spell-crit as on-equip spell effects, not
ItemSparsestat fields, soGetItemStats(and the walk) never reported them — leaving ranking blind to the stats that matter most.tools/build-equip-stats.pyrecovers them from the same DB2 chainbuild-effects.pyuses (ItemEffectTriggerType 1 →SpellEffectauras 52 crit / 54 hit / 55 spell-hit / 552 spell-crit, value = base+1), emits_core/ EquipStats.lua, merged intoGetStats/Searchvialib:LoadEquipStats. Vanilla: 2,660 items (Lionheart Helm → CRIT_PCT=2, HIT_PCT=2, etc.).Set bonuses — what naive scorers miss.
tools/build-sets.pyreads DB2ItemSet(membership) +ItemSetSpell(threshold → spell) →SpellEffect, decoding flat-stat bonuses into our vocabulary (proc bonuses kept but empty) →_core/Sets.lua. APIlib:GetItemSet(itemID),lib:GetSet(setID),lib:GetSetBonusStats(setID, pieces);GetItemScore(..., {setBonus=true})adds a set piece's amortized bonus EP. Vanilla: 481 sets, 1,251 bonus thresholds (Devilsaur 2pc → +2% hit, Arcanist 3pc → +18 spell power, etc.).Realm-aware data (Era vs SoD / Anniversary) — the 1.15 client's DB2 is a superset: original vanilla items (ID < 25000) plus the SoD / Fresh-Anniversary reissues and new items (ID >= 25000, mostly 200000+). Loading all of it mixed the two, so on an Era realm consumers saw duplicate / sourceless seasonal rows (e.g. "Crown of Destruction" as both 18817 and its reissue 228291).
tools/split- seasonal.pynow partitions each version's item-keyed data — base stays in place, the >= 25000 items move toData/<Version>/_seasonal/— and the overlay files self-guard on the newlib:IsSeasonalRealm()(C_Seasons.GetActiveSeason/C_SeasonInfo.GetCurrentDisplaySeasonID; seasons 2 SoD / 11 Fresh / 12 FreshHardcore). Era / Hardcore load original-Classic only; SoD / Fresh also load the overlay — automatically, so consumers need no season logic. No data is discarded. Vanilla: 17,595 base + 6,535 seasonal items. Also addslib:GetSeason().Faction filtering (Alliance / Horde) — faction-restricted gear isn't flagged by a race mask (
AllowableRaceis -1 for both "Highlander's" Alliance and "Defiler's" Horde items).tools/build-factions.pyderives the side from three signals →_core/Factions.lua(lib:LoadFactions, restricted items only, neutral omitted): (1)ItemSparse.AllowableRacerace mask (Alliance 77 / Horde 178); (2) a required reputation only one side can earn (MinFactionID→Faction.ReputationRaceMask_0); (3) PvP rank rewards — "Grand Marshal's ..." (Alliance) and "High Warlord's ..." (Horde) have identical DB2 rows (no faction field at all), so for items carrying aRequiredPVPRankthe side is read from the faction-specific rank-title prefix (the two rank ladders are disjoint bar the shared rank-3 "Sergeant", left neutral) plus an explicit "Alliance"/"Horde" name word. This is faction-only — it never filters on the player's rank; a Horde player still sees every High Warlord's weapon whether or not they've earned it, only the Alliance ones drop out. 686 restricted items on Vanilla (was 240; +446 PvP rank rewards). The ranking APIs (GetRaidBiS/GetSlotRanking/RankItems) hide the other faction's items by default viaUnitFactionGroup(opt out withopts.allFactions);lib:GetItemFaction(id)andlib:GetPlayerFaction()expose it. Also fixedcore_ids()to skip the non-item-keyed_corefiles (Sources/SourceNames/BiS/BiSWeights/Sets/Factions).Class filtering (a Rogue helm won't show for a Hunter) — class-restricted gear is flagged by
ItemSparse.AllowableClass, but the dungeon sets (D1/D2), the AQ40 tier-2.5 sets, the Naxx T3 sets and Atiesh are mis-flagged as all-class — every Bonescythe (Rogue T3) piece and Atiesh come back usable by everyone (and T3 flags are even inconsistent within a set).tools/build-classes.pytrustsAllowableClasswhere it's real (~1815 items) and corrects the mis-flagged sets via set membership (Bonescythe → Rogue, Cryptstalker → Hunter, …; 271 items across 36 curated sets) plus a 4-item map for Atiesh (marked usable by the caster classes) →_core/Classes.lua(lib:LoadClassRestrictions, mask bit =1 << (classID-1); 2,090 restricted items on Vanilla). The ranking APIs hide gear the ranked class can't use by default (opt outopts.allClasses);lib:GetItemClasses(id)/lib:ClassUsable(id, classID)expose it. This is obtainability, not stats — a hunter can't do the rogue/caster quest, so the item never appears; the hunter's own Cryptstalker/Beaststalker/Giantstalker/Lok'delar still rank.Same-name dedup in rankings — several item IDs can share one name and slot (the four class versions of Atiesh, a base item and its deprecated twin). Best-first rankings now keep only the highest-scoring one per (name, slot), which also auto-picks the class-appropriate variant (the caster Atiesh wins for a caster). Opt out with
opts.keepDuplicates. Location:RankItemsinLibItemDB-1.0.lua.Hidden (test / placeholder / above-cap) items — the client DB2 carries items no player of the version can obtain or use: never-implemented junk ("90 Epic Rogue Belt", "[PH]" placeholders, "Test …", "Deprecated …") and unimplemented / future-expansion gear with a normal name but a required level above the cap (e.g. The Twin Blades of Azzinoth and the Warglaive of Azzinoth pair, RequiredLevel 70 in a level-60 game). These are kept in the DB for completeness but no longer served:
tools/build-hidden.pyflags them by two client-derivable signals — name pattern and RequiredLevel > the version's level cap (60/70/80/85/90, from wagoItemSparse) →_core/Hidden.lua(lib:LoadHidden, 927 on Vanilla: 910 by name + 17 by level, zero false positives — Atiesh/Thunderfury/Ashbringer stay).GetInfo/GetLink/GetStats/GetSourcesstill resolve them, butRankItems/GetRaidBiS/GetSlotRanking/Searchskip them by default (opts.includeHiddento include);lib:IsHidden(id)exposes the flag. (The "This was never available to players" tooltip line is the ATT addon's curated list — not a Blizzard string nor in client data — so we use the client-derivable signals instead; ATT's full never-implemented list could layer on later for below-cap cases.)
Known Gaps
- Wrath / Cata / Retail — no drop sources: those versions have no
_coreitem data yet (Wrath/Cata are unshipped placeholders; Retail isn't shipped by this library). Sources build on top of_core, so they follow once those clients are walked andbuild-core/build-localesrun. - Localization — instance and boss names are English-only for now (AtlasLoot's
enUS keys), shipped locale-independent in
_core/SourceNames.lua. Per-locale source names are a follow-up. - BiS / EP is Vanilla-only — drops cover Vanilla/TBC/Mists, but BiS sets,
weights, equip-stat enrichment and set bonuses ship for Vanilla only so far;
TBC/Mists are a
build-bis*.py/build-equip-stats.py/build-sets.pyrun away (WoWSims hastbc-new/moprepos) once validated. - EP is approximate — a linear score (one opinion), not a full sim: stats we
don't capture (haste, expertise, defense, dodge, parry, block, weapon speed,
school-specific spell power) are unscored, so HawsJon weights for those stats are
dropped and ordering can be off where they dominate. HawsJon covers all 9 classes ×
their specs (37 scales); WoWSims still supplies the fixed gear sets and leaves
several healer/tank/paladin sets blank, so
GetBiSis thinner there than the DPS specs (the EP ranking still works for them). No class weapon/armor-proficiency filter yet, so an off-class weapon can rank for a spec. Equip auras for defense/ dodge/parry (30/49/51) aren't decoded yet. - Seasonal overlay is coarse — the
_seasonalsplit separates original-Classic from everything >= 25000, but doesn't yet distinguish SoD from Anniversary within that range (no clean DB2 flag was found — they share the 200000+ block), so a SoD or Fresh realm loads the whole overlay. And the seasonal items carry stats/names/sets but no sources or BiS yet (AtlasLoot-classic / WoWSims-classicdon't cover them; WoWSims has a separatesodrepo). Both are follow-ups.
[v0.3.0] (2026-06-01) - Consumable use-effect stats
New Features
Consumable use-effects — use-effect consumables (food, elixirs, flasks, potions, scrolls, jujus, …) now carry the stat buff they grant on use, which
GetItemStatscan't report. New offline pipelinetools/build-effects.pyreads the static wago.tools DB2 chain:Item → ItemEffect (ParentItemID, on-use TriggerType) → SpellID → SpellEffect (Effect 6 = APPLY_AURA), following theEffectAura 23periodic-trigger hop to the Well Fed spell for food. Auras are decoded to the SAMEKEY=valvocabulary as gear stat blobs:MOD_STAT(29) →ITEM_MOD_<STAT>_SHORT(misc 0–4 = Str/Agi/Sta/Int/Spi, −1 = all),MOD_RESISTANCE(22, misc is a school bitmask) →RESISTANCE<n>_NAME, spell power / attack power / mp5 / healing → theirITEM_MOD_*, plus use-only keysCRIT_PCT/SPELL_CRIT_PCT/HEALTH/MANA. Value =EffectBasePoints + 1(the Classic fixed-effect convention, verified against Elixir of the Mongoose +25 Agi/+2% crit, Flask of the Titans +1200 health, Tender Wolf Steak +12 Sta/+12 Spi). Inclusion is gated onInventoryType == 0(non-equippable) so jujus (item class 12) are captured and gear/trinket on-use procs are excluded. Emitted locale-independent toData/<Version>/_core/Effects.lua. Coverage: Vanilla 277, TBC 339, Mists 651 consumables. Location:tools/build-effects.py,Data/*/_core/Effects.lua, the three flavour.tocfiles.LibItemDB API —
lib:LoadEffects(t),lib:GetEffects(id)(just the use-effect buff stats),lib:HasEffects(id). The effect stats are merged intoGetStatsandSearch(the stat filter and resultstats), so a consumable is queryable by the stat it grants, alongside gear. Library MINOR bumped 4 → 5 (additive). Location:LibItemDB-1.0.lua.
Known Gaps
- Mists combat ratings — MoP rating buffs (crit / haste / mastery, via
EffectAura 189, a combat-rating bitmask) aren't decoded yet, so rating-only consumables on Mists are under-captured; Vanilla/TBC are flat-stat and fully covered.build-effects.pylogs unmapped auras. AddingMOD_RATING(CR bit →ITEM_MOD_*_RATING) is the follow-up.
[v0.2.0] (2026-05-30) - TBC & Mists data + version-aware pipeline
New Features
- TBC and Mists coverage —
Data/TBC/(30,032 items, captured on the Anniversary client build2.5.5.67511) andData/Mists/(88,259 items, Classic progression build5.5.3.67509), each in all 12 client languages with its random-suffix table. Joins the existing Vanilla data. Wrath and Cata have no live client yet (the Anniversary realms reach those phases later), so their.tocfiles stay empty placeholders until a client exists.
Improvements
- Version-aware pipeline —
build-core.pyandbuild-locales.pynow take just the Version (e.g.build-core.py TBC) and resolve the rest from the installed clients: they read<WoW>/.build.info, map each client by its build MAJOR to a Version (1→Vanilla, 2→TBC, 3→Wrath, 4→Cata, 5→Mists), auto-locate that client's walk SavedVariables, and fetch classID + localized names + suffixes from the matching wago build. This tracks the Anniversary realm's progression automatically — when it advances to 3.4.x,build-core.py Wrathresolves to it with no code change.itemdb_common.pygainedinstalled_clients/client_for_version/wago_build_for_version/togtools_sv;--sv/--wago-buildremain as overrides.
[v0.1.0] (2026-05-30) - Stats, random suffixes, 12 languages, core/names split
New Features
- Enriched data: item level + full stats — every item now carries its item
level and the client's fully-aggregated stat set (the walk's
GetItemStats, which includes spell power, mp5, spell healing/damage, resistances and armor — stats the raw wago stat columns omit). New API:GetStats,GetItemLevel, and stat-filteredSearch(stat,minValue,minLevel,maxLevel), so e.g. "plate with strength" is answerable offline. - Random suffixes ("of the Bear") — modern Era encodes a random suffix as a
positive
ItemRandomPropertiesID in field 8 of the item link (item:15218::::::1196). The DB ships those (name + exact stat lines per tier, from wagoItemRandomProperties+SpellItemEnchantment). New API:GetRandomProperty,GetRandomProperties,HasRandomProperty,BuildItemString,GetSuffixLink,ResolveSuffix, and suffix-awareResolveName.BuildItemString(itemID, propID)reconstructs a link the game renders the full, correctly-scaled tooltip from — no stats are computed or stored for the scaling; the client does it. - All 12 client locales for Vanilla —
deDE, enGB, enUS, esES, esMX, frFR, itIT, koKR, ptBR, ruRU, zhCN, zhTW. Names a locale hasn't translated fall back to English, so every language is complete (24,127 items, 2,033 suffix tiers).
Improvements
- core/names storage split (LibStub MINOR → 4) — locale-independent data
(quality, subclass, equip slot, item level, stats) now ships once in
Data/<Version>/_core/<Class>.lua; only localized names ship per language inData/<Version>/<locale>/Names.lua. Newlib:LoadCore/lib:LoadNames(andlib:SetBuild); the oldlib:LoadClassesis kept as a back-compat shim. This avoids duplicating the bulky numeric data across all 12 languages. - Pipeline rewrite — replaced
tools/build-data.shwithtools/build-core.py(walk SavedVariables →_core/, classID joined from wago'sItemtable since the walk's serialization drops it) andtools/build-locales.py(wagoItemSparse.Display_lang→ per-locale names with English fallback; suffix tables via the now locale-awarebuild-suffixes.py), sharingtools/itemdb_common.py. Stats come from the walk; classID + translated names come from wago. Data/classic/→Data/Vanilla/to match the ProfessionDB layout; the Vanilla.toclists the_corefiles (loaded once) plus every locale's self-guardedNames.lua+RandomProps.lua.
[v0.0.1] (2026-05-30) - Initial library
New Features
- LibItemDB-1.0 — Standalone offline item database exposed as a LibStub
library. Resolves item name ↔ id ↔ link and filters by type / subtype with no
client cache required. API:
IsReady,Count,GetMeta,GetName,GetInfo,GetQuality,GetLink,HasItem,GetID,GetClasses,GetSubClasses,Search,Iterate. Data is loaded viaLoadClassesfrom shipped, pre-built data files (no end-user scan). - Classic (Era) enUS data —
Data/classic/enUS.lua, 24,069 items captured from client build 11508 via the TOG Tools Item DB builder. Bucketed by item class; each entry packsname · quality · subClassID · equipLoc. - Ships for every flavour except Retail — per-flavour TOCs for Vanilla/Era
(11508), TBC (20505), Wrath (30405), Cataclysm (40402), and Mists (50503). A
flavour TOC lists its
Data/<version>/<locale>.lualine only once that capture exists; until then it loads the library with an empty database. - Guild version-check —
ItemDB.luaregisters a{ GetName = "LibItemDB", Version }host with VersionCheck-1.0 atPLAYER_LOGIN, so the library shows up in guild version checks like the rest of the TOG suite. Hard dependencies on Ace3 and VersionCheck-1.0 are declared in every.tocand in.pkgmeta(required-dependencies).
This mod has no additional files

