Commit graph

375 commits

Author SHA1 Message Date
Kelsi
abf264abfe feat(editor): add WBAB (Buff & Aura Book) — 102nd open format
Novel replacement for the implicit rank-chain
relationships that vanilla WoW encoded by burying
nextRank/prevRank pointers inside Spell.dbc with no
explicit graph structure. Each WBAB entry is one long-
duration class buff at one specific rank, with explicit
edges to adjacent ranks via previousRankId and
nextRankId fields. The graph-shaped data is novel among
the 100+ catalog set: most catalogs have flat rows; WBAB
is genuinely a graph where rows are nodes and the rank
fields are edges.

Both directions are stored explicitly so the spellbook
UI's "upgrade to next rank" button can traverse without
scanning the full table. Helper methods walkChainBack-
ToRoot() returns the full chain root->tip for the rank-
picker widget; findChainTip() returns the highest rank
for auto-cast logic.

Three preset emitters demonstrating the pattern:
makeMage (Arcane Intellect ranks 1-4 with chain edges),
makeDruid (Mark of the Wild ranks 1-5 with chain edges),
makeRaidMax (6 max-rank standalone raid buffs — one per
buffing class — with no chain edges to show the
standalone case).

Validator catches several rank-chain-specific bugs:
self-referencing edges (entry.next == entry.id would
create a 1-element cycle), missing referenced entries
(next/prev pointing to non-existent ids), and most
importantly back-edge symmetry — if A.nextRankId=B then
B.previousRankId MUST equal A.buffId or the spellbook
upgrade traversal will derail. Symmetric back-edge check
is unique to graph-shaped catalogs.

Also fixed a crash in --catalog-find where the recursive
directory iterator threw on permission-denied subdirs
(common when walking /tmp). Now uses the
skip_permission_denied directory_options + per-step
error_code clearing for defensive resumption.

Format count 101 -> 102. CLI flag count 1134 -> 1139.
2026-05-10 01:13:42 -07:00
Kelsi
471ddfef07 feat(editor): add --catalog-find directory-wide id search
New utility complements --catalog-pluck (single-file id
lookup) by walking a directory tree recursively and
searching every catalog for entries whose primary key
matches the supplied id. Reports each hit as
[WXXX] file:fieldName=id "name" so the operator can
locate where any given id lives across a 100+ format
project. Useful when chasing cross-references like "id
631 is referenced by WGRP.mapId — where is it actually
defined?"

Optional --magic <WXXX> filter narrows the search to one
format family. Necessary because primary-key id ranges
overlap across formats (id=200 might be both a WCMG
group and a WGRP composition); without the filter the
operator gets all collisions, which is itself useful
for spotting unintentional id reuse.

Auto-detects per-file format magic, skips files with
unknown magic and files whose format has no --info-*
surface (asset formats like .wom that aren't catalog-
shaped). Re-uses the same primary-key auto-discovery
+ external-ref filter as --catalog-pluck. Both
utilities should grow into a shared helper header once
a third utility needs the same lookup logic — for now
a noted duplication.

CLI flag count 1133 -> 1134.
2026-05-10 01:09:09 -07:00
Kelsi
dc0c71fdd7 feat(editor): add WEMO JSON round-trip (--export/--import-wemo-json)
Dual encoding for all 3 WEMO enums on import: emoteKind
(int OR "social"/"combat"/"roleplay"/"system"), sex
("both"/"male"/"female"), ttsHint ("talk"/"whisper"/"yell"/
"silent"). Reuses the readEnumField template pattern from
WMSP so the 3 enums share one parser body.

Also escapes a stray %s in the validate-wemo help text
that gcc -Wformat had been flagging as a missing-argument
warning. The literal "%s" appears inside the warning
description string so it must be encoded as %%s for
printf to render the helptext glyph correctly.

All 3 presets (basic/combat/rp) byte-identical roundtrip
OK. Token-form import smoke-tested with mixed roleplay +
female + whisper. CLI flag count 1131 -> 1133.
2026-05-10 01:06:59 -07:00
Kelsi
c9b822002f feat(editor): add WEMO (Emote Definition) — 101st open format
Novel replacement for the EmotesText.dbc + EmotesTextSound
+ EmotesTextData trio that maps /slash-emote commands
(/dance, /wave, /laugh, etc.) to their visible chat text,
animation ID, and per-race voice clip. Each entry binds
one slashCommand to an animationId (refs WANI), soundId
(refs WSND), targetMessage / noTargetMessage formats,
emote kind (Social / Combat / RolePlay / System), sex
filter (Both / Male / Female), required race bit, and a
TTS hint (Talk / Whisper / Yell / Silent) for accessibility
text-to-speech engines.

