Commit graph

431 commits

Author SHA1 Message Date
Kelsi
be3a253dcc feat(pipeline): WPRT mage portal destinations catalog (129th open format)
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Novel replacement for the implicit portal-spell -> destination-
coordinate binding vanilla WoW carried in scattered pieces:
SpellEffects.dbc effect-71 (TELEPORT_UNITS) + per-spell hard-
coded destination tables in the server's SpellMgr +
AreaTrigger.dbc destination rows. Each WPRT entry binds one
Teleport/Portal spellId to its destination world coords,
faction-access gate, level requirement, and reagent
requirement.

PortalKind enum captures the canonical Teleport (self-only,
Rune of Teleportation, level 20) vs Portal (group, Rune of
Portals, level 40) distinction.

Three presets:
  --gen-prt-alliance   4 Alliance city portals (Stormwind /
                       Ironforge / Darnassus / Theramore) with
                       canonical spellIds 10059/11416/11419/
                       49361 and Rune of Portals reagent
  --gen-prt-horde      3 Horde city portals (Orgrimmar /
                       Undercity / Thunder Bluff)
  --gen-prt-teleports  3 self-teleports paired across factions
                       (Teleport: Stormwind/Ironforge Alliance +
                       Teleport: Orgrimmar Horde) — illustrates
                       the Teleport-vs-Portal kind distinction
                       with proper reagent (Rune of Teleportation
                       17031 NOT Rune of Portals 17032)

Validator catches: id+spellId+destination required,
factionAccess/portalKind range, no duplicate portalIds, no
duplicate spellIds (cast-handler conflict). Warns on
levelRequirement < 20 (vanilla mage cannot unlock), Portal kind
without Rune of Portals (17032), Teleport kind without Rune of
Teleportation (17031), and duplicate destination names (could be
legitimate Teleport+Portal pair OR copy-paste bug — the editor
flags both).

Format count 128 -> 129. CLI flag count 1355 -> 1362.
2026-05-10 04:01:42 -07:00
Kelsi
85ac2e0248 feat(editor): WTSC JSON round-trip closure
Adds --export-wtsc-json / --import-wtsc-json with the established
readEnumField template factoring int+name dual encoding for both
vehicleType ("taxi"/"zeppelin"/"boat"/"mount") and factionAccess
("both"/"alliance"/"horde"/"neutral"). Float coordinates
(originX/Y, destinationX/Y) preserved bit-for-bit through JSON.

All 3 presets (zeppelins/boats/taxis) byte-identical binary
roundtrip OK including BootyBay<->Ratchet Neutral cross-faction
boat coords (-14305.f, 570.f -> -984.f, -3835.f).

