Commit graph

419 commits

Author SHA1 Message Date
Kelsi
73323f0b9d feat(editor): add WLAN (Localization) — 123rd open format
Novel replacement for the per-language overlay tables
vanilla WoW carried as Locale_*.MPQ patches plus the
Spell.dbc / Item.dbc trailing 16-locale string columns.
Each entry binds one (originalKey, languageCode,
namespace) triple to its localized translation,
forming a per-language overlay applied AFTER any
per-format catalog has resolved its primary text.

Eleven languageCode values cover the canonical WoW
locales (enUS / enGB / deDE / esES / frFR / itIT /
koKR / ptBR / ruRU / zhCN / zhTW) plus Unknown=255 as
escape hatch. Eight namespace values segment the
lookup space (UI / Quest / Item / Spell / Creature /
Tooltip / Gossip / System) so a UI button "Cancel"
doesn't collide with an item description containing
the word "Cancel".

UTF-8 multibyte support is the novel demonstration —
the originalKey field is typically ASCII (English
canonical key), but localizedText holds Korean (취소),
Simplified Chinese (取消), or other non-Latin scripts.
The string-length-prefixed binary serialization
preserves byte-identical round-trip regardless of
encoding.

Three preset emitters: makeUIBasics (5 UI translations
of the "Cancel" button across deDE/frFR/esES/koKR/zhCN
including the Korean and Chinese multibyte UTF-8 strings),
makeQuestSample (3 entries — one quest title in
deDE/frFR/koKR illustrating the dotted-key convention
"QUEST.123.title"), makeTooltipSet (4 item-tooltip
strings in deDE+frFR — the high-volume client
localization use case).

Validator's most novel check is per-(originalKey,
languageCode, namespace) triple uniqueness — two
entries with all three matching would tie at runtime
when the locale-aware text layer looks up an override.
Plus the warning on empty localizedText (the override
would render blank — possibly worse than fallback to
the catalog default).

Format count 122 -> 123. CLI flag count 1283 -> 1288.
2026-05-10 03:13:28 -07:00
Kelsi
2f1cb271a8 feat(editor): add WPRG JSON round-trip (--export/--import-wprg-json)
Dual encoding for factionFilter (int 1=Alliance / 2=Horde
OR token "alliance" / "horde"). titlePrefix as plain
JSON string — round-trip preserves the spaces and
hyphens in titles like "Knight-Lieutenant" exactly.

honorRequiredWeekly + honorRequiredAchieve serialize as
plain uint32 — vanilla rank thresholds top out around
1,000,000 lifetime RP, well within uint32 range.

All 3 presets (alliance/horde/high-tier) byte-identical
roundtrip OK. CLI flag count 1281 -> 1283.
2026-05-10 03:10:03 -07:00
Kelsi
4ce07d5ca9 feat(editor): add WPRG (PvP Ranking grades) — 122nd open format
Novel replacement for the hardcoded 14-rank vanilla WoW
PvP ladder (Private through Grand Marshal Alliance,
Scout through High Warlord Horde). Each entry binds one
(factionFilter, tier) combination to its display name,
weekly RP threshold to maintain rank, lifetime honor
for first-time achievement, title prefix for player-
name display, and tier-set gear reward.

The vanilla rank-ladder system used a weekly RP-decay
mechanic that punished any week without play with rank-
loss; this catalog stores both the weekly threshold
(maintenance) and the lifetime threshold (achievement)
since both are needed for accurate rank-progression
simulation.

Three preset emitters spanning the rank ladder:
makeAllianceRanks (7 lower-tier ranks Private through
Knight-Lieutenant), makeHordeRanks (7 mirrored Horde
titles Scout through Blood Guard with identical honor
thresholds — factionFilter disambiguates the shared
"Sergeant" title), makeHighRanks (8 high-tier ranks
across both factions Knight-Captain through Lt.
Commander, tiers 8-11 with the iconic legendary
battlegear shoulder unlocks).

Tier 14 (Grand Marshal / High Warlord) intentionally
omitted from presets — it's the legendary top-rank
that historically required dedicated 24/7 grinding.
Catalog supports tiers 1..14 in the schema; consumers
extend as needed.

Validator's most novel checks: per-(faction, tier)
tuple uniqueness — two ranks at the same tier for the
same faction would tie at runtime when the rank-
progression UI looks up "what's tier 5 for Alliance?"
Plus per-faction honor-threshold monotonicity — a
higher tier requiring less honor than a lower tier
would let players "downrank" by gaining honor, which
is a content authoring bug.