Three preset emitters covering the canonical emote
buckets: makeBasic (8 universal social emotes — wave /
bow / laugh / cheer / cry / sleep / kneel / applaud),
makeCombat (5 combat-themed — roar / threaten / charge /
victory / surrender), makeRolePlay (6 RP-focused — bonk
/ ponder / soothe / plead / shoo / scoff). Animation IDs
match AnimationData.dbc convention so existing WoW client
mods continue to play the right anims.

Validator catches authoring bugs unique to slash-command
parsing: leading '/' on slashCommand (chat parser strips
it before lookup so the entry would be doubly-prefixed),
uppercase letters (parser case-folds before lookup so the
entry is unreachable), duplicate slash commands (parser
dispatches by exact match — ambiguity would crash the
chat input handler), %s token counts that don't match
target/no-target distinction.

Also expanded --catalog-pluck's foreign-key filter to
include animationId / soundId / particleId / ribbonId /
vehicleId / seatId / currencyId / trainerId / vendorId /
mailTemplateId — caught during smoke-test where pluck
mis-identified WEMO entries by animationId instead of
emoteId. Same class of bug as the WHRT areaId fix.

Format count 100 -> 101. CLI flag count 1126 -> 1131.
2026-05-10 00:53:33 -07:00
Kelsi
4aa7b56e13 feat(editor): add WMSP JSON round-trip (--export/--import-wmsp-json)
Dual encoding for all 4 WMSP enums (realmType /
realmCategory / expansion / population) on import — each
accepts int OR token string. New readEnumField helper
template factors out the int-or-token coercion logic so
the 4 enums share one parser body instead of repeating
the same 12-line pattern four times. Helper reports the
field label + entry id on parse failure so the operator
knows exactly which token broke.

Export adds a derived versionString "x.y.z" convenience
field alongside the three uint8 components — purely
informational, ignored on import (the int components are
authoritative). gmOnly accepts bool OR int.

All 3 presets (single/cluster/multi-expansion) byte-
identical roundtrip OK. Token-form import smoke-tested
with rppvp + beta + tbc + high (all 4 enums as strings
in the same entry). CLI flag count 1124 -> 1126.
2026-05-10 00:48:27 -07:00
Kelsi
054f44e4aa feat(editor): add WMSP (Master Server Profile) — 100th open format
Novel replacement for the hardcoded realmlist that the
WoW client receives via SMSG_REALM_LIST during login.
Each entry is one selectable realm: name, network address
(host:port), realm type (Normal/PvP/RP/RPPvP/Test), realm
category (Public/Private/Beta/Dev), expansion gating
(Vanilla 1.12.1 / TBC 2.4.3 / WotLK 3.3.5a / Cata 4.3.4),
population indicator (Low/Medium/High/Full/Locked), char-
acter cap, GM-only flag, timezone hint, and per-realm
version+build numbers.

100th open format — milestone marker for the catalog
ecosystem. WMSP is a TOP-LEVEL bootstrap catalog (read by
the login server before any character is loaded), so it
deliberately has no cross-references to other catalogs;
all other social/world/spell catalogs depend on a player
session that doesn't exist until WMSP has been consulted.

Three preset emitters covering common deployment shapes:
makeSingleRealm (1 default WoweeMain WotLK Public),
makePvPCluster (3 realms — PvE/PvP/RP — sharing one login
address so players pick rule-set without changing servers),
makeMultiExpansion (4 progression realms across all
expansion gates with their canonical build numbers from
the matching client).