Live-tested scheduling-overflow detection: hand-mutated routeId=2
(OG<->Grom'Gol travel=90s capacity=40) interval to 60s, validator
correctly errored: "departureIntervalSec=60 < travelDurationSec=90
with finite capacity — vehicle pool overflow (next zeppelin
departs before prior arrives)".

CLI flag count 1353 -> 1355.
2026-05-10 03:57:15 -07:00
Kelsi
12e77e69ce feat(pipeline): WTSC transit schedule catalog (128th open format)
Novel replacement for the implicit taxi/zeppelin/boat scheduling
that vanilla WoW drove from a tangle of TaxiNodes.dbc +
TaxiPath.dbc + per-zeppelin GameObject scripts + hard-coded
transport interval timers in the server's MapManager. Each WTSC
entry binds one scheduled passenger route to its origin /
destination coords, vehicle type (Taxi/Zeppelin/Boat/Mount),
departure interval, in-flight duration, capacity, and faction-
access gate.

Initially designed with magic 'WTRN' but discovered collision
with existing trainers catalog (also WTRN) — renamed to 'WTSC'
(Transit SChedule) and updated all CLI flags.

Three presets:
  --gen-trn-zeppelins  3 vanilla Horde zeppelin routes
                       (OG<->UC 240s interval, OG<->Grom'Gol,
                       UC<->Grom'Gol)
  --gen-trn-boats      3 vanilla boat routes (Auberdine<->
                       Stormwind Alliance, Menethil<->Theramore
                       Alliance, BootyBay<->Ratchet Neutral
                       cross-faction)
  --gen-trn-taxis      3 taxi gryphon/wyvern routes — capacity=0
                       indicates solo gryphon ride

CRITICAL scheduling invariant validator catches: when capacity > 0
the departureInterval MUST be >= travelDuration. A zeppelin with
interval=60s + travel=90s with capacity=40 would overflow the
vehicle pool — next zeppelin departs before prior arrives. Solo
gryphon (capacity=0) is exempt because each ride is independent.

Validator also catches: id+name+origin+destination required,
vehicleType/factionAccess range, zero intervals/travel, duplicate
routeIds, duplicate route names. Warns on same-map routes
(originMapId == destinationMapId) — preset taxi route Crossroads
to Razor Hill triggered this warning in smoke-test (both in
Kalimdor mapId=1, intentional).

Format count 127 -> 128. CLI flag count 1346 -> 1353.
2026-05-10 03:54:39 -07:00
Kelsi
2a28d3c1cd feat(editor): WPHM JSON round-trip closure
Adds --export-wphm-json / --import-wphm-json with the established
readEnumField template factoring int+name dual encoding for the
movementState enum ("idle"/"walk"/"run"/"swim"/"fly"/"sit"/
"mount"/"death").

Both encoding paths verified live: stripped the int field from
exported JSON and re-imported using only the string token —
result byte-identical to the int-keyed form. All 3 presets
(human / orc / undead) byte-identical binary roundtrip OK
preserving Undead shambling variant 38 on Run state and slower
swim transition (400ms vs 350ms for living races).

CLI flag count 1344 -> 1346.
2026-05-10 03:47:03 -07:00
Kelsi
949a6e0182 feat(pipeline): WPHM player movement-to-animation map (127th open format)
Novel replacement for the implicit movementState->animation
binding that vanilla WoW baked into per-race M2 model files.
Each WPHM entry binds one (raceId, genderId, movementState)
tuple to a base M2 animation sequence id, an optional variant
sequence (drunk-walk, wounded-run), and a blend transition
duration in milliseconds.

8-state machine: Idle / Walk / Run / Swim / Fly / Sit / Mount /
Death. Three presets each emit the full 16 bindings (M+F):
  --gen-phm-human   16 bindings with drunk-walk variant on Walk
  --gen-phm-orc     16 bindings with AttackRun variant on Run
                    for war-stance flavor
  --gen-phm-undead  16 bindings with canonical shambling variant
                    (anim 38) on Run for low-health renderer
                    override + slower swim transition (undead are
                    awkward in water)

Validator catches: id required, raceId 1..10, genderId 0..1,
movementState 0..7, no duplicate mapIds, no duplicate
(race,gender,state) triples (renderer dispatch ambiguity),
baseAnimId=0 forbidden on non-Idle states (model would freeze
when entering that state). Warns on variantAnimId==baseAnimId
(no-op overhead) and transitionMs > 2000 (would feel like
animation hang).

Format count 126 -> 127. CLI flag count 1337 -> 1344.
2026-05-10 03:44:31 -07:00
Kelsi
e652f8595d feat(editor): WSPK JSON round-trip closure
Adds --export-wspk-json / --import-wspk-json. spellIds serialize
as JSON int arrays preserving spellbook display order (top-to-
bottom in tab). All 3 presets (warrior/mage/rogue) byte-identical
binary roundtrip OK including Mage Frost tab [116, 122, 10] —
Frostbolt rank 1 still first, Frost Nova second, Blizzard third
after roundtrip.

Importer also restores className via implicit lookup from
classId on the export side, so a hand-edited JSON only needs
classId int — className field is informational.

CLI flag count 1335 -> 1337.
2026-05-10 03:39:52 -07:00
Kelsi
6d9d00fbb9 feat(pipeline): WSPK spell pack catalog (126th open format)
Novel replacement for the implicit per-class spellbook layout
that vanilla WoW derived from SkillLineAbility.dbc + the hard-
coded per-spec tab order baked into the client UI. Each WSPK
entry binds one (classId, tabIndex) pair to an ordered list of
spellIds shown in that spellbook tab.

Three presets seeded with canonical vanilla low-rank spellIds:
  --gen-spk-warrior  4 tabs (General + Arms/Fury/Protection)
                     including Charge, Mortal Strike,
                     Bloodthirst, Shield Block
  --gen-spk-mage     4 tabs (General + Arcane/Fire/Frost)
                     including Frostbolt rank 1 (spellId 116)
                     — the canonical "every mage starts here"
  --gen-spk-rogue    4 tabs (General + Assassination/Combat/
                     Subtlety) with poison + lethality picks

Validator catches: packId+tabName required, classId in 1..11,
tabIndex in 0..3, no duplicate packIds, no duplicate
(classId,tabIndex) pairs (spellbook UI dispatch tie), no zero
spellIds, no duplicate spellIds within any single tab (would
render twice in spellbook). Warns on classId 6 and 10 (vanilla
PlayerClass DBC gaps) and on empty tabs (player would see a
blank spellbook tab).

Format count 125 -> 126. CLI flag count 1328 -> 1335.
2026-05-10 03:37:36 -07:00
Kelsi
fa30db7ae1 feat(editor): WMOD JSON round-trip closure
Adds --export-wmod-json / --import-wmod-json. Variable-length
dependency arrays serialize as JSON int arrays, enabling
hand-edits of dependency graphs. All 3 presets (std-addons /
ui-replacement chain Bartender4->ElvUI->SuperOrders / utility)
byte-identical binary roundtrip OK.

Cycle detection live-tested: hand-mutated ui.wmod.json to add
Bartender4 -> SuperOrders dep, re-imported, validator emits
"dependency cycle detected: 10 -> 12 -> 11 -> 10 — addon-loader
would deadlock" with the full back-edge path extracted.

CLI flag count 1326 -> 1328.
2026-05-10 03:33:29 -07:00
Kelsi
9df1fa39cd feat(pipeline): WMOD addon manifest catalog (125th open format)
Novel replacement for vanilla per-addon TOC (.toc) text files
scattered across Interface/AddOns/. Each WMOD entry binds one
addon to display metadata (name / description / version / author),
client-build gate (minClientBuild), persistence + lazy-load
flags (requiresSavedVariables / loadOnDemand), and required +
optional dependency lists.

Three presets:
  --gen-mod        4 vanilla-era addons (Recount standalone +
                   Atlas standalone + Auctioneer optional-dep
                   on Atlas + Questie standalone)
  --gen-mod-ui     3 UI-replacement chain (Bartender4 root ->
                   ElvUI required-dep on Bartender4 -> SuperOrders
                   required-dep on ElvUI). Exercises the chained
                   required-dep resolution path.
  --gen-mod-util   3 standalone utility addons (XPerl, Decursive,
                   GearVendor loadOnDemand) — empty-deps baseline.

Validator catches: id+name+version required, duplicate addonIds,
duplicate addon names (load-order ambiguity), self-dependency
(load deadlock), missing required-dep addonId, full DFS cycle
detection on required deps (deadlock at load — extracts the
back-edge path so the user can see the loop). Warns on optional
self-dep (no effect, prune) and on minClientBuild < 4500
(below vanilla floor — likely typo).

Format count 124 -> 125. CLI flag count 1319 -> 1326.
2026-05-10 03:31:21 -07:00
Kelsi
09ad03ca34 feat(editor): WGCH JSON round-trip closure
Adds --export-wgch-json / --import-wgch-json with the established
readEnumField template factoring int+name dual encoding for both
channelKind ("global"/"realmzone"/"faction"/"custom") and accessKind
("publicjoin"/"inviteonly"/"autojoinonzone"/"moderated").

All 3 presets (std-channels / roleplay / admin) byte-identical
binary roundtrip OK including the zoneDefaultMapId=1519 Stormwind
auto-join binding on TradeStormwind. Validators pass on round-
tripped binaries.

CLI flag count 1317 -> 1319.
2026-05-10 03:26:29 -07:00
Kelsi
c7d85fc598 feat(pipeline): WGCH global chat channel catalog (124th open format)
Novel replacement for vanilla ChatChannels.dbc + the per-server
zone-default chat-join behavior. Each WGCH entry binds one chat
channel to its access policy: PublicJoin, InviteOnly,
AutoJoinOnZone (with zoneDefaultMapId), or Moderated. Entries
also carry channelKind (Global/RealmZone/Faction/Custom),
passwordRequired, levelMin, maxMembers cap, topic-mod-only flag,
and an icon RGBA color.

Three presets:
  --gen-gch        4 standard server channels (LookingForGroup,
                   World, Trade auto-join Stormwind, General)
  --gen-gch-rp     4 RP channels (RP_OOC public, RP_IC moderated
                   200-cap, RP_Forum invite-only 50-cap, RP_Events
                   password-protected)
  --gen-gch-admin  3 moderator-only channels (GMTraffic, AuditLog,
                   Backstage — all password-gated)

Validator catches: id+name required, channelKind/accessKind
range, duplicate channelIds, duplicate channel names (which
would route /join ambiguously), AutoJoinOnZone with
zoneDefaultMapId=0 (auto-join trigger would never fire). Warns
on dead zoneDefaultMapId set with non-AutoJoin kind.

Format count 123 -> 124. CLI flag count 1290 -> 1317.
2026-05-10 03:23:39 -07:00
Kelsi
cef571bb45 feat(editor): add WLAN JSON round-trip (--export/--import-wlan-json)
Dual encoding for both WLAN enums via the readEnumField
template: languageCode (int 0..10 OR token "enUS" /
"enGB" / "deDE" / "esES" / "frFR" / "itIT" / "koKR" /
"ptBR" / "ruRU" / "zhCN" / "zhTW") and namespace (int
0..7 OR token "ui" / "quest" / "item" / "spell" /
"creature" / "tooltip" / "gossip" / "system").

UTF-8 round-trip preserved through JSON: nlohmann::json
escapes multibyte sequences as \uXXXX surrogate pairs
on export and decodes them back to bytes on import,
producing byte-identical binary output. The Korean
"취소" and Chinese "取消" UI translations from the
preset round-trip cleanly through the binary -> JSON ->
binary cycle.

All 3 presets (ui-basics / quest-sample / tooltip-set)
byte-identical roundtrip OK including the multibyte
strings. CLI flag count 1288 -> 1290.
2026-05-10 03:14:52 -07:00
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