Format count 121 -> 122. CLI flag count 1276 -> 1281.
2026-05-10 03:08:27 -07:00
Kelsi
3e14b7b5b1 feat(editor): add WANV JSON round-trip (--export/--import-wanv-json)
Dual encoding for both WANV enums via the readEnumField
template: eventKind (int 0..6 OR 255 OR token "holiday"
/ "anniversary" / "doublexp" / "doublehonor" /
"petbattle" / "bgbonus" / "seasonalquest" / "misc") and
recurrenceKind (int 0..3 OR token "yearly" / "monthly"
/ "weekly" / "oneoff").

The polymorphic startDay field (1..31 day-of-month for
Yearly/Monthly/OneOff vs 0..6 weekday for Weekly) is
serialized as a plain int — operators editing JSON
need to know the recurrenceKind context to interpret
the value, which the validator already enforces on
import.

All 3 presets (holidays / weekly bonus / anniversary)
byte-identical roundtrip OK. CLI flag count 1274 ->
1276.
2026-05-10 03:04:51 -07:00
Kelsi
0df50f9f72 feat(editor): add WANV (Anniversary & Recurring Events) — 121st open format
Novel replacement for the implicit recurring-event
scheduler vanilla WoW encoded across the GameEvent SQL
table + per-holiday script hooks. Each entry binds one
calendar-driven recurring event (yearly holiday like
Hallow's End, monthly tribute day, weekly Double XP
Weekend, anniversary celebration) to its scheduling
rule and its payload (a spell buff applied to all
online players, a gift item granted on first event-
window login).

Eight eventKind values (Holiday / Anniversary /
DoubleXP / DoubleHonor / PetBattleWeekend /
BattlegroundBonus / SeasonalQuest / Misc) and four
recurrenceKind values (Yearly / Monthly / Weekly /
OneOff). The startDay field is polymorphic per
recurrenceKind: Yearly/Monthly/OneOff use it as
1..31 day-of-month, Weekly uses it as 0..6 weekday
(Sun..Sat) — the validator enforces both ranges per
kind.

Three preset emitters: makeStandardHolidays (5 yearly
holidays with realistic spell+item payload bindings —
Hallow's End spell 24710, Winter Veil 26157, Brewfest
42500, etc.), makeBonusEvents (4 weekly recurring
bonuses — Friday triple-day weekends and Saturday-
Sunday double-day pet-battle bonus), makeAnniversary
(3 game-launch anniversaries — WoW Nov 23 / TBC Jan 16
/ WotLK Nov 13 with overlapping celebration windows).

Validator's most novel checks combine calendar +
recurrence semantics: per-kind schedule validity (Weekly
startDay 0..6 weekday, durationDays <= 7 to prevent
self-overlap; Yearly/Monthly/OneOff startMonth 1..12,
startDay 1..31 with calendar sanity — Feb cap at 29,
Apr/Jun/Sep/Nov cap at 30 for "no Feb 30" / "no Apr 31"
errors).

Format count 120 -> 121. CLI flag count 1269 -> 1274.
2026-05-10 03:03:27 -07:00
Kelsi
695c22b274 feat(editor): add WCFG JSON round-trip (--export/--import-wcfg-json)
Dual encoding for both WCFG enums via the readEnumField
template: configKind (int 0..7 OR 255 OR token "xprate"/
"droprate"/"honorrate"/"restedxp"/"realmtype"/
"worldflag"/"performance"/"security"/"misc") and
valueKind (int 0..3 OR token "float"/"int"/"bool"/
"string"). restartRequired accepts bool or int.

The polymorphic value field is preserved on disk by
emitting ALL THREE value carriers (floatValue,
intValue, strValue) regardless of valueKind — only the
matching one is meaningful at runtime, but the others
must round-trip byte-identically. JSON also includes
the activeValue derived field rendering only the
meaningful one per kind, so operators editing the
sidecar see the right value front-and-center.

intValue uses int64_t for the trade-gold-cap and other
high-magnitude configs that exceed uint32 range.

All 3 presets (rates/perf/security with mixed value
kinds: Float, Int, Bool, String) byte-identical
roundtrip OK. CLI flag count 1267 -> 1269.
2026-05-10 02:59:53 -07:00
Kelsi
441ca0d139 feat(editor): add WCFG (Server Config) — 120th open format
Novel replacement for the worldserver.conf / mangosd.conf
flat-text configuration files vanilla server forks
shipped. Each entry binds one configId to its
polymorphic value via the valueKind enum (Float / Int /
Bool / String) — only the matching value field is
authoritative per entry.

The polymorphic value is the novel data shape: each
entry stores ALL three value carriers on disk
(floatValue float / intValue int64 / strValue string),
and valueKind picks which is meaningful at runtime. Bool
folds into intValue with strict 0/1 semantics. JSON
export reflects this: an activeValue derived field
renders the right form per kind so operators editing
JSON see only the relevant value.

