Commit graph

348 commits

Author SHA1 Message Date
Kelsi
16d004c742 feat(editor): add WSPS JSON round-trip (--export/--import-wsps-json)
Closes the editing loop on the spell-proc-trigger catalog: dump
a .wsps to JSON, hand-edit triggerSpellId / chance / PPM /
internalCooldownMs / charges / procFlags (e.g. retune Windfury
PPM from 20 to 18, add Critical to a proc's flag set so it
fires only on crits, raise an internal cooldown to slow down
spammy procs), re-import to a byte-identical binary.

procFlags is dual-encoded — int bitfield OR pipe-separated label
string ("DealtMeleeAutoAttack|DealtMeleeSpell"). The 13 flag
labels match the canonical WoW proc-event taxonomy. Importer
prefers the int form when both are present so unknown bits
round-trip losslessly.

Verified byte-identical round-trip on all three presets
(weapon / aura / talent). CLI flag count 1046 -> 1048.
2026-05-09 23:34:29 -07:00
Kelsi
16ebcdda44 feat(editor): add WSPS (Spell Proc Trigger) open catalog format
Open replacement for AzerothCore's spell_proc_event SQL table
plus the per-spell proc fields embedded in Spell.dbc. Defines
when a "trigger" spell fires in response to other spell/combat
events: Windfury Weapon procs on melee attack, Clearcasting on
damaging cast, Judgement of Wisdom on melee hit, etc.

Each entry says "when an event matching procFlags fires from a
spell matching procFromSpellId (0 = any), at procChance
probability with at most one trigger per internalCooldownMs
window, fire triggerSpellId". The procPpm field provides an
alternative procs-per-minute formula (when non-zero, supersedes
procChance and scales with weapon speed for melee procs — the
canonical WoW behavior for weapon imbues).

13-bit procFlags bitfield covers the standard event taxonomy:
DealtMeleeAutoAttack / DealtMeleeSpell / TakenMeleeAutoAttack /
TakenMeleeSpell / DealtRangedAutoAttack / DealtRangedSpell /
DealtSpell / DealtSpellHeal / TakenSpell / OnKill / OnDeath /
OnCastFinished / Critical (the last is a modifier — fires only
on crit-tagged events).

Cross-references back to WSPL (triggerSpellId references the
spell that fires; procFromSpellId optionally restricts to a
specific source spell).

Three preset emitters: --gen-sps (4 weapon-imbue procs with
canonical PPM rates and ICDs), --gen-sps-aura (4 aura-tied
procs across multiple proc-flag combinations), --gen-sps-talent
(4 talent procs including charge-consuming Clearcasting and
Nightfall variants).

Validation enforces id+name+triggerSpellId+procFlags presence,
no duplicate ids; warns on:
  - procChance outside [0..1] (engine clamps)
  - procPpm < 0 (invalid PPM rate)
  - both procChance > 0 AND procPpm > 0 set (engine prefers PPM
    so procChance is silently ignored)
  - both procChance=0 AND procPpm=0 (proc never fires)

Wired through the cross-format table; WSPS appears automatically
in all 18 cross-format utilities. Format count 88 -> 89; CLI
flag count 1041 -> 1046.
2026-05-09 23:32:25 -07:00
Kelsi
28becba00e feat(editor): add WTBR JSON round-trip (--export/--import-wtbr-json)
Closes the editing loop on the token-reward catalog: dump a
.wtbr to JSON, hand-edit rewardKind / rewardId / spent token
counts / faction gates (e.g. raise T10 helm cost from 95 to 100
Emblems, swap a server-custom mount reward to a Pet kind, lower
an Argent Tournament reward standing from Exalted to Honored),
re-import to a byte-identical binary.

Two dual-encoded fields:
  - rewardKind: int 0..7 OR "item" / "spell" / "title" /
    "mount" / "pet" / "currency" / "heirloom" / "cosmetic"
  - requiredFactionStanding: int 0..7 OR "hated" / "hostile" /
    "unfriendly" / "neutral" / "friendly" / "honored" /
    "revered" / "exalted"

Verified byte-identical round-trip on all three presets
(raid / pvp / faction). CLI flag count 1039 -> 1041.
2026-05-09 23:28:23 -07:00
Kelsi
c1c4b8fa12 feat(editor): add WTBR (Token Reward) open catalog format
Open replacement for AzerothCore's currency_token_reward SQL
table plus the per-vendor token redemption rows in npc_vendor.
Each entry says "spend N copies of token X to receive reward Y",
with reward type polymorphism: Y can be an item, a spell (taught
to the character), a title, a mount, a companion pet, a currency
conversion, an heirloom unlock, or a cosmetic (tabard / pennant /
fluff). The rewardId field's interpretation depends on the
rewardKind enum.

Distinct from WTKN (Token catalog) which defines the token
currency items themselves. WTKN says "the Champion's Seal exists
as item 44990"; WTBR says "spend 25 Champion's Seals at Argent
Tournament for the Squire's Belt (item 45517)".

Eight rewardKind values cover the full reward space (Item / Spell
/ Title / Mount / Pet / Currency / Heirloom / Cosmetic), and an
8-tier requiredFactionStanding gates by reputation
(Hated / Hostile / Unfriendly / Neutral / Friendly / Honored /
Revered / Exalted) when paired with a non-zero requiredFactionId.

Cross-references back to WTKN (spentTokenItemId), WIT (Item
rewards), WSPL (Spell rewards), WTIT (Title rewards), WMOU
(Mount rewards), WCMP (Pet rewards), WCTR (Currency conversion
rewards), and WFAC (faction-rep gating). findByToken(itemId)
is the engine helper used by vendor frames to populate the
"what can I buy with these?" list.

Three preset emitters: --gen-tbr (5 raid tier-token redemptions
consuming Trophy of the Crusade and Emblem of Frost),
--gen-tbr-pvp (5 PvP rewards spanning honor / arena / conquest
plus title and tabard kinds), --gen-tbr-faction (5 faction-
gated rewards demonstrating each standing tier from Honored
through Exalted).

Validation enforces id+name+spentTokenItemId+spentTokenCount
presence, rewardKind 0..7, requiredFactionStanding 0..7, no
duplicate ids; warns on:
  - rewardId=0 (no actual reward, vendor offers entry but
    grants nothing)
  - requiredFactionStanding > Neutral with requiredFactionId=0
    (rep gate has no faction to check)
  - Currency conversion item -> itself (typo / config bug)

Wired through the cross-format table; WTBR appears automatically
in all 18 cross-format utilities. Format count 87 -> 88; CLI
flag count 1034 -> 1039.
2026-05-09 23:26:13 -07:00
Kelsi
69857eea18 feat(editor): add --catalog-stats for single-file deep size analysis
Reports header bytes (magic + version + nameLen + name +
entryCount) vs entry-section bytes, average entry size, total
file size, the catalog's name string length, and the first
entry's id field. Useful for sizing analysis: "which catalogs
are biggest, where do the bytes go, what's the per-entry
overhead".

Goes deeper than --info-magic which just reports the magic +
version + name + count. The byte breakdown distinguishes:
  - headerBytes        — fixed-cost header
  - catalogNameBytes   — variable-length catalog label
  - entrySectionBytes  — total size of all entries combined
  - averageEntryBytes  — entrySectionBytes / entryCount

Only the first entry's id is sampled because entries have
variable-length name+description strings — multi-sampling
produces garbage for most formats. The single sampled id is
reliable because it sits at exactly headerBytes offset.

JSON sidecar via --json. Format metadata (description / category)
is included from the format table when the magic is recognized;
unknown magics still get the size breakdown but no metadata.

This is the 18th cross-format utility; the second per-file
single-target one (alongside --info-magic). CLI flag count
1033 -> 1034.
2026-05-09 23:21:32 -07:00
Kelsi
6abd3f5398 feat(editor): add --list-by-magic to filter a tree to one specific format
Walks a directory recursively and lists every file matching a
4-character magic. Reports per-file size, entry count, catalog
name, and relative path; aggregates total bytes + total entries
across the matches.

Useful for narrow per-format inventory: "show me all the WSPL
spell catalogs in my project" or "find every WCDF that defines
raid difficulty variants under my custom-content tree". The
existing --summary-dir gives a multi-format rollup; this is the
single-format zoom-in.

Magic argument is case-insensitive — "WSPL" and "wspl" both
match. If the magic is recognized in the format table the report
header includes the format description and category; if not (e.g.
a server-custom magic), the listing still works and just notes
"unknown magic — no metadata". JSON sidecar via --json. Exit 1
if no matches (so it composes into shell "if any matches, do X"
pipelines).

This is the 17th cross-format utility:
  --list-formats / --info-magic / --summary-dir / --rename-by-magic
  --bulk-rename-by-magic / --touch-tree / --tree-summary-md
  --catalog-grep / --diff-headers / --audit-tree / --magic-fix
  --bulk-validate / --bulk-export-json / --bulk-import-json
  --diff-tree / --orphan-jsons / --list-by-magic