Validator catches several real misconfigurations: empty
address (login server cannot route session), realmType
out of {0,1,4,6,8} (the WoW client's RealmType enum is
non-contiguous — 2/3/5/7 are unused values that crash the
picker), characterCap=0 (players can't make characters),
duplicate realm names (picker requires unique display
names), missing port in address.

Format count 99 -> 100. CLI flag count 1119 -> 1124.
2026-05-10 00:47:02 -07:00
Kelsi
2f76653cf7 feat(editor): add WCMG JSON round-trip (--export/--import-wcmg-json)
Dual encoding for categoryKind (int 0..5 OR
"stance"/"form"/"aspect"/"presence"/"posture"/"sigil");
exclusive accepts bool or int. members[] serializes as a
plain JSON array of spell IDs — directly editable for
adding or removing spells from a mutex group without
touching the binary.

All 3 presets (warrior/druid/all) byte-identical
roundtrip OK. Token-form import smoke-tested with mixed
preset values. CLI flag count 1117 -> 1119.
2026-05-10 00:43:28 -07:00
Kelsi
d62ac954da feat(editor): add WCMG (Combat Maneuver Group) open catalog format
Novel replacement for the hardcoded class-mutex tables
the WoW client uses to grey out incompatible action-bar
buttons. Each entry is one mutually-exclusive spell
group: Warrior stances (Battle/Defensive/Berserker),
Druid shapeshift forms (Bear/Cat/Travel/Tree/Moonkin),
Hunter aspects (Hawk/Cheetah/Pack/Viper/Dragonhawk/Beast/
Wild), DK presences (Frost/Unholy/Blood). The action-bar
update path uses the catalog to know which spells share
a mutex bucket and clear "currently active" outlines
when a sibling is cast.

Six categoryKind enum values (Stance / Form / Aspect /
Presence / Posture / Sigil) and variable-length members[]
array of spell IDs (refs WSPL). Three presets:
makeWarrior (Warrior 3-stance), makeDruid (5 shapeshift
+ 2 flight, separate buckets so flying isn't broken by
Cat Form), makeAllMutex (cross-class catalog with one
representative group per mutex-having class).

Validator catches several authoring bugs: empty members[]
(group has nothing to switch between), spellId 0,
duplicate spellId within a group, and — most usefully —
the same spellId appearing in two different exclusive
groups (which would make the action-bar mutex
undecidable: which group's outline does the bar use?).
Warns on single-member groups (mutex with one element
has no exclusion to enforce).

Format count 98 -> 99 (one short of triple-digit
milestone). CLI flag count 1112 -> 1117.
2026-05-10 00:41:45 -07:00
Kelsi
16454c57c4 feat(editor): add --catalog-pluck cross-format entry lookup
New utility extracts a single entry by id from any
registered catalog format without dumping the whole file.
Useful when a catalog has hundreds of entries and you
only want to inspect one — e.g. "show me WBOS encounter
102" or "what's in WHRT bind 204".

Auto-detects format from the file's 4-byte magic, looks
up the registered --info-* flag in the format table,
spawns that handler as a subprocess with --json, then
filters the entries[] array to just the matching id. The
primary-key field is auto-discovered: prefers the first
*Id field that ISN'T a known foreign-key reference (mapId,
areaId, spellId, npcId, factionId, etc. — 25 known
external-ref names filtered out). Falls back to first
remaining *Id, then first numeric field.

Without the foreign-key filter, alphabetical key
iteration in nlohmann::json picks the wrong field — for
WHRT entries with both areaId and bindId, naive code
would identify by areaId and miss obvious lookups.
Caught during smoke-test and fixed before commit.

Output formats: terminal table (default) or --json.
Accepts file path with or without the .wXXX extension.
CLI flag count 1111 -> 1112.
2026-05-10 00:37:53 -07:00
Kelsi
8c0cab27be feat(editor): add WSCB JSON round-trip (--export/--import-wscb-json)
Dual encoding for both channelKind (int 0..4 OR
"login"/"system"/"raidwarning"/"motd"/"helptip") and
factionFilter (int 1..3 OR "alliance"/"horde"/"both") on
import. Export emits both int and *Name string fields
for round-trip safety; the int form is authoritative,
the *Name string is for human edit ergonomics and is
ignored if the int form is also present.

All 3 presets (motd/maintenance/helptips) byte-identical
roundtrip OK. Token-form import smoke-tested with mixed
channel+faction tokens. CLI flag count 1109 -> 1111.
2026-05-10 00:33:38 -07:00
Kelsi
57df129404 feat(editor): add WSCB (Server Channel Broadcast) open catalog format
Novel replacement for the hardcoded login-MOTD chain,
restart-warning announcements, and rotating /help-channel
tips. Each entry is one scheduled or event-triggered
broadcast with channelKind (Login / SystemChannel /
RaidWarning / MOTD / HelpTip), faction filter,
level-range gating, and optional periodic interval for
ticker-driven channels.

Three preset emitters covering the canonical operational
broadcast patterns: makeMotd (4 login MOTDs — welcome
banner, patch summary, Discord, forum), makeMaintenance
(3 RaidWarning entries firing at 15min/5min/60s before
restart, intervalSeconds=0 since they're triggered by
the cron scheduler, not a self-timer), makeHelpTips (6
rotating /help-channel tips on a 600s cycle covering
talents/mounts/auction/professions/dungeon-finder/
hearthstone with appropriate level gates).

Validator catches several real misconfigurations: empty
messageText (no payload), interval>0 with login/MOTD
channel (timer ignored — those fire on session enter),
intervalSeconds<10 (player-spam error), <60 (warning),
text>255 chars (server truncation), level-range
inversions, factionFilter=0 (no audience).

Format count 97 -> 98. CLI flag count 1104 -> 1109.
2026-05-10 00:31:15 -07:00
Kelsi
2d99f82531 feat(editor): add WHRT JSON round-trip (--export/--import-whrt-json)
Dual encoding for both bindKind (int 0..5 OR
"inn"/"capital"/"quest"/"guild"/"specialport"/"faction")
and factionMask (int 1..3 OR "alliance"/"horde"/"both")
on import. Export emits both int and *Name string fields
for round-trip safety and human readability — the int
form is authoritative; the *Name string can be read for
edits but is ignored if the int form is also present.

All 3 presets (starter-cities/capitals/inns) byte-
identical roundtrip OK. Token-form import smoke-tested
with mixed faction+kind tokens. CLI flag count 1102 ->
1104.
2026-05-10 00:27:05 -07:00
Kelsi
c50d3cbae5 feat(editor): add WHRT (Hearth Bind Point) open catalog format
Novel replacement for the hardcoded SMSG_BINDPOINTUPDATE
bind list. Each entry is one valid hearthstone bind
location: a tavern innkeeper, a capital-hall bind clerk,
a quest-given bind reward (Theramore, Wyrmrest), a guild-
hall bind clerk, or a special raid port (Karazhan,
Sunwell). Cross-references WMS for mapId/areaId, WCRT
for the innkeeper NPC, and WCHC for faction-mask bits.

Six bindKind enum values (Inn / Capital / Quest / Guild /
SpecialPort / Faction) and a 3-value factionMask
(AllianceOnly / HordeOnly / Both). Three preset emitters:
makeStarterCities (4 city innkeepers), makeCapitals (6
capital-hall bind clerks), makeStarterInns (8 starter-zone
inns spanning all races).

Validator checks id+name required, factionMask 1..3,
bindKind 0..5, no duplicate ids; warns on (0,0,0)
position (likely forgotten SetPosition; bind would
teleport player to world origin), Inn-kind with no
innkeeper NPC, Quest-kind with no level gate.

Format count 96 -> 97. CLI flag count 1097 -> 1102.
2026-05-10 00:25:55 -07:00
Kelsi
31b9b55ebd feat(editor): add WGRP JSON round-trip (--export/--import-wgrp-json)
JSON sidecar with sizeCategory derived string ("5man" /
"10man" / "25man" / "40man" / "custom") added on export
for human readability. sizeCategory is purely
informational — the round-trip is driven by maxPartySize
itself; the import handler ignores sizeCategory so users
can edit maxPartySize freely without keeping the two in
sync. requireSpec accepts both bool and int on import for
JSON-edit ergonomics.

All 3 presets (5-man/raid10/raid25) byte-identical
roundtrip OK. CLI flag count 1095 -> 1097.
2026-05-10 00:22:14 -07:00
Kelsi
869880fd66 feat(editor): add WGRP (Group Composition) open catalog format
Novel replacement for the hardcoded LFG / Dungeon Finder
group-composition rules. Defines per-instance role
quotas (tanks / healers / dps), party-size bounds, and
spec-gating. Cross-references WMS for mapId, WCDF for
difficulty.

Three preset emitters covering the canonical raid sizes:
makeFiveMan (Classic 1T/1H/3D, Heavy-Heal trash 1T/2H/2D,
Roleless 5D speedrun), makeRaid10 (Standard 2T/3H/5D,
HealingHeavy 2T/4H/4D, MeleeStack 1T/2H/7D for cleave
fights), makeRaid25 (Standard 2T/6H/17D, HealingHeavy
1T/8H/16D, ZergDPS 0T/4H/21D for tank-immune fights).

Validator rejects role-sums that exceed maxPartySize
(unfulfillable comp), enforces min<=max, no duplicate
ids; warns on non-standard sizes (5/10/25/40 only) and
zero-tank comps so authors confirm intent. Caught one
real bug during smoke-test where a 25-player Wintergrasp
preset was mis-bound to a 10-man maxPartySize.

Format count 95 -> 96. CLI flag count 1090 -> 1095.
2026-05-10 00:20:44 -07:00
Kelsi
54353e03e6 feat(editor): add WACT JSON round-trip (--export/--import-wact-json)
Closes the editing loop on the action-bar-layout catalog: dump
a .wact to JSON, hand-edit barMode / classMask / spellId /
itemId / buttonSlot (e.g. swap a Warrior's button 1 from Heroic
Strike to Mortal Strike, move Polymorph from slot 3 to slot 8,
add a Hearthstone item-macro to slot 12, share Mark of the Wild
across Druid+Priest classMask), re-import to a byte-identical
binary.

barMode dual-encoded: int 0..6 OR human-readable name ("main" /
"pet" / "vehicle" / "stance1" / "stance2" / "stance3" /
"custom"). All other fields are scalar so no special handling
needed.

Verified byte-identical round-trip on all three presets
(warrior / mage / pet). CLI flag count 1088 -> 1090.
2026-05-10 00:14:21 -07:00
Kelsi
48dbf72f11 feat(editor): add WACT (Action Bar Layout) open catalog format
Open replacement for the hardcoded per-class default action bar
bindings. Defines which abilities auto-populate which action
button slots when a new character is created or a class is
reset. A Warrior's button 1 binds Heroic Strike, button 2
Charge, button 3 Rend, etc. — new characters of that class get
those buttons pre-populated so the action bar isn't empty on
first login.

Distinct from WKBD (Keybindings) which maps physical keys to
action button slots — WACT maps action button slots to
abilities. The two together complete the default-control
configuration: Key 1 -> Action Slot 1 (WKBD) -> Heroic Strike
(WACT).

Seven barMode values cover the major action bar contexts:
  - Main (slots 0-11, standard 12-button bar)
  - Pet (hunter/warlock pet action bar)
  - Vehicle (mounted/vehicle action bar)
  - Stance1/2/3 (warrior battle/defensive/berserker; druid
    bear/cat/tree)
  - Custom (server-custom bar overlay)

Cross-references back to WCHC (classMask layout), WSPL (spellId
for the bound ability), and WIT (itemId for item-macro bindings
like Hearthstone in slot 12). findByClass(classBit, barMode)
returns the bindings sorted by buttonSlot — used directly by
character creation to populate action bars.

Three preset emitters: --gen-act (10 Warrior starter bindings on
Main bar with canonical 3.3.5a abilities), --gen-act-mage (10
Mage starter bindings including Counterspell + Polymorph),
--gen-act-pet (10 Hunter pet-bar bindings using barMode=Pet for
Attack/Stance/Bite/Claw/Dismiss).

Validation enforces id+name+classMask presence, barMode 0..6,
no duplicate ids; warns on:
  - buttonSlot > 143 (max is 12 bars × 12 slots = 144)
  - both spellId and itemId set (engine prefers spellId, item
    is silently ignored)
  - both spellId=0 AND itemId=0 (button will render empty)
  - (classMask + barMode + buttonSlot) collisions for
    overlapping classes — multiple bindings fighting for the
    same physical slot

Wired through the cross-format table; WACT appears in all 18
cross-format utilities. Format count 94 -> 95; CLI flag count
1083 -> 1088.
2026-05-10 00:11:53 -07:00
Kelsi
48ca202716 feat(editor): add WSTM JSON round-trip (--export/--import-wstm-json)
Closes the editing loop on the stat-modifier-curve catalog: dump
a .wstm to JSON, hand-edit baseValue / perLevelDelta /
multiplier / level range / curveKind (e.g. retune SpellCrit base
from 1%% to 1.5%% for a higher-magic server, halve the
ManaPerSpirit slope by setting multiplier=0.5, restrict an armor
curve to lvl 60+ only by raising minLevel), re-import to a
byte-identical binary.

curveKind dual-encoded: int 0..6 OR human-readable name ("crit"
/ "hit" / "power" / "regen" / "resist" / "mitigation" / "misc").
Float fields (baseValue, perLevelDelta, multiplier) round-trip
through JSON's native number type — IEEE 754 single precision
preserves exactly.

Verified byte-identical round-trip on all three presets
(crit / regen / armor). CLI flag count 1081 -> 1083.
2026-05-10 00:07:23 -07:00
Kelsi
9a85cc029e feat(editor): add WSTM (Stat Modifier Curve) open catalog format
Open replacement for the gtChanceTo*.dbc / gtRegen*.dbc /
gtCombatRatings.dbc family of "1D level-keyed curve" tables.
Each entry defines a single linear curve mapping character level
to a stat value: melee crit chance per level, mana regen per
spirit per level, base armor per level, etc.

Curves are linear: value(level) = baseValue + perLevelDelta *
(level - 1), with the result optionally scaled by a global
multiplier and clamped to a level range. Most stock WoW curves
fit this shape — the few that don't (cubic Combat Ratings) live
in the dedicated WCRR catalog with spline support.

Distinct from WCRR (Combat Rating conversion, integer ratings ->
percentages) and WSPC (Spell Power Cost buckets, per-spell
costs). WSTM is for the generic engine-side stat curves that
aren't per-spell or per-rating.

Seven curveKind values classify the major stat families (Crit /
Hit / Power / Regen / Resist / Mitigation / Misc), and each
curve carries its own [minLevel, maxLevel] applicability range
plus a multiplier for global scaling without retuning each
curve's slope.

Three preset emitters: --gen-stm (5 crit-related curves with
canonical 3.3.5a base+per-level scaling — MeleeCrit 5%+0.05/lvl
resolves to 8.95% at lvl 80), --gen-stm-regen (4 regen curves
including ManaPerSpirit and the Vanilla-era 3 rage/sec OOC
decay), --gen-stm-armor (3 armor/mitigation/resistance curves).

The info renderer demos resolveAtLevel(curveId, 80) inline as
the @lvl80 column — server admins can sanity-check what each
curve resolves to at character cap without writing test code.

Validation enforces id+name presence, curveKind 0..6,
minLevel<=maxLevel, no duplicate ids; warns on:
  - maxLevel > 80 (unreachable at WotLK cap)
  - multiplier=0 (curve always evaluates to 0)
  - multiplier<0 (inverts the curve — possibly intentional)
  - perLevelDelta<0 (curve shrinks with level — unusual)

Wired through the cross-format table; WSTM appears in all 18
cross-format utilities. Format count 93 -> 94; CLI flag count
1076 -> 1081.
2026-05-10 00:05:07 -07:00
Kelsi
68c20bd152 feat(editor): add WSTC JSON round-trip (--export/--import-wstc-json)
Closes the editing loop on the hunter-stable-slot catalog: dump
a .wstc to JSON, hand-edit displayOrder / minLevelToUnlock /
copperCost / isPremium (e.g. lower the lvl 50 stable slot 4
unlock to lvl 45, mark slot 5 as premium for a donor server,
raise slot 3 cost from 2g to 5g for a tighter economy),
re-import to a byte-identical binary.

isPremium dual-encoded: bool OR int (machine-generated sidecars
work too). All other fields are scalar so no special handling
needed.

Verified byte-identical round-trip on all three presets
(std / cata / premium). CLI flag count 1074 -> 1076.
2026-05-10 00:00:43 -07:00
Kelsi
8f6f6ac91e feat(editor): add WSTC (Hunter Stable Slot) open catalog format
Open replacement for the hardcoded hunter pet stable slot
progression. Defines each stable slot's display order in the
stable UI, the character level at which the slot becomes
available, the gold cost to unlock, and whether it's a premium
/ donator-only slot.

In WoW 3.3.5a hunters get 5 stable slots total: the active pet
plus 4 stabled (slots 1-4 unlocking at hunter levels 10/20/30/40
with escalating gold costs 0/10s/50s/2g/10g). Cataclysm raised
the cap to 5 stabled slots, and server-custom expansions go
higher with donator-only "premium" slots that bypass the level
gate. This catalog parameterizes the entire progression instead
of editing engine source.

Consumed directly by the StableMaster service in WBKD entries.
unlockedSlotCount(characterLevel) is the engine helper used by
the stable master frame to decide how many slot tabs to render.

Three preset emitters: --gen-stc (5 canonical slots matching
WoW 3.3.5a), --gen-stc-cata (6 Cata-style slots with slot 5
unlocking at lvl 60 for 25g), --gen-stc-premium (4 server-custom
donator slots with no level/gold gate).

The info renderer pretty-prints copperCost as "free" / "10s 0c" /
"2g 0s 0c" — matches how server admins think about pricing.

Validation enforces id+name presence, no duplicate ids; warns
on:
  - minLevelToUnlock > 80 (unreachable at WotLK cap)
  - Premium slot with non-zero copperCost (donor slots are
    typically free; the gate is donor status, not gold)
  - duplicate displayOrder (stable UI position collision —
    only the first slot would render)

Wired through the cross-format table; WSTC appears in all 18
cross-format utilities. Format count 92 -> 93; CLI flag count
1069 -> 1074.
2026-05-09 23:58:49 -07:00
Kelsi
3f65e63ca1 feat(editor): add WHLD JSON round-trip (--export/--import-whld-json)
Closes the editing loop on the instance-lockout catalog: dump a
.whld to JSON, hand-edit raidLockoutKind / resetIntervalMs /
maxBossKillsPerLockout / raidGroupSize / bonusRolls (e.g. switch
ICC 25-Heroic from Weekly to SemiWeekly for a faster server,
raise Wintergrasp's reset from 2.5h to 3h, mark a custom
heroic+ tier with bonus rolls), re-import to a byte-identical
binary.

raidLockoutKind dual-encoded: int 0..3 OR human-readable name
("daily" / "weekly" / "semi-weekly" / "custom"). All other
fields are scalar uint32/uint8 so no dual encoding needed.

Verified byte-identical round-trip on all three presets
(raid / dungeon / event). The event preset's mixed kinds (2
Daily + 1 Custom 2.5h) round-trip exactly, confirming that the
Custom kind's arbitrary intervalMs preserves through the JSON
serialization. CLI flag count 1067 -> 1069.
2026-05-09 23:54:26 -07:00
Kelsi
321c2610d0 feat(editor): add WHLD (Instance Lockout Schedule) open catalog format
Open replacement for the engine-side instance reset timer logic
plus the per-map InstanceTemplate.dbc reset fields. Defines how
often each (map × difficulty) combination resets its lockout,
how many boss kills each character can claim per lockout window,
and the number of bonus rolls available (Cataclysm+ stub for
forward compatibility).

