Commit graph

406 commits

Author SHA1 Message Date
Kelsi
4be543a2ed feat(editor): add WWFL JSON round-trip (--export/--import-wwfl-json)
Dual encoding for both WWFL enums via the readEnumField
template: filterKind (int 0..5 OR 255 OR token "spam"/
"goldseller"/"allcaps"/"repeatchar"/"url"/"advertreward"/
"misc") and severity (int 0..3 OR token "warn"/"replace"/
"drop"/"mute"). caseSensitive accepts bool or int.

pattern and replacement serialize as plain JSON strings —
operators editing the JSON sidecar can hand-craft new
moderation patterns without binary tooling. Strings are
nlohmann::json-escaped on export and unescaped on import,
preserving literal special characters byte-identically.

All 3 presets (spam/caps/url) byte-identical roundtrip
OK. CLI flag count 1239 -> 1241.
2026-05-10 02:36:32 -07:00
Kelsi
7d201cd6f3 feat(editor): add WWFL (Word Filter) — 116th open format
Novel replacement for the implicit chat-moderation
patterns vanilla WoW carried in the bad-word checker
(the hardcoded substring list the CMSG_MESSAGECHAT
handler walked before broadcasting). Each entry defines
one pattern the chat preprocessor matches against
outbound messages, the replacement to apply (or
"drop"/"warn"/"mute" the sender), and a kind tag for
analytics.

Seven filterKind values (Spam / GoldSeller / AllCaps /
RepeatChar / URL / AdvertReward / Misc) and four
severity levels (Warn — log only, Replace — substitute
matched span, Drop — silently discard, Mute — drop AND
mute sender). Per-filter caseSensitive flag for context-
specific rules (uppercase shouting detection vs
lowercase RMT keyword detection).