Nine configKind values cover the full server-tunable
surface: XPRate / DropRate / HonorRate / RestedXP /
RealmType / WorldFlag / Performance / Security / Misc.
Each kind groups settings the server iterates by kind
at startup (all XPRate entries seed the per-class
experience matrix; all Security entries configure the
anti-cheat thresholds).

Three preset emitters: makeRates (4 vanilla baseline
rate multipliers with valueKind=Float), makePerformance
(4 server tuning configs mixing Int and Float kinds —
max creatures per cell, view distance yards, GC
interval seconds, etc.), makeSecurity (4 anti-cheat
configs FIRST format using valueKind=String for the
cheat-detection sensitivity preset name).

Validator's most novel checks are per-valueKind cross-
field consistency: Bool requires intValue strictly 0/1
(error), and warns on cross-field bleed (Float kind
with non-zero intValue means the int is silently
ignored at runtime but persists on disk). Plus name
uniqueness — server name-based config lookups would
be ambiguous otherwise.

Format count 119 -> 120 (multiple-of-10 milestone).
CLI flag count 1262 -> 1267.
2026-05-10 02:57:26 -07:00
Kelsi
9a734acb87 feat(editor): add WSKP JSON round-trip (--export/--import-wskp-json)
No enum coercion (all-numeric payload). Pct/raw dual
encoding for cloudSpeedX10: stored on disk as raw uint8
(0..255 = 0..25.5 mph) for byte-precision round-trip,
exported with both forms — cloudSpeedX10 (raw int,
authoritative) and cloudSpeedMph (float, derived).
Import accepts either form; pct conversion clamps to
0..255 with rounding.

This is the same dual-encoding pattern WHRD uses for
bonusQualityChance — get byte-identical round-trip from
binary AND human-friendly editing in JSON. cloudOpacity
stays as raw 0..255 since percent conversion would lose
precision (cloudOpacity=78 doesn't round-trip cleanly
through %.2f display).

All 3 presets (stormwind/arctic/hellfire) byte-
identical roundtrip OK. CLI flag count 1260 -> 1262.
2026-05-10 02:53:18 -07:00
Kelsi
0016b0d597 feat(editor): add WSKP (Sky Parameters) — 119th open format
Novel replacement for the LightParams.dbc + Light.dbc
pair vanilla WoW used to drive the per-zone diurnal sky
cycle. Each entry binds one (mapId, areaId,
timeOfDayHour) triplet to its sky-rendering parameters:
sky-dome zenith and horizon colors, sun angle and color,
fog start/end distances, cloud-layer opacity, and cloud
drift speed in tenths-mph.

The renderer interpolates between adjacent keyframes
when the in-game clock crosses an hour boundary, so a
4-keyframe set (Dawn/Noon/Dusk/Midnight) produces the
full diurnal cycle through linear interpolation. Servers
can author finer-grained keyframes (e.g. every 3 hours)
for smoother transitions.

Three preset emitters demonstrating the catalog's range:
makeStormwindDay (4 standard temperate keyframes from
lavender dawn through bright noon to deep blue-black
midnight), makeNorthrendArctic (4 cold steel-blue
keyframes with high-density ice fog peaking at the
midnight blizzard whiteout — minimum 30yd visibility),
makeOutlandHellfire (3 keyframes — no midnight, since
Outland's permanent gravitational anomaly from the
Twisting Nether keeps the sky lit; iconic crimson +
orange palette throughout).

Validator's most novel checks: per-(mapId, areaId,
timeOfDayHour) triple uniqueness — two keyframes at the
same hour for the same area would render in unstable
order during diurnal interpolation. Plus
fogStartYards >= fogEndYards (inverted falloff) error,
sunAngleDeg outside [0,360] warning (renderer wraps
modulo but suggests authoring confusion).

Format count 118 -> 119. CLI flag count 1255 -> 1260.
2026-05-10 02:51:23 -07:00
Kelsi
637a63e395 feat(editor): add WLMA JSON round-trip (--export/--import-wlma-json)
Dual encoding for both modeKind fields (the primary
modeKind AND the timeoutFallbackKind disconnect-fallback)
via the readEnumField template — both accept int 0..5
OR token "freeforall"/"roundrobin"/"masterloot"/
"needbeforegreed"/"personal"/"disenchant".
masterLooterRequired accepts bool or int.

thresholdQuality serializes as both int (authoritative,
0..7) AND derived qualityName string ("Poor"/"Common"/
"Uncommon"/"Rare"/"Epic"/"Legendary"/"Artifact"/
"Heirloom") for human-readable JSON. The qualityName is
informational only — int form is authoritative on
import.