One entry per (map × difficulty × group size). Icecrown Citadel
10-Normal weekly, ICC 25-Normal weekly, ICC 10-Heroic weekly,
and ICC 25-Heroic weekly are four separate entries with the same
mapId but different difficultyId and resetIntervalMs.

Cross-references back to WMS (mapId), WCDF (difficultyId), and
forward to WBOS — the encounters bound to one lockout are the
WBOS entries whose (mapId, difficultyId) pair matches.

Four lockout kinds capture the canonical reset cadences:
  - Daily (24h, 86400000ms) — heroic dungeons, daily quests
  - Weekly (7d, 604800000ms) — raid lockouts
  - SemiWeekly (3.5d, 302400000ms) — Cata+ split lockouts
  - Custom (arbitrary intervalMs) — Wintergrasp 2.5h, holiday
    events with non-standard cadence

nextResetMs(lockoutId, currentMs) is the engine helper that
returns the next reset wall-clock millis after a given current
time, rounded up to the nearest interval boundary. The engine
overrides the epoch with its configured server reset time
(typically Tuesday 8:00am server-local), but the catalog
provides the interval shape.

The info renderer pretty-prints intervals: 86400000ms reads as
"1d", 9000000ms as "150m", which matches how server admins
think about reset cadences.

Three preset emitters: --gen-hld (4 ICC raid weekly lockouts),
--gen-hld-dungeon (4 5-man heroic daily lockouts),
--gen-hld-event (3 world-event lockouts including Wintergrasp's
canonical Custom 2.5h interval).