CLI flag count 1032 -> 1033.
2026-05-09 23:18:46 -07:00
Kelsi
14c6a947ee feat(editor): add WBKD JSON round-trip (--export/--import-wbkd-json)
Closes the editing loop on the NPC service catalog: dump a .wbkd
to JSON, hand-edit serviceKind / requiresGold / factionRequiredId
/ gossipTextId (e.g. retag a city-mail NPC from Mailbox kind to
GuildBanker, raise stable master cost from 1s to 5s, gate an
auctioneer behind faction rep), re-import to a byte-identical
binary.

The exporter emits both serviceKind (int 0..11) and the human-
readable serviceKindName ("banker" / "mailbox" / "auctioneer" /
"stable-master" / "flight-master" / "trainer" / "innkeeper" /
"battlemaster" / "guild-banker" / "reagent-vendor" /
"tabard-vendor" / "misc"); the importer accepts either form.

Verified byte-identical round-trip on all three presets
(city / battle / prof). CLI flag count 1030 -> 1032.
2026-05-09 23:16:32 -07:00
Kelsi
dac8ba8cbc feat(editor): add WBKD (NPC Service Definition) open catalog format
Open replacement for AzerothCore's npc_vendor / npc_trainer /
npc_gossip / npc_options SQL tables plus the engine's hard-coded
service-type dispatch. Defines the kinds of services NPCs can
offer (Banker / Mailbox / Auctioneer / StableMaster /
FlightMaster / Trainer / Innkeeper / Battlemaster / GuildBanker
/ ReagentVendor / TabardVendor / Misc) and the per-service
metadata (gold cost, faction gating, gossip text id).

When a player right-clicks an NPC, the engine looks at the
NPC's serviceId list (from WCRT.npcFlags or equivalent) and
dispatches to the appropriate service-frame handler — Banker
opens the inventory expansion frame, Auctioneer opens the
auction house, StableMaster opens the pet stable. This catalog
defines what each service actually does and what preconditions
it requires.

Cross-references back to WCRT (creature.npcFlags decodes into a
list of service ids defined here), WFAC (factionRequiredId
references factionId for rep-gated services like Argent
Tournament), and WGSP (gossipTextId references menuId for the
"How can I help you?" dialogue line).

Three preset emitters: --gen-bkd (5 city services typical of a
capital city), --gen-bkd-battle (3 battlemaster services for
each Vanilla BG queue), --gen-bkd-profession (4 profession
services). findByKind(kind) is the engine helper used by NPC
spawning to find e.g. all FlightMaster services configured for
a server.