All 3 presets (standard/raid/afk) byte-identical
roundtrip OK. CLI flag count 1253 -> 1255.
2026-05-10 02:47:52 -07:00
Kelsi
6fa81cf185 feat(editor): add WLMA (Loot Mode Policy) — 118th open format
Novel replacement for the implicit loot-distribution
rules vanilla WoW encoded across the GroupLoot system
(CMSG_LOOT_METHOD), the per-quality thresholds for
Need-roll triggering, and the master-looter permission
gates. Each entry binds one group-loot policy mode to
its kind (FFA / RoundRobin / MasterLoot / Need-Before-
Greed / Personal / Disenchant) plus quality threshold,
master-looter requirement, idle-skip seconds, and
disconnect-fallback policy.

Six modeKind values cover the full loot-distribution
surface. The thresholdQuality field uses the WIQR
quality tier convention (0=Poor through 7=Heirloom)
to gate Need-roll triggering — anything below threshold
auto-distributes via FFA-equivalent semantics.

The disconnect-fallback (timeoutFallbackKind) field is
unique to MasterLoot policies — if the master looter
disconnects mid-distribution, the policy auto-promotes
to the fallback mode for democratic recovery. Common
fallbacks: Need-Before-Greed (full roll system),
FreeForAll (fastest unblock).

Three preset emitters: makeStandard (4 5-man / casual
modes covering FFA farming, RoundRobin trash, NBG
Uncommon, MasterLoot Rare), makeRaidPolicies (3 raid
loot policies including MasterLoot Epic with NBG
fallback, Personal Loot, NBG Rare), makeAFKPrevention
(3 AFK-mitigating modes with idleSkipSec gates).

Validator's most novel check is per-kind consistency:
MasterLoot kind REQUIRES masterLooterRequired=1 (else
the policy contradicts itself — "Master Loot mode
without requiring a master looter"). Personal kind
warns if masterLooterRequired=1 (no-op flag). Tightened
fallback-to-self warning to fire ONLY for MasterLoot
where the field is meaningful — original version fired
falsely for FFA/Personal/RoundRobin where the leader-
disconnect scenario doesn't apply (caught + tightened
during smoke-test).

Format count 117 -> 118. CLI flag count 1248 -> 1253.
2026-05-10 02:46:26 -07:00
Kelsi
e7755c77c9 feat(editor): add WMAR JSON round-trip (--export/--import-wmar-json)
Dual encoding for markerKind (int 0..3 OR token
"raidtarget"/"worldmap"/"party"/"custom"). iconPath and
displayChar serialize as plain JSON strings for direct
authoring.

The displayChar field round-trip preserves exact byte
content — important since chat-overlay glyphs may
include short Unicode sequences (the Diamond marker
uses "<>" as a 2-char ASCII fallback for its glyph
rendering).

All 3 presets (raid/world/party) byte-identical
roundtrip OK. CLI flag count 1246 -> 1248.
2026-05-10 02:40:54 -07:00
Kelsi
42842958df feat(editor): add WMAR (Raid Marker Set) — 117th open format
Novel replacement for the hardcoded 8-marker raid set
vanilla WoW shipped (Star/Circle/Diamond/Triangle/Moon/
Square/Cross/Skull) plus world-map pin markers and
5-man party role markers. Each entry binds one marker
slot to its icon resource, single-character chat-overlay
glyph (for "{star}" chat-style links), and priority for
sort order in the marker-picker UI.

Four markerKind values (RaidTarget / WorldMap / Party /
Custom) cover the full marker-system surface. The chat-
overlay displayChar field enables the WoW-canonical
chat shortcuts: "{star}" gets rendered as a star icon
inline in chat, with "*" as the fallback glyph for
non-rich-text contexts (clipboard, log files, mod-
script handlers).

Three preset emitters: makeRaidTargets (8 canonical
raid markers in /raidicon priority order 0..7 with
their iconic colors and glyph mnemonics), makeWorldMap-
Pins (5 world-map pin markers — Pin/Flag/Crosshair/
Question/Compass), makeParty (4 role markers for group-
finder filtering — Tank/Healer/DPS/Caster).

Validator's most novel checks: per-(markerKind,
priority) tuple uniqueness — two markers at same kind+
priority would render in unstable picker UI order. Plus
RaidTarget priority > 7 warns (exceeds canonical 8-slot
/raidicon dispatch range; client keybind macros may not
reach the slot).

Format count 116 -> 117. CLI flag count 1241 -> 1246.
2026-05-10 02:39:55 -07:00
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