Sixty-seven failures arrived with their reasons scattered across thousands
of log lines, and one broken script takes down every file referencing it,
so what matters is seeing them together. Collect a reason per file and
print them as one block; carry the real cause up through includes and
referenced scripts rather than naming the file that referenced them.
WOWEE_LOAD_FRAMEXML now implies WOWEE_LUA_API_FALLBACK. FrameXML cannot
get through its own load without it, so two switches where one is useless
alone was only a way to be handed a wall of failures for setting the
obvious one.
The compile checker counted a file it could not parse as neither pass nor
fail, reporting a clean run over files that never loaded.
Asked Lua itself whether the output compiles, one real file at a time, instead
of only checking its shape against cases I had thought of. That found two faults
the unit tests could not have, and both lose a whole file rather than degrading.
An empty function attribute — <OnMouseWheel function=""/> — passed the
present-or-not check and emitted SetScript("OnMouseWheel", ), which is a syntax
error. Six files carried one.
And every widget was declared as a local. Lua allows 200 per function, and a
large file declares far more: FriendsFrame and InterfaceOptionsPanels both went
over, and the chunk simply refuses to compile. Temporaries now live in one
table, so there is a single local however many widgets a file declares.
140 of 140 FrameXML files now produce Lua that compiles, up from 132 before the
first of these and considerably fewer before the second. The checker is kept as
a tool, because the question it asks is worth being able to ask again.
Before writing more of it, worth knowing how much there is. FrameXML calls 4,316
distinct globals, which sounds impossible until you subtract the 2,796 it
defines itself as it loads. Another 343 are already provided. What is genuinely
missing is 1,218 functions across 2,537 call sites.
The shape matters more than the total. The most-used missing name has 47 uses
and it drops to about two each almost immediately — a very flat tail. So this is
not a wall of important work; it is a long list of functions that mostly need to
exist and return something sane, with a few dozen that need real data behind
them.
Kept as a script rather than a note because the number only means anything if it
can be taken again, and it is the obvious way to watch the gap close.
Add tools/upscale_textures.py: selects BLPs (foliage preset or filters),
decodes via blp_convert, upscales with realesrgan-ncnn-vulkan (ImageMagick
fallback), and writes PNG sidecars the client already prefers over BLPs.
Generated files are manifest-tracked and revertible with --clean.
The DBC loader skipped binary DBCs in Data/db/ on Linux because
filenames are lowercase but code requested mixed-case names. This
caused the corrupted CSV fallback to be used, where nearly all
integer fields (including GeosetGroup) were misclassified as strings
and exported as string-block lookups instead of numeric values.
Add case-insensitive filename matching for the Data/db/ directory
scan and prioritize expansion overlay DBCs. Fix kGeosetBareShins
from 503 to 501, add per-race lowestInGroup fallback (Gnome, Tauren),
and use DBC layout JSON for field indices in game_screen_hud. Exclude
field 0 (record ID) from string detection in both dbc_to_csv and the
asset extractor to prevent future CSV corruption.
Login-critical opcodes (AUTH_CHALLENGE, AUTH_RESPONSE, CHAR_ENUM, etc.)
now fall back to hardcoded wire values when the opcode table fails to
load, preventing the "Unhandled world opcode: 0x1ec" symptom that
blocked character list retrieval.
OpcodeTable::loadFromJson() no longer wipes the working table on
failure — it loads into temporaries and only swaps on success.
Integrity hash now takes clientBuild: Classic-era DLLs (fmod.dll,
ijl15.dll, dbghelp.dll, unicows.dll) are only required for builds
<=6005 or Turtle; TBC/WotLK clients hash only the .exe.
The compiler-warning sweep surfaced one real behavioral bug and a
handful of cosmetic noise:
REAL BUG — editor_app.cpp:1069 misleading-indentation:
if (objectPlacer_.objectCount() > 0 || npcSpawner_.spawnCount() > 0)
objectsDirty_ = true; autoSavePendingChanges_ = true;
The trailing `autoSavePendingChanges_ = true;` was OUTSIDE the if, so
the auto-save flag was set unconditionally on every reload — meaning
zones with zero objects/NPCs were getting needlessly auto-saved. Wrap
both writes in braces so they share the guard.
Cosmetic / noise — also fixed so the warning channel stays useful:
- cli_mesh_io.cpp:193,194,333 — same misleading-indentation pattern
(`if (a) x; if (b) y;` chained on one line). Replaced with std::clamp.
- cli_for_each.cpp:26,114,116 — `\\` at end of `//` comment was
treated as line-continuation, silently extending the comment to the
next line. Replaced the trailing-backslash convention with literal
"(continued)" markers in the example shell command.
- world_map/input_handler.cpp:14 — unused `cosmicEnabled` parameter
marked [[maybe_unused]].
- wowee_player_spawn_profiles.cpp:75 — unused CLS_PALADIN constant
marked [[maybe_unused]] (kept to document the bit layout).
- wowee_crafting_recipes.cpp — three unused `using R = …` aliases
removed.
- cli_data_tree.cpp:609, cli_format_validate.cpp:950 — unused argc
parameters marked [[maybe_unused]] in handlers I touched recently.
CMakeLists.txt: editor's CLI handlers all share `(int& i, int argc,
char** argv)` so they can plug into a function-pointer table; many
handlers don't reference argc. Added -Wno-unused-parameter to the
editor target rather than littering 30+ handler signatures with
[[maybe_unused]] noise. Main wowee target keeps the warning enabled.
Result: clean build with -Wall -Wextra -Wpedantic, zero warnings in
our code (the remaining ones are all in extern/ third-party headers).
Closes the 5 remaining cpp/command-line-injection alerts plus 3
cpp/integer-multiplication-cast-to-long and 1 cpp/uncontrolled-arithmetic
in tools/editor/. (The other open high alerts are all in extern/
third-party headers — imgui, stb_image, miniaudio — and are out of
scope for us to patch.)
Critical (cmd-injection) → shell-free runChild() helper:
- cli_zone_packs.cpp:41,175,182 (+ a 4th site at line 235 that the
alert tooling missed). runSilently() refactored to take argv0+args.
- cli_audits.cpp:68 — per-zone `--validate-…` self-invocation.
- cli_gen_audio.cpp:386 — per-tone `--gen-audio-tone` self-invocation.
- editor_ui.cpp:3038 — manifest "open in default app" used a shell
concat (open / start / xdg-open). Now uses cli_subprocess::runChild
with the platform binary directly.
High (int-mul overflow) → widen one operand to size_t:
- wowee_terrain.cpp:272 — `resolution * resolution * 3` for the zone
map pixel buffer.
- terrain_editor.cpp:1848,1859 — `w * h` for stbi_load{,_16} heightmap
resize loops; precomputed pixelCount and switched the loop counter
to size_t.
High (uncontrolled-arithmetic) → bounded increment:
- editor_ui.cpp:987 — noise-seed `>>` button incremented `int` without
bound. Clamp to INT_MAX.
CodeQL flagged 21 cpp/command-line-injection alerts in tools/editor/.
All matched the same pattern: build a shell command string from
argv[0] + a user-supplied path, then std::system() it. Even though
the threat model (user invokes their own CLI on their own machine)
makes the alert mostly academic, the std::system path is also
fragile — paths with spaces, quotes, or shell metacharacters
silently break.
Add tools/editor/cli_subprocess.{hpp,cpp} exposing a single
runChild(argv0, args, quiet=false) that uses posix_spawn on POSIX
and CreateProcess on Windows. No shell, argv passed verbatim,
optional stdout/stderr redirect to /dev/null (NUL on Windows).
Refactor 14 call sites across cli_convert.cpp, cli_data_tree.cpp,
cli_format_validate.cpp, cli_items.cpp, cli_random.cpp,
cli_repair.cpp, cli_spawn_audit.cpp.
Also fix two cpp/integer-multiplication-cast-to-long alerts:
- cli_gen_texture.cpp:3049 — seeds.reserve grid-size product
- cli_convert_single.cpp:224 — vector size for DBC record block
Both now widen one operand to size_t before multiplying.
cli_introspect.cpp:
Replace `stdout = tmp;` / `stdout = old;` with an RAII fd-level
redirect via dup/dup2 (POSIX) or _dup/_dup2 (Windows). The C
standard does not require `stdout` to be an assignable lvalue;
Apple/Windows clang reject it, breaking the macOS arm64 and Windows
x86-64 / arm64 builds.
tests/CMakeLists.txt:
Link test_editor_units against glm::glm when the target is
available. tools/editor/npc_spawner.hpp transitively pulls in
<glm/glm.hpp>, which fails on the macOS runner where glm is found
via find_package but the test target wasn't picking up the include
paths.
The format magic table (cli_format_table.cpp) and the long-form list
(cli_list_formats.cpp) carried stale extensions for six catalogs that
diverged from what the pipeline actually writes:
WTRN .wtrr -> .wtrn (--info-wtrr -> --info-wtrn)
WGSP .wgoss -> .wgsp (--info-wgoss -> --info-wgsp)
WTIT .wttl -> .wtit (--info-wttl -> --info-wtit)
WSEA .wevt -> .wsea (--info-wevt -> --info-wsea)
WMOU .wmnt -> .wmou (--info-wmnt -> --info-wmou)
WPCD .wcnd -> .wpcd (--info-wcnd -> --info-wpcd)
Each pipeline impl appends its real extension in save() — .wtrn /
.wgsp / .wtit / .wsea / .wmou / .wpcd — and the per-format CLI
handlers register --info-wtrn / --info-wgsp / etc. The two tables
were the only places still pointing at the old names, so:
--info-magic on a real .wtrn was directing users to --info-wtrr
--rename-by-magic would have renamed .wtrn -> .wtrr, breaking
bulk-validate and every other table-driven tool
--bulk-validate skipped these catalogs entirely because
deriveValidateFlag built --validate-wtrr / --validate-wcnd / etc.
which no handler registers
Discovered while running an end-to-end audit across all 146 open
formats — 7 catalogs were unreachable through the magic table. Audit
now reports 139 ok / 6 skipped (asset formats + engine-loaded) / 1
false-positive (--gen-mesh needs additional args).
Adds --export-wlnk-json / --import-wlnk-json mirroring the WCFR/WCAM
closure pattern. linkKind round-trips as both int and name string
("item"/"quest"/"spell"/"achievement"/"talent"/"trade").
requireServerLookup encodes as bool but accepts bool OR uint8 on
import. colorRGBA (uint32 quality color), linkTemplate (sprintf with
%d/%s placeholders), tooltipTemplate, and iconRule preserved
byte-identical.
All 3 presets (lnk-std / lnk-talent / lnk-quality) byte-identical
binary round-trip OK including the 4-placeholder Item template and
the 9-placeholder Achievement template.
Closes the audit gap that pre-existed for the 146th open format —
every catalog with --info/--validate now has matching JSON sidecar
flags.
CLI flag count 1508 -> 1510.
test_camera was the only Catch2 target missing the `if(TARGET glm::glm)`
link block — every other test linked glm but this one didn't, so the
macOS arm64 builder failed to find `glm/glm.hpp` from camera.hpp.
Five fopen call sites in tools/editor/cli_* passed
fs::directory_entry::path().c_str() — that's wchar_t* on Windows.
Routed them through `.string().c_str()` so MSYS2 GCC and clang both
accept the const char* parameter.
Novel replacement for the implicit chat-link format templates
vanilla WoW carried in client-side LUA — each link kind (item /
quest / spell / achievement / talent / trade-skill) had a hard-
coded sprintf template baked into ChatFrame_OnHyperlinkClick
with no formal data-driven extension point. Each WLNK entry
binds one hyperlink kind to its sprintf-style chat-link
template (with %%d/%%s placeholders for link parameters),
tooltip-popup template, quality color (RGBA), icon source
rule, and server-lookup requirement flag.
Three presets covering the link-kind spectrum:
--gen-lnk-std 4 standard hyperlink kinds (Item with
classic 4-rune-slot template / Quest
yellow / Spell white / Achievement with 9
placeholders for criteria-progress data)
--gen-lnk-talent 2 less-common kinds (Talent green for
passive enhancements / Trade-skill recipe
orange with server lookup for ingredients)
--gen-lnk-quality 3 Item-kind variants distinguished by
quality color (Common gray 0x9D9D9D / Epic
purple 0xA335EE / Legendary orange
0xFF8000) — chat composer picks variant by
item quality at link time
Validator catches: id+name+linkTemplate required, linkKind
0..5, no duplicate linkIds. CRITICAL: linkTemplate MUST contain
at least one %%d/%%s placeholder — composer would emit a static
string regardless of input (every link rendering identically
regardless of which item/spell/quest was clicked). Walks the
template character-by-character counting valid format
specifiers (%%d, %%s, %%u, %%i, %%x, %%X, with %%%% literal
double-percent excluded). Warns on > 12 placeholders (unusual,
verify intentional), colorRGBA=0 (fully transparent — link text
invisible), and requireServerLookup=true with empty
tooltipTemplate (server data has nowhere to render).
Format count 145 -> 146. CLI flag count 1508 -> 1515.
Adds --export-wcfr-json / --import-wcfr-json with the established
readEnumField template factoring int+name dual encoding for both
outputStatKind ("ap"/"sp"/"crit"/"dodge"/"parry"/"hit"/
"spellcrit"/"haste") and inputStatKind ("str"/"agi"/"sta"/
"int"/"spi"). Fixed-point conversion ratios (uint32) preserved
through JSON.
All 3 presets (warrior/mage/rogue) byte-identical binary
roundtrip OK including the level-gated Warrior parry formula
(levelMin=30, levelMax=60).
Live-tested applicability-quad duplicate validator: hand-added
a copy of the Rogue Agi->Crit formula with a new formulaId 99
but the SAME (output=crit, input=agi, classMask=0x10, levelMin=
1) quad. Validator correctly errored: "duplicate (output=crit,
input=agi, classMask=0x16, levelMin=1) — runtime stat-compute
would apply both formulas, doubling the contribution". Catches
the class of stat-balance bugs where a copy-paste of one
formula would silently double an entire stat-conversion path
without breaking the build.
CLI flag count 1506 -> 1508.
Originally attempted WMRR (instance lockouts) for this slot but
hit a name-collision with the existing WHLD catalog at the same
file path. Reverted plumbing additions and pivoted to WCFR — a
distinct, complementary domain not covered by any existing
format.
Novel replacement for the per-stat-conversion ratios vanilla
WoW carried in the gtChanceTo*.dbc gameobject tables + the
per-class hard-coded constants in the server's StatSystem (the
"Strength gives 2 AP for Warriors but 1 AP for Mages" rule was
hard-coded; the "1 Agility = 1 Crit% for Hunters but 0.5 Crit%
for Druids" was hard-coded). Each WCFR entry binds one
(outputStat, inputStat, classMask, level-band) tuple to its
conversion ratio in fixed-point units (fp_x100: 100 = 1.0).
Three presets demonstrating per-class ratio variation:
--gen-cfr-warrior 4 Warrior formulas (Str->AP 2.0 / Agi->
Crit 0.05% / Agi->Dodge 0.05% / Str->
Parry 0.04% gated to level 30+)
--gen-cfr-mage 3 Mage formulas (Int->SpellPower 1.0 /
Int->SpellCrit 0.02% / Spi->OOC SpellPower
0.50)
--gen-cfr-rogue 4 Rogue formulas (Str->AP 1.0 / Agi->AP
1.0 / Agi->Crit 0.07% / Agi->Dodge 0.07%)
— Rogue Agi->Crit ratio (7) significantly
better than Warrior's (5), demonstrating
the per-class differentiation the format
captures
Validator catches: id+name required, outputStatKind 0..7,
inputStatKind 0..4, no duplicate formulaIds, no zero
conversionRatio (no-op formula). CRITICAL: no duplicate
(output, input, classMask, levelMin) quad — runtime stat-
compute would apply both formulas, doubling the contribution.
levelMax >= levelMin when set. Warns on conversionRatio > 100x
(likely units-mismatch typo from porting a percentage table
without dividing by 100).
Format count 144 -> 145. CLI flag count 1499 -> 1506.
Adds --export-wcam-json / --import-wcam-json with the established
readEnumField template factoring int+name dual encoding for
purposeKind ("cinematic"/"combat"/"mounted"/"vehicle"/
"cutscene"/"photomode"). Float fields (FOV, distance, pitch,
yaw, shoulder offset) preserved bit-for-bit through JSON.
All 3 presets (combat/mounted/cinematic) byte-identical binary
roundtrip OK including the Cinematic Portrait preset's offbeat
yaw=15deg + 35deg telephoto + head-bone tracking combination.
Live-tested gimbal-lock validator: hand-mutated Cinematic
Establishing preset pitch from -30 to -95 (beyond the -89
gimbal-lock limit). Validator correctly errored:
"pitchDegrees=-95.000000 gimbal-locks the camera (must be
within (-89, +89))". Catches the class of cinematic-camera bugs
where a pitch of ±90 mathematically aligns with the world up
vector and causes the camera basis to collapse.
CLI flag count 1497 -> 1499.
Novel format covering what vanilla WoW handled with hard-coded
camera profiles in the client's CameraMgr (the standard third-
person camera, the flight-path camera, vehicle cameras and
cinematic cameras were all bespoke C++ classes with no data-
driven extension point). Each WCAM entry binds one camera
preset to its FOV, distance, pitch/yaw offsets, shoulder offset,
focus-bone tracking target (M2 bone index, 0xFFFF = follow root),
motion damping curve (0=instant follow / 255=maximum lag), and
intended purpose (Cinematic / Combat / Mounted / Vehicle /
Cutscene / PhotoMode).
Three presets covering common camera scenarios:
--gen-cam-combat 3 Combat variants (default 75deg / wide
ranged 90deg / tight melee 60deg
shoulder-cam tracking chest bone) with
low motion damping (8-15) for responsive
tracking
--gen-cam-mounted 2 Mounted variants (ground 80deg pulled-
back / flying 85deg high-pitch wing-frame)
with medium-high damping (60-90) for
smooth turning
--gen-cam-cinematic 3 Cinematic angles (over-shoulder
dialogue 50deg telephoto / wide
establishing 100deg / portrait 35deg
3/4-face composition with yaw offset and
head-bone tracking) with high damping
(180-220) for film-quality motion
Validator catches: id+name required, purposeKind 0..5, no
duplicate presetIds, FOV in (0,180) (zero/negative makes no
sense, >=180 inverts the view frustum), distanceFromTarget >= 0
(negative places camera in front of target). CRITICAL: pitch
within (-89, +89) — beyond gimbal-locks the camera. Warns on
FOV outside 30..120 player-comfort range (motion-sickness risk
or extreme telephoto compression), distanceFromTarget < 0.5m
(clips into model), and yawOffsetDegrees beyond ±180° (wraps to
smaller equivalent angle — simplify).
Format count 143 -> 144. CLI flag count 1490 -> 1497.
Adds --export-wcmd-json / --import-wcmd-json with the established
readEnumField template factoring int+name dual encoding for both
minSecurityLevel ("player"/"helper"/"moderator"/"gamemaster"/
"admin") and category ("info"/"movement"/"communication"/
"admincmd"/"debug"). Aliases array serialized as JSON string
array.
All 3 presets (basic/movement/admin) byte-identical binary
roundtrip OK including the movement preset's multi-alias
commands (/stand has aliases ["standup", "su"]).
Live-tested flat-namespace collision validator: hand-mutated
/stand (cmdId=11) to add 'sit' as an alias, colliding with the
canonical /sit command. Validator correctly errored: "alias
'sit' collides with another command name or alias — chat
parser would dispatch ambiguously". Catches the class of typo
bugs where a new alias would shadow an existing command, with
the failure surface being silent dispatch ambiguity rather
than an error.
CLI flag count 1488 -> 1490.
Novel replacement for the implicit slash-command registry
vanilla WoW carried in the client's ChatFrame.lua + server-side
per-command CommandHandler hooks (no formal data-driven catalog;
commands were registered ad-hoc with hard-coded security checks
scattered across LevelMgr / WorldMgr / CharacterMgr). Each WCMD
entry binds one command name to its aliases, minimum security
level required, argument schema string, help text, per-player
throttle in ms, hidden flag (for debug-only commands), and
category.
Three presets covering the security-tier spectrum:
--gen-cmd-basic 4 standard player Info commands (/who
/played /time /ginfo) at Player security
with no throttle
--gen-cmd-movement 3 emote-style Movement commands (/sit
/stand /sleep) with short typing-speed
aliases ("sd" / "su" / "sd")
--gen-cmd-admin 3 GameMaster-only Admin commands
(/announce 5s throttle / /kick 2s
throttle / /ban 10s throttle —
demonstrating per-command rate-limiting
to prevent admin-spam abuse)
Validator catches: id+command required, minSecurityLevel 0..4,
category 0..4, no duplicate cmdIds. CRITICAL: command names
AND aliases share one flat namespace (chat parser dispatches
uniformly by typed string) — duplicate name across canonical+
aliases errors. Warns on uppercase chars in names (parser is
case-insensitive but convention is lowercase), Admin-category
command at Player/Helper security level (likely security
misconfiguration — admin commands usually require GameMaster+),
throttleMs > 60000 (likely ms-vs-s units typo — 60+ second
throttle is nearly unusable), self-alias (canonical already
matches), and empty helpText (/help would show the command
without description).
Format count 142 -> 143. CLI flag count 1481 -> 1488.
Adds --export-wtur-json / --import-wtur-json with the established
readEnumField template factoring int+name dual encoding for
triggerEvent ("login"/"zoneenter"/"levelup"/"itempickup"/
"skilltrain"). Title/body/targetUIElementName multibyte text
preserved through JSON.
All 3 presets (newbie/levelup/bg) byte-identical binary roundtrip
OK including the BG preset's per-mapId trigger gating
(AV=30/WSG=489/AB=529).
Live-tested readability validator: hand-mutated Welcome step
(tutId 1) hideAfterSec from 30 to 3 in JSON. Validator
correctly errored: "hideAfterSec=3 is below 5s — popup vanishes
before the player can read it". Catches the class of UX bugs
where overly-aggressive auto-dismiss timers make tutorials
unhelpful (player sees a flash they can't process).
CLI flag count 1479 -> 1481.
Novel format covering what vanilla WoW had as a hard-coded LUA
tipbox sequence (TutorialFrame.xml + Tutorial.lua client-side
with no data-driven extension point). Each WTUR entry binds
one tutorial step to a trigger event (Login / ZoneEnter /
LevelUp / ItemPickup / SkillTrain), an ordered stepIndex within
that trigger group, a title + body for the popup, optional UI-
element name to highlight, and a hide-after auto-dismiss timer.
Three presets covering the most common tutorial scenarios:
--gen-tut-newbie 5 first-login steps (Welcome / Camera /
Interact NPCs / OpenQuestLog / OpenBags)
with 30s auto-dismiss and UI-element
highlight names ("MovementHint",
"QuestLogButton", "BagButton")
--gen-tut-levelup 3 LevelUp-trigger steps gated on
specific level milestones (level 2
spellbook hint / level 5 trainer visit /
level 10 talent unlock)
--gen-tut-bg 3 ZoneEnter-trigger steps gated to BG
mapIds (AV=30 explains 40v40 / WSG=489
explains capture-flag / AB=529 explains
control-point) — explains each ruleset
on first BG entry
Validator catches: id+name+title+body required, triggerEvent
0..4, stepIndex > 0 (sequence starts at 1), no duplicate
tutIds, no duplicate (event,value,step) triples (sequence
ordering ambiguity). CRITICAL: hideAfterSec MUST be 0 (no
auto-dismiss) OR >= 5s — else popup vanishes before player
can read it. Warns on Login event with non-zero triggerValue
(dead data, Login is unconditional), non-Login event with
triggerValue=0 (would fire for ALL events of that kind), and
body length < 10 chars (likely placeholder text).
Format count 141 -> 142. CLI flag count 1472 -> 1479.
Adds --export-wswp-json / --import-wswp-json with the established
readEnumField template factoring int+name dual encoding for
conditionKind ("always"/"zoneonly"/"classonly"/"raceonly"/
"genderonly"). Signed gain field (gainAdjustDb_x10, int16)
preserved bit-for-bit through JSON.
All 3 presets (bosses/race/ui) byte-identical binary roundtrip
OK.
Live-tested both the duplicate-trigger error AND the same-
priority warning in one mutation: copied Nefarian rule's
trigger triple to match Onyxia (originalSoundId=1234,
ZoneOnly, conditionValue=249). Validator emitted BOTH:
ERROR: duplicate trigger triple
WARNING: same priorityIndex=100 — tie-break order undefined
Demonstrates that the validator catches both layers of the
collision (the deterministic data tie + the priority-based
disambiguation that the runtime would have used to resolve it).
CLI flag count 1470 -> 1472.
Novel format covering a need vanilla WoW lacked entirely:
priority-based sound substitution. Blizzard had no formal
mechanism for swapping a stock SoundEntry for a custom
replacement conditionally on zone/class/race/gender; the
closest equivalents were patch-level SoundEntries.dbc edits
with no condition support. Each WSWP entry binds one
(originalSoundId, condition) trigger to a replacementSoundId,
a priority index for tie-breaking (higher wins), and an
optional gain adjustment in 0.1 dB units (range ±30 dB
practical mixer limit).
Three presets covering common substitution scenarios:
--gen-swp-bosses 3 raid-boss zone-only swaps (Onyxia roar
in Onyxia's Lair / Ragnaros emerge in
Molten Core +2dB / Nefarian shout in BWL).
Priority 100 — beats global rules
--gen-swp-race 3 race-conditional voice swaps (BloodElf
priest / Tauren shaman / Undead warlock
cast voices). Priority 50
--gen-swp-ui 3 always-on UI sound swaps (level-up /
quest-complete / mount-up) at priority
10 with +3dB gain (boss/race overrides
win the priority fight)
Validator catches: id+name+original+replacement required,
conditionKind 0..4, no duplicate ruleIds, no self-replacement
(orig==repl is a no-op slot), non-Always kinds require non-
zero conditionValue (kind without target = matches everything,
duplicating Always semantics). CRITICAL: no duplicate
(originalSoundId, conditionKind, conditionValue) trigger
triple — runtime would have two rules for the same trigger
without a tie-breaker. Warns on priorityIndex=0 (rule never
wins), |gainAdjustDb_x10| > 300 (clip risk), Always condition
with non-zero conditionValue (dead data ignored at runtime),
and same-priority within same originalSoundId (tie-break
undefined when both rules' conditions match simultaneously).
Format count 140 -> 141. CLI flag count 1463 -> 1470.
Adds --export-wbrd-json / --import-wbrd-json. battlegroundName
emitted as informational field; battlegroundId int is
authoritative. All 3 presets (av/wsg/ab) byte-identical binary
roundtrip OK including the AB preset's weekly bonus quest token
binding.
Live-tested incentive-inversion validator: hand-mutated WSG
bracket 6 (rewardId 15) to swap winHonor and lossHonor — loss
becomes 750, win becomes 375. Validator correctly errored:
"lossHonor=750 > winHonor=375 — losing rewards more than winning
(no win incentive)". Catches the class of reward-config bugs
where misordered fields would silently flip the optimal player
strategy from "play to win" to "AFK and lose for max XP/hour".
CLI flag count 1461 -> 1463.
Novel replacement for the per-BG per-bracket reward
configuration vanilla WoW carried in BattlemasterList.dbc +
the hard-coded honor table in the server's BattlegroundMgr
(the "Mark of Honor" item granted on win/loss was hard-coded
per-BG type with no formal data-driven scaling). Each WBRD
entry binds one (battlegroundId, levelBracket) pair to its
win/loss honor amounts, win/loss marks, the mark itemId, an
optional weekly-bonus item, and a minimum-players-to-start
gate.
Three presets covering canonical vanilla BGs:
--gen-brd-av Alterac Valley reward ladder for brackets
5-6 (51-69 only — AV was endgame). 20
players/side, Mark of AV item 17502
--gen-brd-wsg Warsong Gulch ladder for all 6 brackets
(10-69), 10 players/side, Mark of WSG item
20558, monotonically scaling honor
(50/100/200/350/500/750)
--gen-brd-ab Arathi Basin ladder for brackets 2-6
(20-69), 15 players/side, Mark of AB item
20559, includes weekly bonus quest token
(placeholder itemId 20560)
Validator catches: id+battlegroundId required, bracketIndex
1..6 (vanilla), no duplicate rewardIds, no duplicate
(bgId,bracket) pairs (runtime reward-lookup tie),
bonusItemCount > 0 requires non-zero bonusItemId, minPlayers
ToStart > 0 (else BG queue would never start a match).
CRITICAL: lossHonor <= winHonor (else losing rewards more
than winning, no incentive to play to win — would degenerate
into AFK farming). Warns on winMarks=0 with markItemId set
(vanilla wins always granted at least 1 mark).
Format count 139 -> 140. CLI flag count 1454 -> 1461.
Adds --export-wauh-json / --import-wauh-json with the established
readEnumField template factoring int+name dual encoding for
factionAccess ("both"/"alliance"/"horde"/"neutral"). All 3
presets (stormwind/orgrimmar/bootybay) byte-identical binary
roundtrip OK including the Booty Bay neutral 15%/15% rate
configuration.
Live-tested economic-trap validator: hand-mutated Booty Bay
deposit to 60% + cut to 50% (sum 110%). Validator correctly
errored: "depositRatePct=6000 + cutPct=5000 = 11000 basis
points — seller would lose money on every sale (combined rates
>= 100%)". Catches misconfigured AH rates that would silently
trap players into negative expected returns on every listing.
CLI flag count 1452 -> 1454.
Novel replacement for the implicit per-faction auction-house
policy vanilla WoW carried in AuctionHouse.dbc + the hard-
coded deposit/cut rate constants in the server's AuctionMgr
(the 5% Alliance/Horde rate vs 15% neutral Booty Bay rate
was hard-coded on AH faction id). Each WAUH entry binds one
auction house instance to its faction-access rules,
deposit-rate (% of vendor sell price held as deposit), AH cut
(% taken from final sale price before crediting seller),
allowed listing durations (min/max hours), per-slot copper
fee, and the auctioneer NPC binding.
Three presets capturing canonical vanilla AH instances:
--gen-auh-stormwind Alliance Stormwind Trade District AH
with 5%/5% deposit/cut rates, 12-48hr
listing tiers, NPC Auctioneer Tricket
(creatureId 8666)
--gen-auh-orgrimmar Horde Orgrimmar Valley of Strength AH
with same vanilla rates as Stormwind,
NPC Auctioneer Tahesh (9856)
--gen-auh-bootybay Neutral Booty Bay AH with the famous
15%/15% penalty rates, NPC Auctioneer
Beardo (9858)
Validator catches: id+name required, factionAccess 0..3,
depositRatePct + cutPct each in 0..10000 (basis points), no
duplicate ahIds, no duplicate (faction,name) pairs (UI tab
dispatch tie), no duplicate npcAuctioneerId (gossip dispatch
tie), maxListingDurationHours > 0 and minListing <= maxListing.
CRITICAL: combined depositRatePct + cutPct < 10000 (else
seller would lose money on every successful sale — economic
trap). Warns on combined > 50% (sellers may abandon AH;
verify intentional like neutral AH penalty).
Format count 138 -> 139. CLI flag count 1445 -> 1452.
Adds --export-wprc-json / --import-wprc-json with the established
readEnumField template factoring int+name dual encoding for
triggerEvent ("onhit"/"oncrit"/"oncast"/"ontakedamage"/"onheal"/
"ondodge"/"onparry"/"onblock"/"onkill").
All 3 presets (weapon/ret/rage) byte-identical binary roundtrip
OK including the rage preset's previously-fixed Berserker Rage
proc rule (sourceSpellId=18499, procEffectSpellId=23691 distinct).
Live-tested self-loop validator a second time: re-introduced the
Berserker Rage source==effect bug via JSON edit (set effect back
to 18499). Validator correctly errored: "sourceSpellId ==
procEffectSpellId=18499 on OnCast trigger — infinite proc loop
(effect re-casts itself)". Confirms the round-trip path
preserves the self-loop guard and that the validator is ready
to catch this class of bug whenever a hand-edit reintroduces it.
CLI flag count 1443 -> 1445.
Adds --export-wirc-json / --import-wirc-json. allowedSlotsMask
emitted as both int + readable string (e.g. "Helm|Chest|Leg|
Boot") for tooling readability. Variable-length enchants
serialize as JSON object array of {enchantId, weight}.
All 3 presets (bear/eagle/tiger) byte-identical binary roundtrip
OK including the bear pool's totalWeight=100 (30+50+15+5 enchant
weight distribution).
Live-tested totalWeight mismatch validator: hand-mutated bear
pool enchant[1].weight from 50 to 60 (sum=110) while leaving
totalWeight=100 in JSON. Validator correctly errored:
"totalWeight=100 does not match sum of enchant weights=110 —
loot generator would mis-pick". Catches the class of denormal-
ized-cache-staleness bugs where the loot generator's hot-path
roll uses a precomputed total that no longer matches the
enchant table — players would see wrong rates of each enchant
tier without any obvious symptom.
CLI flag count 1434 -> 1436.
Novel replacement for the random-suffix enchant pool that
vanilla WoW carried in ItemRandomProperties.dbc +
ItemRandomSuffix.dbc (TBC+) + the per-item RandomProperty
rolls baked into the LootMgr. Each WIRC entry binds one
random-property pool to a name suffix ("of the Bear", "of the
Eagle"), a weighted enchant table (variable-length array of
{enchantId, weight}), and the equipment slots + class
restrictions where the suffix can roll.
At loot time, each green+ item rolls one pool based on its
slot, then picks one enchant from that pool weighted by
enchant.weight / totalWeight. The denormalized totalWeight
field is precomputed for the loot generator's hot-path roll.
Three presets covering the canonical vanilla suffix archetypes:
--gen-irc-bear STA-focused for plate slots (Helm/Chest/
Leg/Boot) with 4 weighted +Sta enchants
(3/5/7/10 — middle tier most common at
weight 50/100). Warrior+Paladin+DK class
mask
--gen-irc-eagle INT+STA caster pool for cloth slots with
5 weighted +Int+Sta enchants (3/5/7/10/12).
Mage+Priest+Warlock class mask
--gen-irc-tiger STR+AGI hybrid for leather slots with 5
weighted enchants. Rogue+Hunter+Druid class
mask
Validator catches: id+name required, allowedSlotsMask != 0
(else pool is unreachable — no slot would ever roll it),
non-empty enchant array, no zero-id enchants, no duplicate
enchantIds within same pool (should be merged with summed
weight). CRITICAL: totalWeight MUST equal sum of enchant
weights (else the loot generator's denormalized roll
mis-picks the distribution — players would see wrong rates of
each enchant tier). Warns on enchant weight=0 (never picked,
dead entry to remove or assign weight).
Format count 136 -> 137. CLI flag count 1427 -> 1434.
Adds --export-wbhv-json / --import-wbhv-json with the established
readEnumField template factoring int+name dual encoding for both
creatureKind ("melee"/"caster"/"tank"/"healer"/"pet"/"beast")
and evadeBehavior ("resettospawn"/"healatpath"/"fleetospawn"/
"noevade"). Variable-length specialAbilities serialize as JSON
object array of {spellId, cooldownMs, useChancePct}.
All 3 presets (melee/caster/boss) byte-identical binary
roundtrip OK including the boss preset's 4-ability rotation
with NoEvade and 90s-cooldown Deep Breath.
Live-tested leash<aggro un-killable invariant: hand-mutated
Onyxia boss leashRadius from 999 to 10 (below aggroRadius=50).
Validator correctly errored: "leashRadius=10.000000 <
aggroRadius=50.000000 — creature would evade before engaging
(un-killable from outside the leash)". Catches the class of
spawn-config bugs where a creature aggros at distance X but
evades at distance < X — no player can ever close to melee.
CLI flag count 1425 -> 1427.
Novel replacement for the implicit creature-behavior rules
vanilla WoW carried in creature_template.AIName + per-creature
C++ scripts in the server's ScriptMgr (most rare-elites and
bosses had hand-coded class-derived behaviors). Each WBHV entry
binds one combat behavior archetype to its creature kind (Melee
/ Caster / Tank / Healer / Pet / Beast), aggro / leash radii,
evade-on-leash policy (ResetToSpawn / HealAtPath / FleeToSpawn
/ NoEvade for raid bosses), corpse persistence duration,
default rotation spell, and a variable-length list of special
abilities (spellId + cooldown + use-chance triplets in basis
points).
Three presets covering common archetypes:
--gen-bhv-melee 3 entry-tier melee creatures (Kobold Worker
+ Timber Wolf + Stranglethorn Raptor) with
1 special ability each
--gen-bhv-caster 3 caster patterns (Defias Wizard with
Polymorph + Frost Nova / Murloc Coastrunner
with Frost Bolt + Lesser Heal / Voidwalker
Pet Pattern with Taunt + Sacrifice +
Suffering — Sacrifice intentionally has
useChancePct=0 as owner-triggered, exercising
the validator owner-triggered warning)
--gen-bhv-boss 1 Onyxia-pattern dragon (Tank kind,
NoEvade leash, 600s corpse for 40-man loot
distribution, 4 abilities including 90s-CD
Deep Breath)
Validator catches: id+name required, creatureKind 0..5,
evadeBehavior 0..3, aggroRadius > 0, no duplicate behaviorIds,
no zero-spellId specials, no duplicate spellId within same
behavior. CRITICAL invariant: leashRadius >= aggroRadius (else
creature evades back to spawn before reaching its target —
permanently un-killable from outside the leash radius). Warns
on corpseDuration < 60s (looting may fail in busy zones), and
useChancePct=0 on a special ability (correctly flagged on the
Voidwalker Sacrifice spec — verified live in smoke-test).
Format count 135 -> 136. CLI flag count 1418 -> 1425.
Adds --export-wbnd-json / --import-wbnd-json with the established
readEnumField template factoring int+name dual encoding for both
bindKind ("bindonpickup"/"bindonequip"/"bindonuse"/
"bindonaccount"/"soulbound"/"nobind") and itemQualityFloor
("poor"/"common"/"uncommon"/"rare"/"epic"/"legendary"/
"artifact"/"heirloom"). All 3 presets (vanilla/TBC/WotLK)
byte-identical binary roundtrip OK including the WotLK
Heirloom rule with accountBoundCrossFaction=true.
Live-tested raid-trade-window=0 contradiction validator:
hand-mutated TBC Uncommon rule (ruleId 12) tradableWindowSec
to 0 while keeping tradableForRaidGroup=true. Validator
correctly errored: "tradableForRaidGroup=true with
tradableWindowSec=0 — window expires instantly, equivalent
to no window". Catches a subtle policy-config bug where the
flag claims a feature exists but the duration silently
disables it.
CLI flag count 1416 -> 1418.
Novel replacement for the implicit item-binding policy vanilla
WoW carried in ItemTemplate.bondingType + per-item special-case
rules in the server's LootMgr (the 2-hour raid-loot trade
window was hard-coded; the account-bound-shared-across-faction
rule for heirlooms was a TBC+ addition with no formal data-
driven format). Each WBND entry binds one soulbind rule to its
bind kind (BoP / BoE / BoU / BoA / Soulbound / NoBind),
itemQualityFloor predicate (rule applies to items of this
quality and above unless overridden by a stricter rule),
tradable-window duration, raid-trade allowance, BoE-becomes-
BoP trigger, and cross-faction sharing flag.
Three presets capturing actual expansion-era policy evolution:
--gen-bnd-vanilla 4 rules — Poor=NoBind, Common=BoE,
Uncommon+=BoP no-window, Epic+=Soulbound.
NO raid-trade window — the 1.12 master-
loot drama era
--gen-bnd-tbc 5 rules adding the iconic 2-hour raid-
trade window for BoP items (Uncommon and
Rare quality)
--gen-bnd-wotlk 6 rules adding Heirloom = BindOnAccount
+ cross-faction (Alliance<->Horde via
account-mail) for the WotLK level-1-to-80
twink path
Validator catches: id+name required, bindKind 0..5,
itemQualityFloor 0..7, no duplicate ruleIds, no duplicate
(bindKind,qualityFloor) pairs (resolveForQuality lookup tie).
Hard error: tradableForRaidGroup=true with window=0 (window
expires instantly = no window at all). Warns on contradictions:
tradableForRaidGroup with non-BoP kind, window > 0 without
raid-trade flag, boeBecomesBoP without BoE kind,
accountBoundCrossFaction without BoA kind (all flag-ignored at
runtime).
Format count 134 -> 135. CLI flag count 1409 -> 1416.
Adds --export-wloc-json / --import-wloc-json with the established
readEnumField template factoring int+name dual encoding for both
locKind ("poi"/"rarespawn"/"herbnode"/"mineralvein"/"fishingspot"/
"areatrigger"/"portallanding") and factionAccess ("both"/
"alliance"/"horde"/"neutral"). Float coords (x/y/z) and skill
fields preserved bit-for-bit through JSON.
All 3 presets (poi/herb/rare) byte-identical binary roundtrip
OK including the rare-spawns preset's mixed respawn timers
(1800s..7200s).
Live-tested spawnable-kind respawn=0 validator: hand-mutated
Mor'Ladim's respawnSec to 0 in JSON, validator correctly
errored: "spawnable kind (rarespawn) with respawnSec=0 —
entity would spawn once and never come back". Catches the
common "added a rare spawn but forgot the timer" bug class
where rare-elites silently disappear after first kill.
CLI flag count 1407 -> 1409.
Novel unified replacement for the half-dozen proprietary
location tables vanilla WoW scattered across AreaPOI.dbc
(zone-discovery landmarks), gameobject_template.spawn rows
(herb/mineral/fishing nodes), creature_template rare-spawn
entries, and AreaTrigger.dbc (zone boundary teleports). Each
WLOC entry binds one world coord (mapId, x, y, z) to its kind
(POI / RareSpawn / HerbNode / MineralVein / FishingSpot /
AreaTrigger / PortalLanding), respawn timer, gathering-skill
gate, and on-discovery XP.
Three presets covering the major location flavors:
--gen-loc-poi 4 Alliance POIs (Stormwind/Ironforge/
Goldshire/Sentinel Hill) with discoverable
XP (50..100) and POI-kind iconry
--gen-loc-herb 5 Elwynn/Westfall herb nodes (Peacebloom
skill 1 to Stranglekelp skill 85) with 600s
respawn and Herbalism (skillId 182) gating
--gen-loc-rare 4 vanilla rare-elites with realistic
respawn timers (Mor'Ladim 1hr, Princess
Tempestria 2hr, Foreman Rigger 30min, Lord
Sakrasis 1hr)
Validator catches: id+name required, locKind 0..6,
factionAccess 0..3, no duplicate locationIds, spawnable kinds
(Rare/Herb/Mineral/Fishing) MUST have respawnSec > 0 (else
entity spawns once and never returns — common typo when
adding new spawns). Warns on discoverableXp set with non-POI
kind (XP would never award), requiredSkillId set with non-
gather kind (skill check would never fire), and gather kind
with skill > 0 but level = 0 (every player satisfies — almost
certainly a typo).
Format count 133 -> 134. CLI flag count 1400 -> 1407.