Intentionally non-profanity focused — the ecosystem
distributes through CI / public PRs where embedded
profanity creates reviewer-experience and licensing
concerns. The presets cover the moderation surfaces
server admins actually need: makeSpamRMT (5 RMT
patterns: wts/wtb gold drops, g0ld typo-substitution
replace, 1000g-for bulk-offer drop, free-gold mute),
makeAllCaps (3 shouting patterns), makeURLDetect (3
URL-leakage patterns: http://, https://, www.).
Profanity-list integration is left to deployment-time
configuration where local laws and community standards
apply.

Validator's most novel check is per-pattern uniqueness
— two filters with the same pattern would dispatch
ambiguously through the chat preprocessor. Also warns
on Replace severity with empty replacement (would
silently lose match — use Drop explicitly if intended).

Format count 115 -> 116. CLI flag count 1234 -> 1239.
2026-05-10 02:35:06 -07:00
Kelsi
aaf169a8af feat(editor): add WTRD JSON round-trip (--export/--import-wtrd-json)
Dual encoding for both WTRD enums via the readEnumField
template: ruleKind (int 0..6 OR token "allowed"/
"forbidden"/"soulboundexception"/"crossfactionallowed"/
"levelgated"/"goldescrowmax"/"auditlogged") and
targetingFilter (int 0..4 OR token "anyplayer"/
"samerealmonly"/"samefactiononly"/"sameaccountonly"/
"gmonly").

itemCategoryFilter serializes as raw uint32 (it's a
WIT item-class bitmask — pretty-printing would
duplicate WIT's existing class-name table).
goldEscrowMaxCopper as uint64 to support the >4 billion
copper cap range (~430,000 gold) typical for high-
value AH-bypass trades.

All 3 presets (standard/admin/rmt) byte-identical
roundtrip OK. CLI flag count 1232 -> 1234.
2026-05-10 02:31:44 -07:00
Kelsi
05bb96d23b feat(editor): add WTRD (Trade Window Rules) — 115th open format
Novel replacement for the implicit player-to-player
trade policy rules vanilla WoW hardcoded across the
trade-window message handlers (CMSG_INITIATE_TRADE,
CMSG_SET_TRADE_ITEM, CMSG_SET_TRADE_GOLD), the
soulbound-item check, the cross-faction-trade
rejection, and the GM-trade audit hooks. Each entry is
one trade-policy rule the trade-window state machine
consults at every state transition.

Seven ruleKind values (Allowed / Forbidden /
SoulboundException / CrossFactionAllowed / LevelGated /
GoldEscrowMax / AuditLogged) and five targetingFilter
values (AnyPlayer / SameRealmOnly / SameFactionOnly /
SameAccountOnly / GMOnly) cover the full trade-policy
surface. Priority field resolves rule conflicts —
higher priority wins (Allowed at 100 overrides
Forbidden at 10).

Three preset emitters cover real-world deployment
patterns: makeStandard (4 baseline rules — Soulbound
Forbidden globally, Quest items Forbidden, 2hr Soul-
boundException for raid trade-back, SameFactionOnly),
makeServerAdmin (3 server-custom overrides — GM-only
escrow at priority 100, AccountBound own-character
transfer, CrossFactionAllowed at level 80 for RP
servers), makeRMTPrevent (4 anti-RMT rules — 10g cap
for low-level trades, 500g cap for accounts < 30 days,
audit log for trades > 1000g, 24hr first-trade delay).

Validator's most novel check is the GoldEscrowMax /
goldEscrowMaxCopper consistency rule: a GoldEscrowMax-
kind rule MUST specify a non-zero gold cap (zero would
mean unlimited which contradicts the rule's purpose).
Also warns on GMOnly targeting with priority < 50 (GM-
mediated rules typically need high priority to override
player-initiated rules) and levelRequirement > 80
(exceeds current cap, rule never applies).

Format count 114 -> 115. CLI flag count 1227 -> 1232.
2026-05-10 02:30:32 -07:00
Kelsi
65c51a272f feat(editor): add WVOX JSON round-trip (--export/--import-wvox-json)
Dual encoding for both WVOX enums via the readEnumField
template: eventKind (int 0..8 OR token "greeting"/
"aggro"/"death"/"queststart"/"questprogress"/
"questcomplete"/"goodbye"/"special"/"phase") and
genderHint (int 0..2 OR token "male"/"female"/"both").
volumeDb serializes as signed int8 since boss-call
clips use +5dB above ambient and quiet whisper clips
can use -10dB or lower.

transcript serializes as a plain JSON string —
operators editing the JSON sidecar can rewrite voice
transcripts for accessibility (TTS engines and chat-
bubble subtitles) without binary tooling.

All 3 presets (questgiver/boss/vendor) byte-identical
roundtrip OK. CLI flag count 1225 -> 1227.
2026-05-10 02:27:03 -07:00
Kelsi
78555a79b0 feat(editor): add WVOX (Voiceover Audio) — 114th open format
Novel replacement for the implicit per-NPC voice dialog
system vanilla WoW encoded across CreatureTextSounds
(server-side aggro/death barks), npc_text (gossip audio
cross-references), and per-quest dialog blobs. Each
entry binds one NPC to one voice clip for one
triggering event with metadata covering audio path,
duration, volume, gender hint for randomized casts,
variant index for multiple lines per event, and a
transcript field for accessibility (TTS engines + chat-
bubble subtitles).

Nine eventKind values cover the full NPC dialog
surface: Greeting / Aggro / Death / QuestStart /
QuestProgress / QuestComplete / Goodbye / Special /
Phase. The Phase kind specifically supports boss-fight
percentage milestones (75%/50%/25% transitions) where
multiple Phase entries with distinct variantIndex
disambiguate the boss-encounter scripting.

Three preset emitters: makeQuestgiver (5-clip canonical
quest dialog flow), makeBoss (6-clip Lich King fight
with phase milestones at 75/50/25%, special mechanic
call at +5dB for raid audibility, death line),
makeVendor (4-clip vendor interaction).

Validator's most novel check is per-(npcId, eventKind,
variantIndex) triple uniqueness — two clips with all
three matching would be ambiguous when the trigger
handler picks one randomly. The vendor preset
originally bound both Buy and Sell to (Special, 0)
which the validator caught and flagged before commit;
fix uses variantIndex 0 for Buy and 1 for Sell so the
trigger handler can distinguish.

Validator also warns on durationMs=0 with non-empty
audioPath (subtitle sync impossible), volumeDb outside
[-20,+6] (clip risk), and empty transcript (TTS +
chat-bubble subtitle would be blank).

Format count 113 -> 114. CLI flag count 1220 -> 1225.
2026-05-10 02:25:34 -07:00
Kelsi
a5a16cae52 feat(editor): add WSPV JSON round-trip (--export/--import-wspv-json)
Dual encoding for conditionKind (int 0..5 OR token
"stance"/"form"/"talent"/"race"/"equippedweapon"/
"auraactive"). conditionValue stays as raw uint32 since
its semantics depend on conditionKind — pretty-printing
would require six different lookup paths (stance
spellId, talent ID, race bit, etc.) which is more
complexity than benefit.

All 3 presets (warrior/talent/racial) byte-identical
roundtrip OK. CLI flag count 1218 -> 1220.
2026-05-10 02:21:32 -07:00
Kelsi
6403d84a28 feat(editor): add WSPV (Spell Variant) — 113th open format
Novel replacement for the implicit context-conditional
spell substitution rules vanilla WoW encoded across
SpellSpecificType, SpellEffect.EffectMechanic override
fields, and the proc-modified spell tables in
SpellProcEvent. Each entry binds one base spell to a
variant spell that activates when a runtime condition
is met (player in a specific stance, talent talented,
racial buff active, weapon equipped, aura present).

Six conditionKind values cover the full substitution
surface: Stance / Form / Talent / Race / EquippedWeapon
/ AuraActive. The conditionValue field is polymorphic —
its semantics depend on conditionKind (a stance spellId,
a talentId, a race bit, etc.). The spell-cast pipeline
iterates findByBaseSpell at cast time and picks the
highest-priority variant whose condition is satisfied,
falling through to the base spell if none matches.

Three preset emitters demonstrating the pattern:
makeWarriorStance (4 stance-conditional Warrior
variants — Heroic Strike Berserker damage bonus,
Battle baseline, Mocking Blow Defensive AoE taunt,
Pummel Berserker-only gate), makeTalentMod (4 talent-
modified variants — Frostbolt + Brain Freeze instant,
Lava Burst + Flame Shock auto-crit, Earth Shield +
Improved bonus heal, Ferocious Bite + Berserk),
makeRacial (4 race-gated racials — Stoneform Dwarf,
War Stomp Tauren, Berserking Troll, Will of the
Forsaken).

Validator's most novel check is the (baseSpell,
conditionKind, conditionValue, priority) 4-tuple
uniqueness — two variants with all four matching
would tie at runtime and resolve non-deterministically
(the spell-cast pipeline's std::sort by priority is
stable but the underlying iteration order is undefined
when priorities tie). Packs the tuple into 64 bits
(base 32 | value 16 | kind 8 | prio 8) for set lookup.

Format count 112 -> 113. CLI flag count 1213 -> 1218.
2026-05-10 02:20:19 -07:00
Kelsi
81eb854709 feat(editor): add WMVC JSON round-trip (--export/--import-wmvc-json)
Dual encoding for category (int 0..6 OR token
"production"/"music"/"audio"/"engineering"/"art"/
"voice"/"special"). lines[] serializes as a plain JSON
string array — directly editable to add, remove, or
reorder credit lines without binary tooling. The string-
array round-trip preserves all whitespace and special
characters byte-identically since nlohmann::json escapes
them on export and unescapes on import.

All 3 presets (wotlk/quest/starter, 12 credit blocks
across 44 lines total) byte-identical roundtrip OK.
CLI flag count 1211 -> 1213.
2026-05-10 02:16:54 -07:00
Kelsi
46213baea0 feat(editor): add WMVC (Movie Credits Roll) — 112th open format
Novel replacement for the embedded credit-roll text
vanilla WoW carried inside the cinematic-renderer blob
(the post-cinematic credits that scroll up the screen
after each expansion intro). Each entry binds one
credits category (Production / Music / Voice Acting /
etc.) for one cinematic to its ordered list of credit
lines.

First catalog with a variable-length STRING array
payload — previous variable-length formats used int
arrays (WCMR waypoints / WCMG mutex spells / WPTT
rank-spells / WBAB rank chains / WRPR unlocked items +
recipes). The lines[] field serializes as count +
(length + bytes)* per line, mirroring how strings work
elsewhere in the catalog set just lifted into a per-
entry array.

Seven category enum values cover the full credit
taxonomy: Production / Music / Audio / Engineering /
Art / Voice / Special. Three preset emitters:
makeWotLKIntro (5 blocks for the WotLK Arthas/Terenas
cinematic with the actual canonical music credits —
Brower/Duke/Stafford/Hayes), makeQuestCinema (3-block
template for per-quest cinematics), makeStarterRoll
(4-block generic template).

orderHint sorts blocks within a single cinematic so
the renderer can render Production -> Direction ->
Music -> Voice -> Special Thanks in canonical order
without depending on entry order in the binary.

Validator's most novel checks combine string + grouping
constraints unique to credit rolls: per-cinematic
orderHint slot uniqueness — two blocks at the same
(cinematicId, orderHint) would render in non-
deterministic order due to the std::sort being stable
but content-order undefined. Per-line: empty lines
warn (would render as blank, intentional spacers
should use a placeholder character), lines >80 chars
warn (text-buffer wrap at the canonical 80-char
credit-renderer width).

Format count 111 -> 112. CLI flag count 1206 -> 1211.
2026-05-10 02:15:34 -07:00
Kelsi
9a0309818e feat(editor): add WPCR JSON round-trip (--export/--import-wpcr-json)
Dual encoding for actionKind (int 0..10 OR token form
covering all 11 kinds: revive/mend/feed/dismiss/tame/
beastlore/stable/untrain/rename/abandon/summon).
requiresPet and requiresStableNPC accept bool or int.
happinessRestore serializes as signed int8 (-25..+25
typical, +/-127 hard limit) — matters because Abandon's
happiness penalty (canonically -10) is negative.

All 3 presets (hunter/stable/warlock) byte-identical
roundtrip OK. CLI flag count 1204 -> 1206.
2026-05-10 02:12:01 -07:00
Kelsi
cebf821205 feat(editor): add WPCR (Pet Care & Action) — 111th open format
Novel replacement for the implicit pet-management action
rules vanilla WoW scattered across spell_template
(Revive Pet / Mend Pet / Feed Pet / Dismiss Pet
definitions), npc_text (stable master gossip), and
per-class trainer SQL. Each entry binds one pet
management action to its dispatching spell, gold cost,
reagent requirement, cast time, cooldown, and pet/NPC
pre-conditions.

Eleven actionKind enum values cover the full pet
management surface: Revive / Mend / Feed / Dismiss /
Tame / BeastLore / Stable / Untrain / Rename / Abandon
(Hunter), plus Summon (Warlock minion conjures). The
classFilter field uses WCHC class-bit conventions
(4=Hunter, 256=Warlock) so a single WPCR catalog can
cover both class systems.

Three preset emitters: makeHunterCare (5 Hunter pet
care actions), makeStableActions (4 stable-master
gold-cost actions), makeWarlockMinions (4 Warlock minion
summons with shared 10s cooldown + Soul Shard reagent).

Validator's most novel checks are PER-KIND constraints:
Tame and Summon require requiresPet=0 (you can't tame
or summon while another pet is active) — these are
ERRORS, not warnings, since the action would simply
fail at runtime. Stable kind without requiresStableNPC
warns (stable-slot purchases are normally gated to
stable-master conversation). Tame kind without cooldown
warns (canonical 15s anti-macro-spam cooldown). The
TameBeast preset originally omitted this cooldown — the
validator caught and flagged it during smoke-test, fix
applied before commit.

Format count 110 -> 111. CLI flag count 1199 -> 1204.
2026-05-10 02:10:54 -07:00
Kelsi
cbc4991305 feat(editor): add --catalog-id-range for ID-allocation planning
New utility scans every catalog file under a directory
tree and reports the primary-key id range, gap count,
first gap, and recommended next id (smallest gap if any,
else max+1). Useful when adding new entries without
conflicts: instead of opening the file in --info to read
the current max id, run --catalog-id-range and pick the
recNext value.

Optional --magic <WXXX> filter narrows to one format
family. Output is sorted by file path for deterministic
shell-pipeline behavior.

Skips files whose format has no --info-* surface
(asset formats like .wom/.wob/.whm). Permission-denied
subdirs handled gracefully via skip_permission_denied.
Reuses the same primary-key heuristic + foreign-key
filter as --catalog-pluck and --catalog-find.

CLI flag count 1198 -> 1199.
2026-05-10 02:06:49 -07:00
Kelsi
cf886ec94b feat(editor): add WMNL JSON round-trip (--export/--import-wmnl-json)
Plain-shape JSON sidecar — WMNL has no enum fields to
coerce, just positional + textual data: levelIndex
(uint8), minZ/maxZ (floats), texturePath/displayName
(strings). Float-precise round-trip preserved (the
Stormwind/Dalaran/Undercity Z-range boundaries don't
have any rounding artifacts since they were authored
as integer-multiple values like 80.0, 130.0, 200.0).

All 3 presets (Stormwind/Dalaran/Undercity, 3+4+5 = 12
levels total across the city stack) byte-identical
roundtrip OK. CLI flag count 1196 -> 1198.
2026-05-10 02:04:47 -07:00
Kelsi
d49080db92 feat(editor): add WMNL (Minimap Multi-Level) — 110th open format
Novel replacement for the WorldMapTransforms.dbc +
WorldMapOverlay.dbc pair vanilla used to describe zones
with multiple vertical layers visible on the minimap
(Stormwind has Old Town / Cathedral / Keep at three
altitudes; Dalaran has Sewers / Street / Above Street /
Floating; Undercity has 5 distinct levels Sewer to
Throne Room). Each entry binds one (mapId, areaId,
levelIndex) triplet to a Z-range, minimap layer texture,
and display label.

The catalog acts as a per-level overlay on top of WMPX
world-map mappings: at every camera tick, the minimap
renderer queries findContainingZ(playerZ) to swap the
overlay layer when the player crosses a floor boundary.

Three preset emitters one per layered city: makeStormwind
(3 levels), makeDalaran (4 levels), makeUndercity (5
levels — deepest stack). Z-ranges abut precisely to
ensure clean transitions: Sewer Z[-110, -85), Canal
Z[-85, -65), Outer Ring Z[-65, -45), Inner Ring Z[-45,
-20), Throne Z[-20, 30) — half-open intervals so the
boundary Z value belongs to the upper level.

Validator's most novel checks combine grouping +
geometric constraints unique to multi-level layouts:
- per-area levelIndex uniqueness (no two levels at the
  same index — picker UI would show duplicate slot)
- per-area Z-range non-overlap (overlapping ranges
  would cause minimap-flicker as the player crosses
  the overlap region; the renderer can't decide which
  layer to display)
Plus the standard: id+name+areaId required, minZ<maxZ
(non-empty range), no duplicate levelIds.

Format count 109 -> 110. CLI flag count 1191 -> 1196.
2026-05-10 02:03:43 -07:00
Kelsi
4aa2138749 feat(editor): add WRPR JSON round-trip (--export/--import-wrpr-json)
Two parallel variable-length arrays (unlockedItemIds +
unlockedRecipeIds) serialize as plain JSON int arrays —
operators editing JSON can add or remove unlock entries
without touching either array's parallel partner.
grantsTabard and grantsMount accept bool or int.

Export adds a derived standingTier label
("Friendly"/"Honored"/"Revered"/"Exalted"/"Neutral"/etc.)
alongside the raw minStanding int — purely informational
for JSON readability, ignored on import (the int is
authoritative). Reuses the standingTierName helper
already used by --info-wrpr table display.

All 3 presets (argent/kaluak/accord) byte-identical
roundtrip OK. CLI flag count 1189 -> 1191.
2026-05-10 02:00:19 -07:00
Kelsi
8fee281899 feat(editor): add WRPR (Reputation Reward tier) — 109th open format
Novel replacement for the implicit reputation-tier rules
vanilla WoW encoded across multiple SQL tables
(npc_vendor with reqstanding columns, item_template
faction gates, quest_template ReqMinRepFaction). Each
WRPR entry binds one (factionId, minStanding) tier to
its rewards: a vendor discount percentage, two variable-
length arrays of unlocked content (item IDs + recipe
IDs), and tabard + mount unlock boolean flags.

First catalog with TWO variable-length payload arrays
per entry (unlockedItemIds + unlockedRecipeIds) —
previous variable-length formats used a single array
(WCMR waypoints, WCMG members, WPTT spellIdsByRank,
WBAB rank-chain pointers). The two-array shape is
serialized as count1 + ids1[] + count2 + ids2[] for
easy reader-side validation.

Three preset emitters: makeArgentCrusade (4 tiers
Friendly/Honored/Revered/Exalted with progressive items
+ recipes plus Argent Charger mount at Exalted),
makeKaluak (4 fishing-themed tiers with cooking recipe
unlocks plus Pygmy Suit cosmetic at Exalted),
makeAccordTabard (3 tiers showcasing both grantsTabard
and grantsMount flags via Wyrmrest Accord's iconic
Reins of the Red Drake mount).

Validator's most novel checks combine relational and
domain logic: (factionId, minStanding) tuple uniqueness
prevents ambiguous active-tier lookup, AND per-faction
monotonic discount progression — sorts each faction's
tiers by standing and verifies discountPct is non-
decreasing. A higher reputation tier giving a worse
vendor discount would be a content authoring bug.

findActiveTierFor() helper picks the highest-standing
tier the player meets — used by the vendor UI to
compute the active discount without scanning the
catalog.

Format count 108 -> 109. CLI flag count 1184 -> 1189.
2026-05-10 01:59:03 -07:00
Kelsi
f7ea99948a feat(editor): add WHRD JSON round-trip (--export/--import-whrd-json)
Novel pct/basis-points dual encoding for
bonusQualityChance: stored on disk as raw uint16 basis
points (0..10000) for compactness and integer-precision
math, but exported with both forms — bonusQualityChance
(int basis points, authoritative) and bonusQualityPct
(float, derived = bp / 100). Import accepts either form;
when only bonusQualityPct is present, converts pct *
100 to basis points with rounding.

This pattern is novel for the catalog set: most
percentage-style fields stored either as int (precise,
unfriendly) or as float (friendly, lossy round-trip).
The dual form gets both: byte-identical round-trip from
binary, AND human-friendly editing in JSON.

itemLevelDelta serializes as signed int16 (Heroic
modes can technically downgrade ilvl, though the
validator warns on negative). dropChanceMultiplier as
plain float.

All 3 presets (5man/raid25/challenge-mode) byte-
identical roundtrip OK. Pct-form import smoke-tested
with 7.5%% -> 750 basis points conversion. CLI flag
count 1182 -> 1184.
2026-05-10 01:55:23 -07:00
Kelsi
81b8572b50 feat(editor): add WPTT JSON round-trip (--export/--import-wptt-json)
Dual encoding for treeKind (int 0..2 OR token "cunning"
/ "ferocity" / "tenacity"). spellIdsByRank serializes as
a plain JSON array of spell IDs — directly editable for
adding or removing per-rank spells from a talent.

All 3 preset trees (ferocity/cunning/tenacity) byte-
identical roundtrip OK. CLI flag count 1180 -> 1182.
2026-05-10 01:54:13 -07:00
Kelsi
ede6fb9c3a feat(editor): add WHRD (Heroic Loot Scaling) — 108th open format
Novel replacement for the implicit Heroic-mode loot rules
vanilla WoW encoded in dungeon/raid script systems: a
Normal-mode boss drops items from one loot table, the
Heroic-mode version drops the same items at +N item
levels with M× drop chance plus an optional Heroic-only
currency token. Each WHRD entry binds one (mapId,
difficultyId) combination to its scaling rules so the
loot-roll engine can layer the modifiers over the base
WLOT loot table at encounter death.

Six tunable fields per scaling: itemLevelDelta (signed
int16, typically +13 for 5-man Heroic, +13 to +26 for
raid Heroic), bonusQualityChance (basis points 0..10000
for the probability of a +1-quality-tier bonus drop),
dropChanceMultiplier (float, 1.0 = same rate, 1.5 =
+50%), heroicTokenItemId (per-tier currency reward like
Emblem of Frost), bonusEmblemCount (extra emblems on
top of base 1× per boss).

mapId=0 is a wildcard that applies the scaling to ANY
map at the given difficultyId — used by the
ChallengeMode preset to define generic Bronze/Silver/
Gold tier scalings without naming each instance.

Three preset emitters: makeWotLK5manHeroic (5 WotLK
5-man Heroics: Utgarde Keep, Nexus, Azjol-Nerub,
Ahn'kahet, Drak'Tharon — all +13/2× Emblem of Heroism),
makeRaid25Heroic (4 25H raids: Naxx +13, EoE +13,
Ulduar +26, ICC +26 with corresponding Conquest/Triumph/
Frost emblems), makeChallengeMode (3 anachronistic
challenge-mode tiers as a template for custom servers
backporting MoP-era systems).

Validator's most novel checks are bounds-aware:
bonusQualityChance capped at 10000 basis points (above
that would guarantee multiple bonus drops), no negative
itemLevelDelta (Heroic shouldn't be worse than Normal —
warning, not error), no >50 ilvl delta (beyond canonical
range — warning), no zero or excessive dropChance-
Multiplier, AND (mapId, difficultyId) tuple uniqueness
unless mapId=0 wildcard (multiple scalings binding the
same instance+difficulty would make loot-roll lookup
ambiguous).

Format count 107 -> 108. CLI flag count 1175 -> 1180.
2026-05-10 01:52:58 -07:00
Kelsi
81070c470c feat(editor): add WPTT (Pet Talent Tree) — 107th open format
Novel replacement for the PetTalent.dbc + PetTalentTab.dbc
pair that defined the Hunter pet talent system added in
WotLK. Each entry is one talent in one of the three pet
trees (Cunning/utility, Ferocity/DPS, Tenacity/tank),
placed at a (tier, column) grid position with a per-rank
spell ID array, an optional prerequisite-talent edge,
and a legacy loyalty-level requirement carried over
from Vanilla pet happiness mechanics.

Combines three patterns previously seen separately into
one format: variable-length payload (spellIdsByRank[]
mirroring WCMR's members[]), graph edge
(prerequisiteTalentId mirroring WBAB's previousRankId),
and grid placement (tier+column — first format with
explicit 2D layout coordinates the renderer can use to
draw the talent tree UI).

Three preset emitters one per tree: makeFerocity (6
talents tiers 0-3 with prereq chain CobraReflexes ->
SpikedCollar -> SpidersBite plus parallel Serpent ->
Boars -> Rabid), makeCunning (5 talents Dash/Owls/
Recovery/Cornered/Phoenix), makeTenacity (5 talents
Charge/Stamina/Stomp/Taunt/LastStand).

Validator's most novel checks combine grid + graph
constraints unique to talent-tree formats:
- (tree, tier, column) cell uniqueness — two talents in
  the same cell would render on top of each other
- prereq must resolve to an existing talent IN THE SAME
  TREE (cross-tree prereqs are illegal)
- prereq tier must be STRICTLY LESS than this tier
  (talents only depend on earlier tiers, no
  same-tier or backward dependencies)
- spellIdsByRank.size() must EQUAL maxRank exactly
- no zero spell IDs within the rank array
Plus the standard: id+name required, treeKind 0..2,
tier 0..6 (7 tiers), column 0..2 (3 columns), maxRank
1..5, no duplicate talentIds, no self-referencing
prereqs.

Format count 106 -> 107. CLI flag count 1170 -> 1175.
2026-05-10 01:49:20 -07:00
Kelsi
3c69f33465 feat(editor): add --catalog-by-name entry-name substring search
New utility complements --catalog-find (id-based,
single-id lookup) and --catalog-grep (catalog-header
label only) by searching every catalog file under a
directory tree for entries whose `name` field contains a
substring. Reports each hit as [WXXX] file id=N "name"
so the operator can find catalog entries by half-
remembered names — "find anything called Mark of the
Wild" hits all 5 rank entries across WBAB, "find Argent"
hits the WTBD tabard, etc.

Optional flags:
- --magic <WXXX>: limit search to one format family,
  same convention as --catalog-find.
- --ignore-case: lowercase both pattern and haystack
  before substring match.

Returns rc=0 on hits, rc=1 if no entries matched (so the
caller can `if --catalog-by-name ... ; then ...; fi`).
Skips files with unknown magic and files whose format
has no --info-* surface (asset formats like .wom).
Permission-denied subdirs skipped via
skip_permission_denied directory_options.

Closes the search triplet:
  --catalog-grep    catalog-header label
  --catalog-find    entry primary-key id
  --catalog-by-name entry name substring

CLI flag count 1169 -> 1170.
2026-05-10 01:45:09 -07:00
Kelsi
3c8516f8d4 feat(editor): add WCRE JSON round-trip (--export/--import-wcre-json)
Dual encoding for ccImmunityMask: int OR "+"-joined
token string composed from {root, snare, stun, fear,
sleep, silence, charm, disarm, polymorph, banish,
knockback, interrupt, taunt, bleed} plus the shorthands
"all" (0xFFFF, typical raid-boss profile) and "none" (0).
The "+" syntax matches what ccImmunityString emits on
info display so the round-trip uses identical syntax.

Per-bit token form is much more useful than raw bitfield
ints for hand-edited JSON — operators can read
"fear+silence" and immediately know the buff hits both
without doing 0x0028 & flag math.

mechanicImmunityMask remains a raw uint32 (no token form
yet — the underlying mechanic enum has 30+ values that
would clutter the token table; deferred until a real
authoring need surfaces).

All 3 presets (bosses/elites/immunities) byte-identical
roundtrip OK. Token-form import smoke-tested with
"fear+silence+polymorph" parsing to 0x0128 correctly.
CLI flag count 1167 -> 1169.
2026-05-10 01:42:56 -07:00
Kelsi
f9cad45154 feat(editor): add WCRE (Creature Resistance & Immunity) — 106th open format
Novel replacement for the per-creature resistance columns
that vanilla WoW buried inside creature_template
(resistance1..6 fields) plus the SpellSchoolMask immunity
and mechanic_immune_mask columns. Each entry is one
creature's full defensive profile: 6 magic-school resist
values (int16, with 32767 as the full-immunity sentinel),
a physical-resistance percentage (0..75 game-engine cap),
plus three immunity bitmasks (CC kinds, spell mechanics,
magic schools).

The CC-immunity mask uses 14 named bits: ImmuneRoot /
Snare / Stun / Fear / Sleep / Silence / Charm / Disarm /
Polymorph / Banish / Knockback / Interrupt / Taunt /
Bleed. The info display renders the mask as a "+"-joined
token list ("root+stun+fear") for readability; "all" for
0xFFFF (typical raid-boss CC profile) and "none" for 0.

Three preset emitters: makeRaidBosses (5 canonical raid
bosses with iconic single-school immunities — Ragnaros
fire / Vael 50%-all / Hakkar arcane / Kel'Thuzad shadow
/ Onyxia fire+frost partial), makeElites (5 mid-tier
elites with single-school resists), makeImmunities (4
selective CC-immunity test cases — root-immune treant,
stun-immune worg, silence-immune acolyte, fear+charm+
poly-immune undead).

Validator's most novel check is creatureEntry uniqueness
— multiple WCRE entries binding the same creature would
make the damage-calc lookup ambiguous (which profile
applies?). Also catches negative resists < -100 (extreme
>2x damage taken), physicalResistPct > 75 (clamped at
runtime to game-engine armor cap), and reserved bits in
schoolImmunityMask (only bits 0-5 are meaningful).

Format count 105 -> 106. CLI flag count 1162 -> 1167.
2026-05-10 01:40:39 -07:00
Kelsi
e4e15b3ffa feat(editor): add WLDN JSON round-trip (--export/--import-wldn-json)
Dual encoding for all 3 WLDN enums on import: triggerKind
(int 0..5 OR token "levelreach"/"factionstanding"/
"itemacquired"/"questcomplete"/"spelllearned"/
"zoneentered"), channelKind (int 0..4 OR token
"raidwarning"/"systemmsg"/"subtitle"/"tutorial"/
"motdappend"), factionFilter (int 1..3 OR token
"alliance"/"horde"/"both"). Reuses readEnumField
template pattern from prior catalog imports.

triggerValue serializes as signed int32 — required for
FactionStanding which can range from -42000 (Hated) to
+42000 (Exalted). Most other triggerKinds use positive
ids but the schema accepts any int32.

All 3 presets (levels/account/rep) byte-identical
roundtrip OK. Token-form import smoke-tested with
questcomplete + tutorial + alliance combination. CLI
flag count 1160 -> 1162.
2026-05-10 01:36:58 -07:00
Kelsi
15bb3e09bf feat(editor): add WLDN (Learning Notification) — 105th open format
Novel replacement for the hardcoded server-side
milestone messages that fire when a player crosses a
progression threshold ("You can now learn Apprentice
Riding" at level 20, "Dual specialization is now
available", "You have unlocked the auction house"). Each
entry binds one trigger condition (LevelReach /
FactionStanding / ItemAcquired / QuestComplete /
SpellLearned / ZoneEntered) to a delivery channel
(RaidWarning banner / SystemMsg / Subtitle / Tutorial
popup / MOTDAppend) and an optional fanfare sound.

The triggerValue field is polymorphic — its semantics
depend on triggerKind. The validator enforces per-kind
ranges: LevelReach 1-80 (current cap), FactionStanding
+/-42000 (Hated to Exalted bounds), ItemAcquired/
QuestComplete/SpellLearned/ZoneEntered must be a
positive id (>0). This is the first format to use
per-trigger discriminated value validation.

Three preset emitters: makeLevelMilestones (5
LevelReach unlocks at canonical thresholds 20/30/40/60/
80), makeAccountUnlocks (4 mixed-kind notifications:
first-mail tutorial gated to <2hr playtime, Stormwind
auction-house location subtitle, dual-spec activation
on spell-learn, transmog vendor unlock on quest-
complete), makeReputation (3 FactionStanding milestones
at Honored/Revered/Exalted standings).

minTotalTimePlayed gates first-time-only tutorials —
the auction-house location subtitle fires only for
characters with <2hr total time so veterans don't get
spammed.

Format count 104 -> 105. CLI flag count 1155 -> 1160.
2026-05-10 01:35:40 -07:00
Kelsi
267d525fe7 feat(editor): add WSPM JSON round-trip (--export/--import-wspm-json)
Dual encoding for edgeFadeMode (int 0..2 OR token "hard"
/ "softedge" / "pulse"). stackable and destroyOnCancel
accept both bool and int. Float fields (radius, duration)
serialize as JSON floats; tickIntervalMs and decalColor
as plain unsigned integers — operators editing JSON can
hand-write 0xAARRGGBB hex colors via Python int() prep.

All 3 presets (mage/raid/env) byte-identical roundtrip
OK. Token-form import smoke-tested with mixed pulse +
stackable + destroyOnCancel false. CLI flag count 1153
-> 1155.
2026-05-10 01:31:49 -07:00
Kelsi
62a10937e0 feat(editor): add WSPM (Spell Persistent Marker) — 104th open format
Novel replacement for the SpellAreaTrigger.dbc +
AreaTriggerCreateProperties pair vanilla used for AoE
ground decals. Each entry binds one spellId to a
ground-tracked decal: texture path, radius (in yards),
duration, damage tick interval, RGBA decal color, edge-
fade rendering mode (Hard / SoftEdge / Pulse), stack
flag, and destroy-on-cancel semantics for channeled
spells.

The catalog covers three distinct gameplay surfaces in
one shape: player-cast AoE (Blizzard, Flamestrike, etc.
that the visual effects pipeline spawns at cast time),
boss-arena hazard zones (Putricide poison pool,
Sindragosa frost tomb, Marrowgar Bone Storm radius
that raid encounters need to render so players know to
move), and persistent environmental effects
(Wintergrasp lightning strike, Silithus sandstorm cone
that the weather system spawns).

Three preset emitters one per surface: makeMageAoE
(Blizzard/Flamestrike/BlastWave/FrostNova), makeRaid-
Hazards (5 ICC encounter zones), makeEnvironment (3
weather/world hazards). Hazard variants set
destroyOnCancel=0 since they persist beyond any caster;
environment variants additionally set stackable=1 since
multiple lightning strikes can overlap.

Validator's most novel check is spellId uniqueness —
multiple WSPM entries binding the same spellId would
make the spell-cast lookup ambiguous (which decal does
the spell spawn?). Also catches empty texture paths
(decal would render solid color), radius<=0 (zero area),
tickIntervalMs<100ms (perf risk for stackable markers),
decalColor alpha=0 (invisible), and edge-fade enum
range.

Format count 103 -> 104. CLI flag count 1148 -> 1153.
2026-05-10 01:29:56 -07:00
Kelsi
7fa24af3b2 feat(editor): add WTBD JSON round-trip (--export/--import-wtbd-json)
Dual encoding for both WTBD enums on import:
backgroundPattern (int 0..4 OR token "solid" / "gradient"
/ "chevron" / "quartered" / "starburst") and
borderPattern (int 0..3 OR token "none" / "thin" /
"thick" / "decorative"). isApproved accepts bool or int.
The 3 RGBA color fields serialize as 0xAARRGGBB uint32
values directly — operators picking colors in JSON can
use any familiar hex form via Python int("0x...", 16) or
similar pre-processing.

All 3 presets (alliance/horde/faction) byte-identical
roundtrip OK. Token-form import smoke-tested with
starburst + decorative pattern combination. CLI flag
count 1146 -> 1148.
2026-05-10 01:26:02 -07:00
Kelsi
0f4c619b49 feat(editor): add WTBD (Tabard Design / Heraldry) — 103rd open format
Novel replacement for the GuildBankTabard / TabardConfig
blob that vanilla WoW stores per-guild in guild_member
SQL. Each entry is one tabard design: triplet of
(background pattern + color, border pattern + color,
emblem glyph + color), plus optional guild and creator
attribution and a server-approval flag for tabard-
moderation policies.

Five background patterns (Solid / Gradient / Chevron /
Quartered / Starburst), four border patterns (None /
Thin / Thick / Decorative), and 1024 possible emblem
glyph IDs. Three preset emitters demonstrate the
convention: makeAllianceClassic (4 Alliance-themed
system tabards: Lion, DwarvenHammer, KulTirasAnchor,
HighlordSword), makeHordeClassic (4 Horde: Wolfhead,
CrossedAxes, Skull, Pyramid), makeFactionVendor (6
faction-rep tabards spanning Argent Crusade, Ebon
Blade, Sons of Hodir, Wyrmrest Accord, Kalu'ak,
Frenzyheart Tribe).

Validator's most novel check is a color-similarity
heuristic — squared RGB distance between background and
emblem colors. If under 1500 (empirically derived
threshold for visual readability), warns the operator
that the emblem won't be readable against its
background. Also catches alpha=0 on any color layer
(would render fully transparent), pattern enum out-of-
range, and emblemId>1023 (beyond canonical glyph
range).

Also added per-magic explicit primary-key override to
--catalog-pluck and --catalog-find so they pick the
right field for catalogs where the heuristic fails.
WTBD has creatorPlayerId/emblemId/guildId all
alphabetically before tabardId, and guildId can't be
filtered globally because WGLD uses it as a primary
key. The override table is small (1 entry currently —
WTBD->tabardId) and grows only when a new format
catches the same conflict.

Format count 102 -> 103. CLI flag count 1141 -> 1146.
2026-05-10 01:24:46 -07:00
Kelsi
2d78dd57a7 feat(editor): add WBAB JSON round-trip (--export/--import-wbab-json)
Dual encoding for both WBAB enums on import:
statBonusKind (int 0..9 OR 255 OR token "stamina" /
"intellect" / "spirit" / "allstats" / "armor" /
"spellpower" / "attackpower" / "critrating" /
"hasterating" / "manaregen" / "other"), and a NEW
"+"-joined bitmask string form for targetTypeMask
("self+party+raid+friendly" parsed by splitting on '+'
then OR-ing the bits). The "+" syntax matches what
targetMaskString emits on info display so the round-
trip uses identical syntax.

Per-bit token form is more useful than raw bitfield
ints for hand-edited JSON — operators can read
"self+raid" and immediately know the buff hits self
plus all raid members without doing 0x05 & flag math.

All 3 presets (mage/druid/raid) byte-identical
roundtrip OK. Token-form import smoke-tested with
spellpower + self+raid+friendly together. CLI flag
count 1139 -> 1141.
2026-05-10 01:15:26 -07:00
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