Validation enforces id+name+kind+resetIntervalMs presence, no
duplicate ids; warns on non-standard raidGroupSize, kind/interval
mismatches (Daily kind without 24h interval, Weekly kind without
7d interval), and 0 boss kill cap (instance grants no
lockout-bound progress, every visit is fresh).

Wired through the cross-format table; WHLD appears in all 18
cross-format utilities. Format count 91 -> 92; CLI flag count
1062 -> 1067.
2026-05-09 23:51:49 -07:00
Kelsi
b16ee9886b feat(editor): add WBOS JSON round-trip (--export/--import-wbos-json)
Closes the editing loop on the boss-encounter catalog: dump a
.wbos to JSON, hand-edit phaseCount / enrageTimerMs / berserk
spell / recommended item level / required party size (e.g.
shorten Lich King's enrage from 15 to 12 minutes for a sweatier
server, raise Marrowgar's recommended ilvl from 232 to 245 for
a Heroic-equivalent variant, swap a 5-phase encounter to 6
phases for a custom server fight), re-import to a byte-identical
binary.

All fields are scalar uint32/uint16/uint8 so no dual encoding
needed — the JSON exporter emits each field directly. The
catalog is the highest fan-out so far among raid-content
catalogs, cross-referencing five other catalogs (WCRT for boss
creature, WMS for map, WCDF for difficulty routing, WSPL for
berserk spell, WACR back-references via achievement targetId).