Validation enforces id+name presence, serviceKind 0..11, no
duplicate ids; warns on:
  - Mailbox kind with non-zero gossipTextId (mailboxes are
    gameobject services with no NPC dialogue; gossip won't show)
  - Innkeeper kind with gossipTextId=0 (no welcome/bind dialog;
    will silently bind the hearthstone)
  - Battlemaster kind with non-zero requiresGold (battle queue
    services are typically free)

Wired through the cross-format table; WBKD appears automatically
in all 16 cross-format utilities. Format count 86 -> 87; CLI
flag count 1025 -> 1030.
2026-05-09 23:15:20 -07:00
Kelsi
fd0e2cc50b feat(editor): add WIFS JSON round-trip (--export/--import-wifs-json)
Closes the editing loop on the item-flag bit catalog: dump a
.wifs to JSON, hand-edit bitMask hex / flagKind / isPositive
(e.g. add a server-custom Heroic25man flag at bit 0x10000000,
re-tag NoLoot from Drop kind to Quality kind, mark BindToAccount
as positive for an account-wide-loot server), re-import to a
byte-identical binary.

Two dual-encoded fields:
  - flagKind: int 0..6 OR "quality" / "drop" / "trade" /
    "magic" / "account" / "server" / "misc"
  - isPositive: bool OR int (machine-generated sidecars work
    too)

Hex bitMasks export as raw uint32 — pasting "0x40000000" hex
values into JSON works directly.

Verified byte-identical round-trip on all three presets
(std / bind / srv). CLI flag count 1023 -> 1025.
2026-05-09 23:12:01 -07:00
Kelsi
0ceb70f3e7 feat(editor): add WIFS (Item Flag Set) open catalog format
Open replacement for the bit-flag meanings used in Item.dbc /
item_template.Flags. Documents every individual bit of the
32-bit item flags field with a human-readable name, description,
kind classification, and is-positive hint.

WoW's Item.dbc Flags field packs ~25 bits of metadata like
Heroic, Lootable, NoLoot, Conjured, BindOnPickup, BindOnEquip —
each controlling a specific gameplay behavior. The hardcoded
client knows what each bit means via a switch statement; this
catalog exposes that table to data-driven editors so:
  - server admins can document custom flag bits
  - tooltip generators can decode "why is this item soulbound?"
    via flag-name lookup (decode(0x40240000) returns
    ["Heroic", "BindOnPickup", "Unique"])
  - validators can warn about contradictory flag combinations

Seven flagKind values classify the bit families (Quality / Drop
/ Trade / Magic / Account / Server / Misc), and an isPositive
hint tells UIs whether the flag enhances the item (green) or
restricts it (red).

Cross-references back to WIT (decodes WIT.flags into the
matching named flag list) and WIQR (validators can pair Heroic
flag with WIQR Epic+ quality requirement).

Three preset emitters: --gen-ifs (8 canonical Item.dbc bits
matching the standard 3.3.5a constants), --gen-ifs-binding (5
binding-related flags BindOnPickup / BindOnEquip / etc — all
restrictive so isPositive=0), --gen-ifs-server (5 server-custom
bits in the upper range demonstrating how to overlay extra
metadata without colliding with Blizzard's bits).

Validation enforces id+name+bitMask presence, flagKind 0..6, no
duplicate ids, no duplicate bitMasks (collision means engine
would only honor first matching name when decoding); warns on
multi-bit masks (unusual — usually want individual bits).

decode(flagsValue) is the engine helper that expands a raw
flags integer into its named flag list — used directly by the
tooltip generator and item info renderers. Wired through the
cross-format table; WIFS appears automatically in all 16
cross-format utilities. Format count 85 -> 86; CLI flag count
1018 -> 1023.
2026-05-09 23:10:35 -07:00
Kelsi
f43b444056 feat(editor): add --orphan-jsons to find sidecars without binaries
Walks a directory recursively and flags every .wXXX.json sidecar
whose matching binary .wXXX is missing. Useful after deleting or
moving binary catalogs — the orphan JSON sidecars accumulate as
noise and may shadow re-imports if --bulk-import-json runs over
the tree (since import would re-create the binary from possibly
stale JSON).

Reports total sidecars / paired / orphan counts, lists orphan
paths with their expected binary, and suggests recovery
(re-import to recreate from JSON, or delete the orphan
sidecars). Returns exit 1 if any orphans found, so it composes
into shell pipelines and CI checks.

Companion to --bulk-export-json + --bulk-import-json: those two
go between binary <-> JSON; this one keeps the binary side
in sync. Suggested workflow:
    --bulk-export-json mydir   # generate sidecars
    git rm something.wsrg       # delete a binary
    --orphan-jsons mydir        # detects something.wsrg.json
                                 # is now orphaned

Reuses the same case-insensitive extension matcher as
--audit-tree and --magic-fix. CLI flag count 1017 -> 1018.

This is the 16th cross-format utility:
  --list-formats / --info-magic / --summary-dir / --rename-by-magic
  --bulk-rename-by-magic / --touch-tree / --tree-summary-md
  --catalog-grep / --diff-headers / --audit-tree / --magic-fix
  --bulk-validate / --bulk-export-json / --bulk-import-json
  --diff-tree / --orphan-jsons
2026-05-09 23:07:09 -07:00
Kelsi
7d7e1d73dc feat(editor): add WSCS JSON round-trip (--export/--import-wscs-json)
Closes the editing loop on the skill-cost catalog: dump a .wscs
to JSON, hand-edit costKind / skill ranges / requiredLevel /
copperCost (e.g. retune Apprentice profession from 1s to 5s for
a faster server, raise Cold Weather Flying from lvl 77 to 75,
add a custom WeaponSkill tier above Master), re-import to a
byte-identical binary.

The exporter emits both costKind (int 0..4) and the human-
readable costKindName ("profession" / "weapon" / "riding" /
"class-skill" / "misc"); the importer accepts either form.

Verified byte-identical round-trip on all three presets
(prof / weapon / riding). CLI flag count 1015 -> 1017.
2026-05-09 23:05:23 -07:00
Kelsi
b10bc2be5d feat(editor): add WSCS (Skill Cost) open catalog format
Open replacement for Blizzard's SkillCostsData.dbc plus the
per-rank training cost tables. Defines the tiered progression of
trainable skills: each rank unlocks a skill range, requires a
minimum character level, and costs a fixed amount of gold to
learn.

The canonical 6-tier profession progression captured by the
default preset:
  Apprentice    skill 0-75     lvl 5    1s
  Journeyman    skill 50-150   lvl 10   5s
  Expert        skill 125-225  lvl 20   1g
  Artisan       skill 200-300  lvl 35   5g
  Master        skill 275-375  lvl 50   10g
  Grand Master  skill 350-450  lvl 65   25g

Same shape applies to weapon skills (free, level-gated, capped at
5x char level) and riding skills (canonical Vanilla / TBC / WotLK
gold costs from 90g Apprentice through 5000g Artisan flying down
to 1000g Cold Weather Flying).

Five costKind values cover the full training-skill space
(Profession / WeaponSkill / RidingSkill / ClassSkill / Misc).
Each entry's copperCost stores the cost in copper (1g = 10000c)
which the info renderer pretty-prints as "25g 0s 0c".

Cross-references back to WSKL — skill entries reference costId
here for the tiered training schedule. nextTrainable(currentSkill,
characterLevel) is the engine helper that returns the lowest-rank
tier a character qualifies for and hasn't capped yet — used by
trainer NPCs to populate their offered-skill list.

Three preset emitters: --gen-scs (6 profession tiers), --gen-scs-
weapon (5 weapon skill tiers), --gen-scs-riding (5 riding tiers
with canonical gold costs).

Validation enforces id+name presence, costKind 0..4, no duplicate
ids, min<max range; warns on:
  - requiredLevel > 80 (unreachable at WotLK cap)
  - RidingSkill with requiredLevel < 20 (Apprentice canonically
    unlocks at 20)
  - Profession kind with copperCost=0 (every standard tier costs
    at least a copper — usually a config bug)

Wired through the cross-format table; WSCS appears automatically
in all 15 cross-format utilities. Format count 84 -> 85; CLI
flag count 1010 -> 1015.
2026-05-09 23:04:02 -07:00
Kelsi
a7057393eb feat(editor): add WIQR JSON round-trip (--export/--import-wiqr-json)
Closes the editing loop on the item-quality catalog: dump a
.wiqr to JSON, hand-edit color hex / vendor multiplier / level
gates / disenchant flag / border texture path (e.g. retune Epic
purple from #a335ee to #c33fff for a brighter look, raise
Heirloom unlock from lvl 80 to 85, mark Legendary as
disenchantable, swap a server-custom tier's border texture),
re-import to a byte-identical binary.

The colors export as raw RGBA uint32 — direct pasting of
0xAARRGGBB hex values without conversion. canBeDisenchanted
round-trips as a JSON bool but accepts int as well so machine-
generated sidecars work too.

Verified byte-identical round-trip on all three presets
(std / srv / raid). CLI flag count 1008 -> 1010.
2026-05-09 23:00:32 -07:00
Kelsi
efb88be366 feat(editor): add WIQR (Item Quality) open catalog format
Open replacement for the hardcoded item quality tiers in the
WoW client (Poor / Common / Uncommon / Rare / Epic / Legendary
/ Artifact / Heirloom). Defines each tier's tooltip text color,
inventory slot border color, vendor price multiplier, drop-level
gating, and disenchant eligibility.

The hardcoded client uses a fixed color table (gray/white/green/
blue/purple/orange/red/gold). This catalog lets server admins:
  - retune the colors (rename "Epic" to "Tier 1" with custom hex)
  - add server-custom tiers above Heirloom
  - change vendor markup per tier (legendary 50x base price)
  - gate quality drops by character level (Heirlooms unlock 80)

The standard preset reproduces the canonical 8-tier scale with
exact hex values from the live client (#9d9d9d through #00ccff)
and standard disenchant rules (Common+ disenchantable, Legendary
and Artifact aren't). The server-custom preset shows 4 tiers
above the standard range with non-standard pricing (Junk 0.1x,
QuestLocked 0.0x unsellable). The raid preset gates 4
progression tiers behind minLevelToDrop=60 with escalating
vendor multipliers up to 50x for Legendary.

Cross-references back to WIT — item entries reference qualityId
here for tooltip color and sort order. canDropAtLevel(id, lvl)
is the engine helper used by loot generation.

Validation enforces name presence, no duplicate ids,
vendorPriceMultiplier >= 0, minLevelToDrop <= maxLevelToDrop;
warns on:
  - minLevelToDrop > 80 (unreachable at WotLK cap)
  - vendorPriceMultiplier > 100x (sanity check the economy)
  - nameColorRGBA with alpha=0 (text would be invisible in
    tooltips — common bug when copy-pasting RGB hex without
    alpha byte)

Wired through the cross-format table; WIQR appears automatically
in all 15 cross-format utilities. Format count 83 -> 84; CLI
flag count 1003 -> 1008.
2026-05-09 22:59:27 -07:00
Kelsi
18f07f1b8a feat(editor): add WAUR JSON round-trip (--export/--import-waur-json)
Closes the editing loop on the spell-aura-type catalog: dump a
.waur to JSON, hand-edit auraKind / targetingHint / tick interval
/ stack rules (e.g. retune PeriodicDamage from 3000ms to 2500ms
ticks for a faster server, mark a custom aura as
HostileOnly+stackable up to 10, switch a server-custom DoT from
StatMod to Periodic kind), re-import to a byte-identical binary.

Three dual-encoded fields:
  - auraKind: int 0..8 OR "periodic" / "stat-mod" / "damage-mod"
    / "movement" / "visual" / "trigger" / "resource" / "control"
    / "misc"
  - targetingHint: int 0..3 OR "any" / "self" / "hostile" /
    "beneficial"
  - isStackable: bool OR int (machine-generated sidecars work
    too)

Verified byte-identical round-trip on all three presets
(periodic / stats / movement). CLI flag count 1001 -> 1003.
2026-05-09 22:56:20 -07:00
Kelsi
7d3430c8fe feat(editor): add WAUR (Spell Aura Type) — companion to WSEF, CLI flag count breaks 1000
Open replacement for the SpellEffect.EffectAuraType field
meanings used when SpellEffect.Effect=APPLY_AURA. Defines what
each aura-type integer value actually does once an aura is
attached to a unit — PERIODIC_DAMAGE ticks damage every N
seconds, MOD_STAT adds a stat bonus, MOD_INCREASE_SPEED scales
movement, MOD_DAMAGE_PERCENT_DONE scales spell power, etc.

Companion to WSEF — together they cover the full spell-effect
classification space:
  WSEF: outer effect ID — what does the effect DO?
        (APPLY_AURA, SCHOOL_DAMAGE, HEAL, etc)
  WAUR: inner aura type — when WSEF=APPLY_AURA, what KIND
        of aura is applied? (PERIODIC_DAMAGE, MOD_STAT,
        STUN, ROOT, etc)

Nine auraKind values (Periodic / StatMod / DamageMod /
Movement / Visual / Trigger / Resource / Control / Misc)
classify the major behavior families. Periodic auras carry an
updateFrequencyMs (canonical 3s for DoT/HoT, 2s for energize,
1s for fast triggers). Stackable auras carry a maxStackCount.

Cross-references back to WSEF (this catalog is the secondary
classification that WSEF entry id 6 (APPLY_AURA) dispatches
into) and forward to WSPL (spell entries with effect=APPLY_AURA
reference an auraTypeId here).

Three preset emitters: --gen-aur (5 periodic auras with
canonical tick intervals), --gen-aur-stats (5 stat-modifier
auras instantly applied on attach), --gen-aur-movement (4
movement-impairing CC auras typical of crowd-control spells).

Validation enforces name presence, auraKind 0..8, targetingHint
0..3, no duplicate ids; errors on Periodic kind without
updateFrequencyMs (would never tick); warns on:
  - non-Periodic/Trigger kinds with updateFrequencyMs > 0
    (engine ignores tick interval)
  - maxStackCount > 0 with isStackable=false (cap unreachable)

Wired through the cross-format table; WAUR appears automatically
in all 15 cross-format utilities. Format count 82 -> 83;
CLI flag count 996 -> 1001 — broke the 1000-flag mark.
2026-05-09 22:54:53 -07:00
Kelsi
9f5678f67e feat(editor): add WSEF JSON round-trip (--export/--import-wsef-json)
Closes the editing loop on the spell-effect-type catalog: dump a
.wsef to JSON, hand-edit effectKind / behaviorFlags / baseAmount
(e.g. tag a server-custom effect ID as Damage kind, add
IgnoresImmunities to a custom dispel, retune ScriptedHeal's
default baseAmount), re-import to a byte-identical binary.

Two dual-encoded fields:
  - effectKind: int 0..9 OR "damage" / "heal" / "aura" /
    "energize" / "trigger" / "movement" / "summon" / "dispel"
    / "dummy" / "misc"
  - behaviorFlags: int bitfield OR pipe-separated label string
    ("RequiresTarget|IsHostileEffect|TriggersGCD"). Importer
    prefers int form when both present so unknown bits round-
    trip losslessly.

Verified byte-identical round-trip on all three presets
(damage / healing / aura). CLI flag count 994 -> 996.
2026-05-09 22:51:40 -07:00
Kelsi
24e9a55a10 feat(editor): add WSEF (Spell Effect Type) open catalog format
Open replacement for the SpellEffect.Effect field meanings in
Spell.dbc plus the engine's hard-coded effect dispatch table.
Defines what each spell-effect integer value actually does —
SCHOOL_DAMAGE=2 deals magical damage, DUMMY=3 is a script hook,
HEAL=10 restores health, ENERGIZE=30 restores power,
APPLY_AURA=6 attaches a buff/debuff, etc.

WotLK's Spell.dbc has 192+ effect type integers, each with its
own resolver in the spell engine. This catalog lets the engine
look up "given effect=10, what resolution behavior do I run?"
via a single table lookup instead of a hard-coded switch
statement, and lets server-custom spells reference new effect
IDs without touching engine code.

Ten effectKind values capture the major behavior families
(Damage / Heal / Aura / Energize / Trigger / Movement / Summon /
Dispel / Dummy / Misc), and a 6-bit behaviorFlags field
captures targeting/gating semantics:
  - RequiresTarget       — must have a target
  - RequiresLineOfSight  — LoS check on target
  - IsHostileEffect      — hostile only (PvP gating)
  - IsBeneficialEffect   — friendly only
  - IgnoresImmunities    — bypasses Bubble / IBF / etc
  - TriggersGCD          — counts toward GCD

Distinct from WAUR (Spell Aura Type, future format) which is the
secondary classification used when effectType is APPLY_AURA. The
two together cover the full spell-effect classification space.

Three preset emitters: --gen-sef (5 damage effects covering
typical Spell.dbc damage IDs), --gen-sef-healing (4 heal
effects all flagged IsBeneficialEffect), --gen-sef-aura (5
aura-application effects covering single-target / pet /
party-wide / area variants).

Validation enforces name presence, effectKind 0..9, no duplicate
ids; warns on:
  - both Hostile and Beneficial flags set (engine picks Hostile,
    contradiction suggests config bug)
  - Damage kind without TriggersGCD (most damage should be on GCD
    — env damage is the canonical exception)
  - Heal kind without IsBeneficialEffect (engine treats heal as
    ungated, may damage enemies)

Wired through the cross-format table; WSEF appears automatically
in all 15 cross-format utilities. Format count 81 -> 82; CLI flag
count 989 -> 994.
2026-05-09 22:50:13 -07:00
Kelsi
a97bc676db feat(editor): add --diff-tree to compare two directories of .w* catalogs
Walks both trees in parallel, classifies each file by its 4-byte
magic, and bucks differences into five categories:
  - only-in-A         file present in A, missing from B (removed)
  - only-in-B         file present in B, missing from A (added)
  - magic-changed     same path but the format swapped
                      (e.g. somebody renamed a .wsrg to .wmat)
  - size-changed      same magic, different byte size
                      (content was edited)
  - identical         same magic, same size

Exit 1 if any category but identical is non-zero, so it composes
into shell pipelines and CI. JSON sidecar via --json.

Useful for project-version comparison: did anything actually
change between two snapshots? --diff-tree answers in one pass.
Pairs naturally with the existing --diff-headers (which goes
deeper on a single file pair) and the --bulk-* / --audit-tree
family of cross-tree utilities.

Files whose magic isn't recognized by the format table are
silently skipped so unrelated junk in the tree (build artifacts,
temp files) doesn't pollute the diff. Identity check is
magic+size only — true byte-equality would need a hash and the
heuristic is good enough for the typical use case.

This is the 15th cross-format utility:
  --list-formats / --info-magic / --summary-dir / --rename-by-magic
  --bulk-rename-by-magic / --touch-tree / --tree-summary-md
  --catalog-grep / --diff-headers / --audit-tree / --magic-fix
  --bulk-validate / --bulk-export-json / --bulk-import-json
  --diff-tree

CLI flag count 988 -> 989.
2026-05-09 22:46:52 -07:00
Kelsi
6b2bfb0f5a feat(editor): add WACR JSON round-trip (--export/--import-wacr-json)
Closes the editing loop on the achievement-criteria catalog: dump
a .wacr to JSON, hand-edit criteriaType / targetId / requiredCount
/ timeLimitMs / progressOrder (e.g. retune a kill-count from 50
boars to 25, swap a quest progression target, add a time limit to
turn a normal achievement into a speedrun, reorder progressOrder
so the easiest sub-objective shows first in the UI), re-import to
a byte-identical binary.

The exporter emits both criteriaType (int 0..12) and the human-
readable criteriaTypeName ("kill-creature" / "reach-level" /
"complete-quest" / "earn-gold" / "gain-honor" / "earn-reputation"
/ "explore-zone" / "loot-item" / "use-item" / "cast-spell" /
"pvp-kill" / "dungeon-run" / "misc"); the importer accepts either
form. The 13-way enum is the largest single-field dual-encoding
implemented so far in any catalog round-trip.

Verified byte-identical round-trip on all three presets
(kill / quest / mixed). CLI flag count 986 -> 988.
2026-05-09 22:45:02 -07:00
Kelsi
94e145541a feat(editor): add WACR (Achievement Criteria) open catalog format
Open replacement for Blizzard's Achievement_Criteria.dbc. Defines
the individual progression criteria that a character must
complete to earn an achievement.

Each WACH achievement has a tree of WACR criteria — "Kill 100
boars" is one criteria entry with criteriaType=KillCreature,
targetId=boarCreatureId, requiredCount=100. Multi-criteria
achievements (e.g. "Visit all 3 capital cities") have one entry
per sub-objective, all referencing the same achievementId, with
progressOrder determining their display sequence in the
achievement UI.

Thirteen criteriaType values cover the full progression variety:
  KillCreature / ReachLevel / CompleteQuest / EarnGold /
  GainHonor / EarnReputation / ExploreZone / LootItem /
  UseItem / CastSpell / PvPKill / DungeonRun / Misc

The targetId field is type-polymorphic — for KillCreature it
references WCRT.creatureId, for CompleteQuest it references
WQT.questId, for ExploreZone it's a WMS.zoneId, etc. The engine
interprets it based on criteriaType.

Cross-references back to WACH (achievementId), WCRT
(KillCreature.targetId), WQT (CompleteQuest.targetId), WIT
(LootItem/UseItem.targetId), WMS (ExploreZone.targetId), WSPL
(CastSpell.targetId). findByAchievement(achId) returns all
criteria for an achievement sorted by progressOrder — used
directly by the achievement UI to render the progress checklist.

Three preset emitters: --gen-acr (5 kill criteria under one
composite achievement showing different creature targets),
--gen-acr-quest (4-step quest progression), --gen-acr-mixed
(5 cross-type criteria demonstrating the full CriteriaType
variety).

Validation enforces id+name+achievementId presence,
criteriaType 0..12, no duplicate ids; warns on:
  - missing targetId for type-specific kinds (KillCreature,
    CompleteQuest, etc. — engine cannot track without it)
  - ReachLevel with requiredCount > 80 (above WotLK cap)
  - timeLimitMs set on non-time-sensitive types (engine
    ignores it for ReachLevel / EarnGold)
  - requiredCount=0 (criteria completes instantly on first
    progress event — usually a misconfig)

Wired through the cross-format table; WACR appears automatically
in all 14 cross-format utilities. Format count 80 -> 81; CLI flag
count 981 -> 986.
2026-05-09 22:43:44 -07:00
Kelsi
8e85278d89 feat(editor): add WSPR JSON round-trip (--export/--import-wspr-json)
Closes the editing loop on the spell-reagent catalog: dump a
.wspr to JSON, hand-edit reagentKind / spellId / per-slot
itemId+count pairs (e.g. swap a Mage Portal's reagent from Rune
of Portals to a server-custom item, change a Warlock summon to
need 2 Soul Shards instead of 1, add Tier-3 Catalyst entries
for upgraded spell variants), re-import to a byte-identical
binary.

Reagent slot arrays (8 itemIds, 8 counts) are exported as JSON
arrays — easier to read and edit than 16 individual numbered
fields. Importer pads arrays shorter than 8 with zeros and
silently truncates longer arrays, so hand-edited sidecars don't
need to spell out every empty slot.

The exporter emits both reagentKind (int 0..4) and the human-
readable reagentKindName ("standard" / "soul-shard" /
"focused-item" / "catalyst" / "tradeable"); the importer accepts
either form.

Verified byte-identical round-trip on all three presets
(mage / warlock / rez). CLI flag count 979 -> 981.
2026-05-09 22:40:12 -07:00
Kelsi
d97f4bf5db feat(editor): add WSPR (Spell Reagent) — 80th open format milestone
Open replacement for the per-spell reagent fields in Spell.dbc
(Reagent[8] + ReagentCount[8]). Defines the item reagents that a
spell consumes from the caster's inventory each time it's cast —
Mage Portal needs a Rune of Portals, Resurrection needs a Holy
Candle (focused, not consumed), Warlock summons consume Soul
Shards.

One entry per reagent-using spell — most spells have no reagents
and are absent from this catalog. Each entry can list up to 8
(itemId, count) pairs which all must be present for the spell
to cast. Five reagentKind values capture the variety of reagent
semantics:
  - Standard      — ordinary consumed reagent
  - SoulShard     — warlock-specific shard tracking
  - FocusedItem   — required to cast but NOT consumed
                    (Symbol of Divinity for Resurrection)
  - Catalyst      — enables a stronger version of the spell
  - Tradeable     — crafting reagent for trade-skill recipes

Cross-references back to WSPL (every entry references a spellId)
and WIT (every reagent itemId references an item entry).
findBySpell(spellId) is the primary engine lookup.

Three preset emitters: --gen-spr (4 mage portal/teleport
reagents using Rune of Teleportation 17031), --gen-spr-warlock
(4 demon summons each consuming 1 Soul Shard 6265),
--gen-spr-rez (3 resurrection variants demonstrating each
ReagentKind including a no-reagent Druid Rebirth and a
focused-item Priest Resurrection).

Validation enforces id+name+spellId presence, reagentKind 0..4,
no duplicate ids; warns on:
  - slot itemId/count mismatch (id without count or vice versa)
  - SoulShard kind with non-canonical reagent (not item 6265)
  - FocusedItem kind with no reagent slots set (focused-item
    gating has nothing to gate)
  - duplicate spellId across entries (engine honors only first)

This is the 80th open format milestone. Wired through the
cross-format table; WSPR appears automatically in all 14
cross-format utilities. Format count 79 -> 80; CLI flag count
974 -> 979.
2026-05-09 22:38:36 -07:00
Kelsi
736ec3a1c0 feat(editor): add --bulk-export-json + --bulk-import-json paired utilities
Two new cross-format utilities for git-friendly catalog editing:

  --bulk-export-json <dir>  recursively walks the tree, peeks
                            each file's magic, and dispatches the
                            per-format --export-X-json flag for
                            every .w* it finds. Writes one .json
                            sidecar per binary.

  --bulk-import-json <dir>  inverse direction — recursively walks
                            *.wXXX.json sidecars and dispatches
                            the per-format --import-X-json flag
                            to write back the byte-identical
                            binary.

Both report total / processed / failed / skipped counts and exit
1 on any failure. Asset-style formats with no per-format JSON
exporter are counted in the "skipped" bucket.

Use case — git-friendly diffs of binary catalogs:
    --bulk-export-json mydir              # convert binaries to JSON
    git add mydir/*.json && git commit    # version control as text
    git diff                              # see exact catalog changes
    --bulk-import-json mydir              # restore binaries

Verified end-to-end: 5 different format presets (WSRG, WCDF,
WMAT, WTLE, WCTR) round-trip byte-identical through export -> JSON
-> import. Reuses the same shellQuote / WEXITSTATUS scaffolding
as --bulk-validate. CLI flag count 972 -> 974.

This brings the cross-format utility count to 14:
  --list-formats / --info-magic / --summary-dir / --rename-by-magic
  --bulk-rename-by-magic / --touch-tree / --tree-summary-md
  --catalog-grep / --diff-headers / --audit-tree / --magic-fix
  --bulk-validate / --bulk-export-json / --bulk-import-json
2026-05-09 22:35:12 -07:00
Kelsi
4426f26f79 feat(editor): add WCTR JSON round-trip (--export/--import-wctr-json)
Closes the editing loop on the currency-type catalog: dump a
.wctr to JSON, hand-edit currencyKind / cap values / categoryId
/ isAccountWide / iconPath (e.g. raise Conquest Points weekly
cap from 1650 to 2200, retag a server-custom currency from
Misc to FactionToken kind, mark Honor Points as account-wide
for a casual server), re-import to a byte-identical binary.

The exporter emits both currencyKind (int 0..5) and the human-
readable currencyKindName ("pvp-honor" / "pve-raid" /
"faction-token" / "event-token" / "crafting" / "misc"); the
importer accepts either form. isAccountWide round-trips as a
JSON bool but accepts int as well so machine-generated sidecars
work too.

Verified byte-identical round-trip on all three presets
(pvp / pve / faction). CLI flag count 970 -> 972.
2026-05-09 22:33:12 -07:00
Kelsi
b8bd80cb35 feat(editor): add WCTR (Currency Type) open catalog format
Open replacement for Blizzard's CurrencyTypes.dbc plus the
per-currency cap tables in CurrencyCategory.dbc. Defines the
in-game currencies that are NOT regular item stacks: Honor
Points, Arena Points, Justice Points, Valor Points, Conquest
Points, plus the various faction tokens (Champion's Seal,
Wintergrasp Mark of Honor, Emblem of Frost).

Distinct from regular items in WIT — currencies are tracked
per-character as scalar quantities with weekly + absolute caps,
not as stackable inventory slots. Some currencies are still
backed by a WIT item entry for the icon and tooltip text
(itemId field), while others (Honor, Arena) live entirely in
the currency system.

The cap model captures both shapes:
  - maxQuantity = absolute lifetime cap (Honor Points 75k)
  - maxQuantityWeekly = weekly earn cap, no absolute cap
    (Conquest Points 1650/wk)
  - both 0 = uncapped (faction tokens, Emblem of Frost)
earnableNow(id, current, weekly) is the engine helper that
returns the smaller of (remaining absolute, remaining weekly).

Cross-references back to WIT (itemId for tooltip art) and
WFAC (categoryId references factionId for FactionToken kind —
the rep gate that lets you spend the token).

Three preset emitters: --gen-ctr (4 PvP currencies covering
absolute, weekly-only, and uncapped tiers), --gen-ctr-pve (4
PvE raid currencies with same cap variety), --gen-ctr-faction
(4 faction tokens with their categoryId pointing at WFAC
faction ids).

Validation enforces id+name presence, currencyKind 0..5, no
duplicate ids; warns on:
  - maxQuantityWeekly > maxQuantity (weekly cap will never be
    reached, absolute cap blocks first)
  - FactionToken kind with categoryId=0 (rep gate breaks)
  - no caps + no itemId + no iconPath (currency has no display
    data and unbounded earn rate)

Wired through the cross-format table; WCTR appears automatically
in all 12 cross-format utilities. Format count 78 -> 79; CLI flag
count 965 -> 970.
2026-05-09 22:31:42 -07:00
Kelsi
fa23aff6e9 feat(editor): add WTLE JSON round-trip (--export/--import-wtle-json)
Closes the editing loop on the talent-tab catalog: dump a .wtle
to JSON, hand-edit roleHint / classMask / displayOrder /
iconPath / backgroundFile (e.g. swap a Mage Frost tab from DPS
to Hybrid, point Holy Paladin's tab to a custom background art
file, reorder displayOrder to put Protection first), re-import
to a byte-identical binary.

The exporter emits both roleHint (int 0..4) and the human-
readable roleHintName ("dps" / "tank" / "healer" / "hybrid" /
"pet"); the importer accepts either form.

Verified byte-identical round-trip on all three presets
(warrior / mage / paladin). CLI flag count 963 -> 965.
2026-05-09 22:28:28 -07:00
Kelsi
bf8d55cb3e feat(editor): add WTLE (Talent Tab) open catalog format
Open replacement for Blizzard's TalentTab.dbc plus the per-tab
fields in Spell.dbc / Talent.dbc. Defines the three talent trees
that each class has — Warrior: Arms / Fury / Protection;
Mage: Arcane / Fire / Frost; Paladin: Holy / Protection /
Retribution; etc.

Each tab carries its own name, role hint (DPS / Tank / Healer /
Hybrid / PetClass), display order in the talent UI, background
artwork path (e.g. "WarriorArms" for the parchment background),
icon path, and the class bitmask it belongs to.

Distinct from WTAL (which defines individual talent points) —
WTLE says "the Arms tree exists for Warriors, displays in tab 1,
is a DPS spec"; WTAL says "Mortal Strike is a 1-point talent in
the Arms tree, row 7, requires Improved Charge as a prerequisite".

Cross-references back to WCHC (classMask uses the same bit
layout) and forward to WTAL (talent entries reference tabId
here). findByClass(classBit) returns all tabs for a class
sorted by displayOrder — the talent UI uses this directly to
populate its tab buttons.

Three preset emitters: --gen-tle (Warrior 3 tabs with two DPS +
one Tank), --gen-tle-mage (Mage 3 DPS tabs), --gen-tle-paladin
(Paladin 3 tabs covering all three roles in one preset).

Validation enforces id+name+classMask presence (classMask=0
means no class can use the tab — usually a config bug),
roleHint 0..4, no duplicate ids; warns on empty iconPath
(missing-texture render), empty backgroundFile (no panel art),
displayOrder>3 (UI shows at most 4 tabs), and (classMask +
displayOrder) collisions for overlapping classes (two tabs
claiming the same UI slot for the same class).

Wired through the cross-format table; WTLE appears automatically
in all 12 cross-format utilities. Format count 77 -> 78; CLI flag
count 958 -> 963.
2026-05-09 22:27:18 -07:00
Kelsi
9a09831957 feat(editor): add WPSP JSON round-trip (--export/--import-wpsp-json)
Closes the editing loop on the player-spawn-profile catalog: dump
a .wpsp to JSON, hand-edit raceMask / classMask / spawn coords /
bind point / starting items + counts / starting spell ids
(e.g. move Northshire's spawn coords, swap a Hunter's starter
weapon, give every Warrior a Hearthstone, retune a DK's spell
loadout to include Frost Strike), re-import to a byte-identical
binary.

Verified byte-identical round-trip on all three presets
(alliance / horde / dk). CLI flag count 956 -> 958.
2026-05-09 22:24:19 -07:00
Kelsi
082ee495dc feat(editor): add WPSP (Player Spawn Profile) open catalog format
Open replacement for AzerothCore's playercreateinfo SQL table
plus the per-class/race starting fields in CharStartOutfit.dbc.
Defines the initial state for a newly created character: starting
map / zone / position / facing, bind point (Hearthstone
destination), up to 4 starting items with counts, and up to 4
starting spells.

One entry per (race, class) combination — a Human Warrior spawns
at Northshire Abbey with a Worn Shortsword and Heroic Strike
already learned, while an Orc Hunter spawns in Valley of Trials
with Aimed Shot and a starter rifle. Death Knights have their
own preset spawning at lvl 55 in Acherus, the Ebon Hold.

The race+class fields are bitmasks (mirroring WCHC layout) so
one profile entry can cover multiple class/race combinations
that share starting state. findByRaceClass(raceBit, classBit)
is the engine helper used by character creation.

Cross-references back to WCHC (race/class bit layouts), WMS (map
ids), WIT (starting item ids), and WSPL (starting spell ids).

Three preset emitters: --gen-psp (5 Alliance combos covering
each starting zone from Northshire to Ammen Vale), --gen-psp-horde
(5 Horde combos from Valley of Trials to Sunstrider Isle),
--gen-psp-dk (2 DK combos at lvl 55 in Acherus with Death Coil /
Plague Strike / Death Grip starter loadout).

Validation enforces id+name+race+class+startingLevel presence,
no duplicate ids; warns on (0,0,0) spawn (uninitialized entry),
item id/count mismatch (granted item without count or vice versa),
startingLevel > 80 (above WotLK cap), and Death Knight class
with startingLevel < 55 (DKs canonically start at 55).

Wired through the cross-format table; WPSP appears automatically
in all 12 cross-format utilities. Format count 76 -> 77; CLI flag
count 951 -> 956.
2026-05-09 22:22:57 -07:00
Kelsi
f606edc4c9 feat(editor): add --bulk-validate to run every format's validator across a tree
Recursively walks a directory, peeks each file's 4-byte magic to
identify the format, derives the per-format --validate-X flag
from the format table's --info-X entry, and invokes that
validator as a subprocess for each file. Reports total / passed
/ failed / skipped counts and lists the failure paths with their
exit codes; returns exit 1 if any failure is found.

For each failure it prints the exact follow-up command needed to
reproduce the detailed error message, so the user doesn't have
to remember which validator goes with which extension. Asset-
style formats with no validator (.wom / .wob / .whm world
geometry) are counted in the "skipped" bucket but don't fail
the run.

Composes with the existing audit/fix tooling:
  --audit-tree dir          # find header-level breakage
  --magic-fix dir --apply   # auto-fix ext/magic mismatches
  --bulk-validate dir       # run every per-format validator
  # then re-run --validate-X on individual failures for detail

This is the 12th cross-format utility — depth-checking that
catches per-format semantic errors (duplicate ids, invalid
enums, contradictory flag combos, dangling cross-refs) that
--audit-tree's header-only scan can't see. CLI flag count
950 -> 951.
2026-05-09 22:19:09 -07:00
Kelsi
9318b6c006 feat(editor): add WMAT JSON round-trip (--export/--import-wmat-json)
Closes the editing loop on the item-material catalog: dump a
.wmat to JSON, hand-edit materialKind / weightCategory / foley /
impact sound bindings / material flags (e.g. swap a Hide entry
from Light to Medium weight, add IsBreakable to a wooden bow,
re-bind Plate's foley to a different WSND entry, mark a new
HolyForged variant), re-import to a byte-identical binary.

Three different field types each take dual int+name forms:
  - materialKind: int 0..11 OR "cloth"/"leather"/"mail"/"plate"/
    "wood"/"stone"/"metal"/"liquid"/"organic"/"crystal"/
    "ethereal"/"hide"
  - weightCategory: int 0..2 OR "light"/"medium"/"heavy"
  - materialFlags: int bitfield OR pipe-separated label string
    ("IsMagical|IsBreakable|IsHolyCharged"). Importer prefers
    int form when both present so unknown bits round-trip
    losslessly.

Verified byte-identical round-trip on all three presets
(armor / weapon / magical). CLI flag count 948 -> 950.
2026-05-09 22:16:13 -07:00
Kelsi
b220eeba61 feat(editor): add WMAT (Item Material) open catalog format
Open replacement for Blizzard's Material.dbc plus the Material
and SheatheType fields in ItemDisplayInfo.dbc. Defines the
material categorization that items reference (Cloth / Leather /
Mail / Plate / Wood / Steel / Crystal / Ethereal / etc), each
with its own foley sound (played on item use), impact sound
(played on drop / hit), weight category, and material-property
flags (IsBreakable / IsMagical / IsFlammable / IsConductive /
IsHolyCharged / IsCursed).

The engine plays a sword's metallic clang from impactSoundId
when it hits a stone wall, but a cloth tabard makes no such
sound — the difference is exactly the material assigned by this
catalog. Every armor and weapon item in WIT references a
materialId here.

Twelve materialKind values cover the standard armor classes
(Cloth/Leather/Mail/Plate/Hide), structural materials (Wood /
Stone / Metal), and special categories (Liquid / Organic /
Crystal / Ethereal). Three weight tiers (Light / Medium / Heavy)
control encumbrance UI hints.

Cross-references back to WSND (foleySoundId / impactSoundId
reference WSND sound entries) and forward to WIT (item entries
reference materialId here).

Three preset emitters: --gen-mat (5 armor materials matching
WoW's armor classes), --gen-mat-weapon (5 weapon materials from
breakable+flammable Wood through enchanted endgame steel),
--gen-mat-magical (4 magical materials with special flags
including the IsHolyCharged anti-undead property).

Validation enforces id+name presence, materialKind 0..11,
weightCategory 0..2, no duplicate ids; warns on:
  - IsHolyCharged + IsCursed both set (engine picks one,
    typically IsCursed wins)
  - Plate kind that's not Heavy weight (canonical violation)
  - Cloth kind that's not Light weight (canonical violation)

Wired through the cross-format table; WMAT appears automatically
in all 11 cross-format utilities. Format count 75 -> 76; CLI flag
count 943 -> 948.
2026-05-09 22:14:44 -07:00
Kelsi
75c254925f feat(editor): add WCDF JSON round-trip (--export/--import-wcdf-json)
Closes the editing loop on the creature-difficulty routing
catalog: dump a .wcdf to JSON, hand-edit baseCreatureId / 4
variant ids / spawnGroupKind (e.g. swap a Lich King variant from
heroic25Id 39168 to a custom encore template, change a 5-man
boss from Boss to MiniBoss kind, route a new world boss through
the WorldBoss kind), re-import to a byte-identical binary.

The exporter emits both spawnGroupKind (int 0..5) and the human-
readable spawnGroupKindName ("boss" / "mini-boss" / "rare-elite"
/ "trash" / "add" / "world-boss"); the importer accepts either
form.

Verified byte-identical round-trip on all three presets
(starter / wotlk / fiveman). CLI flag count 941 -> 943.
2026-05-09 22:11:16 -07:00
Kelsi
fa8719009b feat(editor): add WCDF (Creature Difficulty) open catalog format
Open replacement for Blizzard's CreatureDifficulty.dbc. Maps a
base creature entry to its difficulty variants:
Normal-10 / Normal-25 / Heroic-10 / Heroic-25 in WotLK raid
format. Each variant is itself a separate WCRT creature entry
with its own stats, abilities, and loot.

When a 25-man party engages an instance, the engine looks up the
encounter base creature's difficultyId, reads the normal25Id
field, and spawns that variant instead. This is how Lord
Marrowgar in 25-Heroic ICC has 30M HP and hits for 80k while the
same encounter in 10-Normal has 5M HP and hits for 25k — same
spawn point, different WCRT entries.

5-man dungeons typically use only normal10Id + heroic10Id (the
25-man fields stay 0 — engine falls through to the 10-man
variant when 25-man is queried). World bosses don't scale at
all (all 4 variant fields stay 0, engine falls back to the base
entry).

Cross-references back to WCRT — every non-zero variant id field
points at a WCRT.creatureId entry; the base creature itself
lives in WCRT too.

Three preset emitters: --gen-cdf (4 example bosses with full
4-variant routing), --gen-cdf-wotlk-raid (4 ICC-style raid
bosses Marrowgar/Deathwhisper/Saurfang/LK with all 4 difficulty
variants), --gen-cdf-fiveman (4 5-man dungeon bosses with only
Normal+Heroic 10-man set). resolveVariant(id, mode) is the
engine helper.

Validation enforces id+name+baseCreatureId presence,
spawnGroupKind 0..5, no duplicate ids; warns on:
  - WorldBoss kind with non-zero variant ids (world bosses don't scale)
  - duplicate baseCreatureId across routes (only first honored)
  - all-self-reference non-WorldBoss (creature doesn't actually scale)
  - Boss with n25 but not n10 (raid sequencing typo — n10 always
    comes with n25)

Wired through the cross-format table; WCDF appears automatically
in all 11 cross-format utilities. Format count 74 -> 75; CLI flag
count 936 -> 941.
2026-05-09 22:10:09 -07:00
Kelsi
ea173d6ff8 feat(editor): add WGFS JSON round-trip (--export/--import-wgfs-json)
Closes the editing loop on the glyph-slot catalog: dump a .wgfs
to JSON, hand-edit slotKind / displayOrder / minLevelToUnlock /
requiredClassMask (e.g. add a fourth Major slot, lower a Minor
slot's unlock from 75 to 70, restrict a Prime slot to Mages
only), re-import to a byte-identical binary.

The exporter emits both slotKind (int 0..2) and the human-
readable slotKindName ("major", "minor", "prime"); the importer
accepts either form.

Verified byte-identical round-trip on all three presets
(starter / wotlk / cata). CLI flag count 934 -> 936.
2026-05-09 22:06:16 -07:00
Kelsi
48c770f5ea feat(editor): add WGFS (Glyph Slot) open catalog format
Open replacement for Blizzard's GlyphSlot.dbc. Defines the
per-class glyph slot layout: which slots a class has (Major /
Minor / Prime), in which display order they appear in the
spellbook UI, and at which character level each slot becomes
available for use.

Distinct from WGLY (GlyphProperties) which defines the individual
glyphs themselves. WGLY says "Glyph of Polymorph exists, costs 1
inscription dust, modifies Polymorph"; WGFS says "the slot that
holds Glyph of Polymorph is the second Major Glyph Slot, unlocks
at level 25, and only Mages have it".

Layout grew across expansions, captured by the three presets:
  - --gen-gfs       — 6 slots: 3 Major + 3 Minor all-class
                       baseline (25/50/75 each)
  - --gen-gfs-wotlk — 6 slots: 3 Major (15/30/50) + 3 Minor
                       (15/50/70) matching WotLK 3.3.5a
  - --gen-gfs-cata  — 9 slots: 3 Prime + 3 Major + 3 Minor
                       matching Cataclysm

Cross-references back to WGLY (glyphs reference slotKind to
constrain which glyph fits which slot) and WCHC (requiredClassMask
uses the same bit layout as WCHC class IDs).

Validation enforces id+name+classMask presence (classMask=0 means
no class can use the slot — usually a config bug), slotKind 0..2,
no duplicate ids; warns on minLevelToUnlock>80 (would never
unlock at WotLK cap), displayOrder>4 (UI typically shows 3-4),
and (kind+order) collisions for overlapping classMask (two slots
claiming the same UI position would render on top of each other).
isUnlockedFor(id, classBit, level) is the engine helper.

Wired through the cross-format table; WGFS appears automatically
in all 11 cross-format utilities. Format count 73 -> 74; CLI flag
count 929 -> 934.
2026-05-09 22:05:05 -07:00
Kelsi
47d8892f74 feat(editor): add WSPC JSON round-trip (--export/--import-wspc-json)
Closes the editing loop on the spell-power-cost bucket catalog:
dump a .wspc to JSON, hand-edit baseCost / perLevelCost /
percentOfBase / powerType / costFlags (e.g. retune LowMana from
5% to 4%, add ScalesWithMastery to a class bucket, switch a
Whirlwind cost from 25 rage to 20), re-import to a byte-identical
binary.

The exporter emits both powerType (int 0..11) and the human-
readable powerTypeName ("mana" / "rage" / "focus" / "energy" /
"happiness" / "runic-power" / "runes" / "soul-shards" /
"holy-power" / "eclipse" / "health" / "no-cost"). costFlags is
emitted as both int bitfield AND pipe-separated label string.
The importer prefers the int form for costFlags when both are
present so unknown flag bits round-trip losslessly.

Verified byte-identical round-trip on all three presets
(starter / rage / mixed). CLI flag count 927 -> 929.
2026-05-09 22:02:08 -07:00
Kelsi
88effe39cd feat(editor): add WSPC (Spell Power Cost) — completes spell-bucket five-pack
Open replacement for the per-spell power-cost fields in Spell.dbc
plus SpellPowerCost-related side tables. Defines categorical
power-cost buckets that spells reference (LowMana 5% / MediumMana
15% / HighMana 30% of caster max mana; fixed Rage-30 /
Energy-40 / Runic-30 / etc), so spells share cost metadata across
ranks instead of embedding per-rank cost numbers.

Completes the small lookup-bucket five-pack:
  WSRG — range bucket
  WSCT — cast time bucket
  WSDR — duration bucket
  WSCD — cooldown bucket
  WSPC — power cost bucket   (this catalog)

Five small integer ids per spell (range / cast / dur / cd / cost)
replace the dozens of duplicate per-rank fields that Blizzard's
Spell.dbc carries. Editing one bucket here retunes every spell
that references it — change LowMana from 5% to 4% and every
rank-1 bolt across every caster class becomes cheaper.

Cost can be flat (baseCost), per-level scaled (perLevelCost), or
percentage-of-max-power (percentOfBase) — the engine sums
whichever fields are non-zero. resolveCost(id, level, maxPower)
does the math. Twelve power types covering every WoW resource
(Mana / Rage / Focus / Energy / Happiness / Runic Power / Runes /
Soul Shards / Holy Power / Eclipse / Health / NoCost).

Three preset emitters: --gen-spc (4 baseline mana tiers),
--gen-spc-rage (4 fixed warrior rage costs including stance-locked
Whirlwind), --gen-spc-mixed (5 cross-class costs covering every
non-mana power type with refund-on-miss flag for energy).

Validation enforces id+name presence, powerType 0..11, no
duplicate ids; warns on percentOfBase outside [0,1] (would
overflow), NoCost type with non-zero cost fields, and non-NoCost
types with no cost set (would cast for free — easy bug to ship).

Wired through the cross-format table; WSPC appears automatically
in all 11 cross-format utilities. Format count 72 -> 73; CLI flag
count 922 -> 927.
2026-05-09 22:00:55 -07:00
Kelsi
74be3f6135 feat(editor): add --magic-fix to auto-rename files to canonical extension
Natural follow-up to --audit-tree: when that utility flags
ext-mismatch or magic-no-ext issues, --magic-fix proposes (and
optionally applies) the renames that resolve them. Walks a
directory recursively, reads each file's 4-byte magic, looks it
up in the format table, and renames to the canonical extension
when the current extension doesn't match (or is absent).

Defaults to dry-run for safety — prints the proposed renames so
they can be reviewed first; pass --apply to commit them. Refuses
to clobber existing files: when the target path already exists
(e.g. foo.wsct + foo.wsrg both with WSRG magic), the rename is
flagged as a collision and skipped, leaving both files in place
for manual resolution. Returns exit 1 if any proposals exist (in
dry-run) or any collisions are skipped (in apply), so it composes
into shell pipelines.

JSON sidecar via --json. Suggested workflow:
  --audit-tree dir          # find what's broken
  --magic-fix dir           # preview the auto-fixes
  --magic-fix dir --apply   # commit them
  --audit-tree dir          # confirm clean

CLI flag count 921 -> 922.
2026-05-09 21:57:26 -07:00
Kelsi
3e73860475 feat(editor): add WCEF JSON round-trip (--export/--import-wcef-json)
Closes the editing loop on the creature-family catalog: dump a
.wcef to JSON, hand-edit familyKind / petTalentTree /
minLevelForTame / petFoodTypes (e.g. add Bread to Bear's diet,
move Boar from Tenacity to Cunning, drop the tame requirement on
exotic Worm from 50 to 45), re-import to a byte-identical binary.

Three different field types each take dual int+name forms:
  - familyKind: int 0..5 OR "beast"/"demon"/"undead"/"elemental"/
    "not-pet"/"exotic"
  - petTalentTree: int 0..3 OR "none"/"ferocity"/"tenacity"/
    "cunning"
  - petFoodTypes: int bitfield OR pipe-separated label string
    ("Meat|Fish|Raw"). Importer prefers the int form when both
    are present so unknown bits round-trip losslessly.

Verified byte-identical round-trip on all three presets
(starter / ferocity / exotic). CLI flag count 919 -> 921.
2026-05-09 21:55:23 -07:00
Kelsi
12faffeb87 feat(editor): add WCEF (Creature Family) open catalog format
Open replacement for CreatureFamily.dbc plus the per-creature
family fields in Creature.dbc. Defines the family categorization
that pet-able beasts share (Bear / Cat / Wolf / Boar / Crab /
Raptor / Devilsaur / etc), each with its own pet talent tree
(Ferocity / Tenacity / Cunning), food preferences as a bitmask
(Meat / Fish / Bread / Cheese / Fruit / Fungus / Raw), the skill
line that family-specific abilities reference, and the minimum
hunter level required to tame it.

Used by the hunter pet system to decide which talent tree a tamed
pet uses, validate that a hunter can tame a creature, match
feeding-table food items to pet preferences, and gate exotic-beast
families behind the Beast Master 51-point talent.

Cross-references back to WCRT (creature.familyId points here) and
WSPL (family-specific abilities reference WSPL spellId via the
skillLine field).

Three preset emitters: --gen-cef (5 baseline families covering
both major talent trees), --gen-cef-ferocity (4 DPS-tree pets
with bleed/howl/armor-shred mechanics), --gen-cef-exotic (4
exotic Beast Master families requiring 51-point talent).

Validation enforces id+name presence, familyKind 0..5, talent
tree 0..3, no duplicate ids, and warns on:
  - NotPet families with a non-None talent tree (irrelevant)
  - Exotic families with minLevelForTame > 80 (level-cap unreachable)
  - Beast/Exotic families with no food types set (pet would starve)

Wired through the cross-format table; WCEF appears automatically
in all 10 cross-format utilities. Format count 71 -> 72; CLI flag
count 914 -> 919.
2026-05-09 21:54:00 -07:00
Kelsi
7ecc829c4d feat(editor): add WSCD JSON round-trip (--export/--import-wscd-json)
Closes the editing loop on the spell-cooldown bucket catalog: dump
a .wscd to JSON, hand-edit cooldownMs / bucketKind / category
flags (e.g. retune the global cooldown from 1.5s to 1.0s, mark a
bucket as IgnoresCooldownReduction, add Polymorph variants to the
class family bucket), re-import to a byte-identical binary.

The exporter emits both bucketKind (int 0..4) and the human-
readable bucketKindName ("spell", "item", "class", "global",
"misc"); same dual form for categoryFlags (int bitfield AND
"AffectedByHaste|OnGCDStart" pipe-separated label string). The
importer prefers the int form for categoryFlags when both are
present so unknown flag bits round-trip losslessly, falling back
to the label form for hand-edited sidecars.

Verified byte-identical round-trip on all three presets
(starter / class / items). CLI flag count 912 -> 914.
2026-05-09 21:50:39 -07:00
Kelsi
493db026dd feat(editor): add WSCD (Spell Cooldown Category) open catalog format
Open replacement for SpellCooldown.dbc plus the per-spell
category-cooldown fields in Spell.dbc. Defines the shared-cooldown
buckets that related spells reference: casting one spell triggers
a cooldown on every other spell in the same bucket. Mage Polymorph
variants (Sheep / Pig / Turtle / Cat) all share one bucket so
morphing a target locks all variants at once. Healing potions and
mana potions share the SharedWithItems bucket so consuming one
locks the other.

Distinct from WSDR (which times how long an aura stays on a
target) — WSCD times how long before a spell can be cast again.
The global cooldown (GCD) is itself just one bucket of this kind,
flagged with OnGCDStart so the engine triggers it at cast start
rather than cast finish.

Three preset emitters: --gen-cdb (4 baseline buckets including
GCD), --gen-cdb-class (5 mage-specific class cooldowns including
the Polymorph family), --gen-cdb-items (5 item cooldowns
including the heal/mana potion shared bucket and the 60min
Hearthstone family). Validation enforces id+name presence,
bucketKind 0..4, no duplicate ids, and warns on Global without
OnGCDStart (engine wouldn't trigger on cast start) and Spell
kind with SharedWithItems (contradictory).

categoryFlags is a bitfield (AffectedByHaste / SharedWithItems /
OnGCDStart / IgnoresCooldownReduction); --info-wscd decodes the
bits to label list. Wired through the cross-format table; WSCD
appears automatically in all 9 cross-format utilities. Format
count 70 -> 71; CLI flag count 907 -> 912.
2026-05-09 21:49:13 -07:00
Kelsi
824b6ebf53 feat(editor): add --audit-tree to flag corrupted/misnamed Wowee files
Walks a directory recursively and groups problems by category:
too-small (file under 16 bytes — can't hold a header),
unknown-magic (.w* file whose magic is not in the format table),
ext-mismatch (extension says one format but the magic says
another — usually from a renamed file), magic-no-ext (file with
recognized Wowee magic but no .w* extension), and header-trunc
(magic matches but the rest of the header is truncated). Returns
exit 1 if any issue is found, so it composes into shell pipelines
and CI checks. JSON sidecar via --json.

Catches the kinds of breakage that --summary-dir silently rolls
into the "unrecognized" bucket — a renamed .wsrg file masquerading
as .wsct shows up cleanly here but would otherwise be invisible.
Like every cross-format utility this reuses cli_format_table.cpp,
so new formats are audited automatically. CLI flag count
906 -> 907.
2026-05-09 21:45:54 -07:00
Kelsi
b8dc28d704 feat(editor): add WSDR JSON round-trip (--export/--import-wsdr-json)
Closes the editing loop on the spell-duration bucket catalog: dump
a .wsdr to JSON, hand-edit baseDurationMs / perLevelMs / clamp /
durationKind (e.g. retune Renew from 9s to 10s base across every
spell that references the HoT5Tick bucket), re-import to a
byte-identical binary. The exporter emits both durationKind (int
0..4) and the human-readable durationKindName ("instant", "timed",
"tick", "until-cancelled", "until-death"); the importer accepts
either, so JSON sidecars stay readable without losing the
canonical binary encoding.

Verified byte-identical round-trip on all three presets
(starter / buffs / dot). CLI flag count 904 -> 906.
2026-05-09 21:43:16 -07:00
Kelsi
98f899cf7c feat(editor): add WSDR (Spell Duration Index) — completes WSRG/WSCT/WSDR triplet
Open replacement for SpellDuration.dbc plus per-spell duration
fields in Spell.dbc. Defines the categorical duration buckets
that auras / DoTs / HoTs / buffs reference (5s / 30s / 5min / 1hr
/ UntilCancelled / UntilDeath).

Together with WSRG (range) and WSCT (cast time), this completes a
small triplet of spell-metadata catalogs: instead of every
Frostbolt rank embedding its own range, cast time, and
chill-debuff duration as duplicate fields, each spell holds three
small integer ids that resolve through these three tables. The
engine retunes thousands of spells at once by editing one bucket.

Duration scales with caster level via perLevelMs (a rank-1 Renew
at 9s grows to 12s at lvl 60), then is clamped to maxDurationMs.
Negative baseDurationMs is the canonical sentinel for "no timer"
(UntilCancelled / UntilDeath); resolveAtLevel returns -1 for
those so HUD code can render the indefinite-duration glyph.

Three preset emitters: --gen-sdr (5 baseline tiers from instant
to one-hour), --gen-sdr-buffs (4 long-duration buffs including
UntilDeath), --gen-sdr-dot (4 tick-based DoT/HoT buckets at 3s
ticks). Validation enforces base>0 for Timed/TickBased, base<0
for permanent kinds, max>=base, durationKind 0..4, no duplicate
ids, and warns on Instant+nonzero base.

Wired through the cross-format table; WSDR appears automatically
in all 9 cross-format utilities. Format count 69 -> 70; CLI flag
count 899 -> 904.
2026-05-09 21:41:55 -07:00
Kelsi
479e96a68a feat(editor): add WSCT JSON round-trip (--export/--import-wsct-json)
Closes the editing loop on the spell-cast-time bucket catalog:
dump a .wsct to JSON, hand-edit baseCastMs / perLevelMs / clamp
bounds (retune a Pyroblast bucket from 3000ms to 2800ms across
every spell that references it), re-import to a byte-identical
binary. The exporter emits both castKind (int 0..4) and the
human-readable castKindName ("instant", "cast", "channel",
"delayed", "charge"); the importer accepts either.

Verified byte-identical round-trip on all three presets
(starter / channel / ramp). CLI flag count 897 -> 899.
2026-05-09 21:38:56 -07:00
Kelsi
8dcbd08a16 feat(editor): add WSCT (Spell Cast Time Index) open catalog format
Companion to WSRG: open replacement for SpellCastTimes.dbc plus
the per-spell castTime fields in Spell.dbc. Defines categorical
cast-time buckets (Instant 0ms / FastCast 1s / MediumCast 1.5s /
LongCast 3s) that thousands of spells reference instead of each
embedding their own ms count. Together WSRG and WSCT let the
spell engine resolve "Frostbolt's range bucket = id 3" and
"Frostbolt's cast time bucket = id 5" with two table reads
instead of duplicating per-rank data.

Cast time can scale with character level via perLevelMs (a rank-1
spell at 1000ms can grow to 2200ms at lvl 60), then the bucket
result is clamped to [minCastMs, maxCastMs] before haste is
applied. resolveAtLevel() does the math for engine consumers.

Three preset emitters: --gen-sct (4 baseline buckets),
--gen-sct-channel (3 channeled-spell durations), --gen-sct-ramp
(4 level-scaled buckets with non-zero perLevelMs). Validation
catches negative baseCastMs, min>max, duplicate ids, warns on
Instant kind with non-zero base (cast bar would still show), and
errors on Channel kind with zero base (would tick once and end).

Wired through the cross-format table; WSCT appears automatically
in all 9 cross-format utilities. Format count 68 -> 69; CLI flag
count 892 -> 897.
2026-05-09 21:37:42 -07:00