Verified byte-identical round-trip on all three presets
(5man / raid / world). CLI flag count 1060 -> 1062.
2026-05-09 23:47:19 -07:00
Kelsi
acaef78696 feat(editor): add WBOS (Boss Encounter Definition) open catalog format
Open replacement for AzerothCore's instance_encounter SQL table
plus the per-boss script bindings. Defines raid boss encounter
metadata: which creature is the boss, which map and difficulty
variant it lives in, how many phases the encounter has, the
soft-enrage timer and berserk spell, recommended group size, and
item level.

One entry per (boss × difficulty) combination. Lord Marrowgar in
10-Normal ICC is one entry; Lord Marrowgar in 25-Heroic ICC is a
separate entry with a higher recommendedItemLevel and a different
difficultyId pointing into WCDF.

This format ties together five other catalogs into a coherent
encounter description:
  - WCRT for the boss creature template
  - WMS for the instance map
  - WCDF for difficulty routing (10/25/H10/H25 variants)
  - WSPL for the berserk spell that fires at enrage
  - WACR for achievement criteria like "kill The Lich King in
    25-Heroic" that point back via KillCreature targetId

findByMap(mapId) returns all encounters in one raid instance,
sorted by their catalog order — used by the Encounter Journal
UI and instance lockout logic. findByBossCreature(bossId)
returns all difficulty variants of one boss.

Three preset emitters: --gen-bos (3 5-man dungeon bosses with
no soft-enrage), --gen-bos-raid10 (4 ICC 10-Normal bosses
including 5-phase Lich King with 15min hard enrage via Fury of
Frostmourne 72546), --gen-bos-world (2 outdoor world bosses
with 25-player size + no difficulty).

Validation enforces id+name+boss+map+phases+size presence, no
duplicate ids; warns on:
  - non-standard requiredPartySize (canonical sizes are
    5/10/25/40)
  - berserkSpellId set without enrageTimerMs (spell never fires)
  - enrageTimerMs > 30 minutes (sanity check)

Wired through the cross-format table; WBOS appears in all 18
cross-format utilities. Format count 90 -> 91; CLI flag count
1055 -> 1060.
2026-05-09 23:45:26 -07:00
Kelsi
5a12f5d183 feat(editor): add WCMR JSON round-trip (--export/--import-wcmr-json)
Closes the editing loop on the creature-patrol catalog: dump a
.wcmr to JSON, hand-edit pathKind / moveType / waypoint coords /
delays (e.g. add a new mid-route waypoint to a city guard's
patrol, change Stormwind cathedral guards from Walk to Run kind,
extend an ICC patrol from 16 to 24 waypoints to cover a wider
area), re-import to a byte-identical binary.

First round-trip with truly variable-length payloads. The
waypoint arrays serialize as JSON arrays of {x, y, z, delayMs}
objects — adding or removing waypoints in the JSON sidecar
preserves the length-prefixed binary layout on import. The
generic --bulk-export-json / --bulk-import-json utilities work
on these too without modification, since the sidecar follows
the same magic-pattern naming convention.

Two dual-encoded fields:
  - pathKind: int 0..3 OR "loop" / "one-shot" / "reverse" /
    "random"
  - moveType: int 0..3 OR "walk" / "run" / "fly" / "swim"

Verified byte-identical round-trip on all three presets
(patrol / city / boss). The boss preset's 12-pt + 8-pt + 16-pt
patrols (36 waypoints total = 576 bytes of waypoint payload)
round-trip exactly, confirming the variable-length encoding
preserves byte-for-byte order in both directions. CLI flag count
1053 -> 1055.
2026-05-09 23:41:17 -07:00
Kelsi
7d3b80e1f7 feat(editor): add WCMR (Creature Patrol Path) — 90th open format milestone
Open replacement for AzerothCore's creature_movement / waypoints
SQL tables plus the per-spawn waypoint arrays. Defines named
waypoint paths that creatures patrol along: Stormwind guards
walking the city perimeter, AQ40 trash rotating through the
chamber, ICC patrols circling the spire.

Each entry binds a creatureGuid to a sequence of (x, y, z,
delayMs) waypoints. The pathKind controls cycling behavior
(Loop / OneShot / Reverse / Random) and moveType controls the
locomotion kind (Walk / Run / Fly / Swim) — a flying patrol
ignores ground geometry, a swimming patrol stays underwater.

This is the first open format with truly variable-length
per-entry payload. Earlier formats with multi-slot fields
(WSPR's 8-reagent slots, WPSP's 4-item arrays) used fixed-size
caps padded with zeros. WCMR instead uses an inline
length-prefixed waypoint array — entries can be 4 waypoints or
4000, with the loader advancing through the file by reading the
count first then count*16 bytes of waypoint data. Cap of 64K
waypoints per path keeps a corrupted file from allocating
gigabytes.

pathLengthYards(pathId) is the engine helper that sums segment
distances between consecutive waypoints (closing the loop for
Loop kind). Tested across 12-point and 16-point circular paths
that geometrically resolve to the expected ~25y radius and
~60y radius totals.

Cross-references back to WCRT — creatureGuid points at the
spawned creature instance whose behavior mode follows this
patrol.

Three preset emitters: --gen-cmr (3 small paths showing each
pathKind variant), --gen-cmr-city (4 capital-city guard 6-point
loops with 2.0-2.5s waypoint dwell), --gen-cmr-boss (3 long
raid-zone patrols up to 16 waypoints, demonstrating that
variable-length payloads scale).

Validation enforces id+name+creatureGuid+waypoints presence,
pathKind 0..3, moveType 0..3, no duplicate ids; warns on
1-waypoint paths (creature would idle in place) and Loop with
fewer than 3 waypoints (degenerate — indistinguishable from
Reverse).

This is the 90th open format milestone. Wired through the
cross-format table; WCMR appears in all 18 cross-format
utilities. Format count 89 -> 90; CLI flag count 1048 -> 1053.
2026-05-09 23:38:59 -07:00
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