Markdown counterpart to --list-zone-deps. PR reviewers see at a
glance whether every referenced model exists in either open or
proprietary form across the conventional asset roots:
wowee_editor --export-zone-deps-md custom_zones/MyZone
# -> custom_zones/MyZone/DEPS.md
# Dependencies — MyZone
*Auto-generated. Status is best-effort — checks zone-local, output/,
custom_zones/, Data/ roots in that order.*
## Direct M2 placements (12)
| Refs | Path | Status |
|---:|---|---|
| 8 | `World/Tree.m2` | open + proprietary |
| 3 | `World/Lamp.m2` | open only |
| 1 | `World/Banner.m2` | MISSING |
Status column resolves each path against zone-local + output/ +
custom_zones/ + Data/ roots, trying both .wob/.wmo for buildings
and .wom/.m2 for models. Catches missing assets BEFORE pack-wcp
would silently include broken refs.
GitHub-Flavored Markdown — sortable by Refs column once rendered,
backtick-wrapped paths so URLs/spaces don't confuse the viewer.
Verified: scaffolded zone with 2 M2 placements (one duplicated) +
1 WMO placement → DEPS.md has 3 sections (one per category) with
correct ref counts (Tree.m2 ×2) and MISSING status for paths that
don't resolve in any root.
Diff family is now exhaustively complete across every shippable
format:
--diff-wcp archive
--diff-zone unpacked zone dir
--diff-glb glTF binary
--diff-wom WOM model
--diff-wob WOB building
--diff-whm WHM/WOT terrain pair
--diff-woc WOC collision
--diff-jsondbc JSON DBC sidecar <- new
Schema-level compare:
- format tag
- source filename
- recordCount + fieldCount (header values)
- actualRecs (records[] array length)
Useful for catching schema regressions when a sidecar is regenerated
by a different tool version, or for verifying a --migrate-jsondbc
pass actually changed what it claimed.
Verified: same file vs itself reports IDENTICAL exit 0;
2-record vs 3-record (with same format/source/fieldCount) reports
2 DIFFs (recordCount + actualRecs) with exit 1.
Last two missing entries in the diff family — terrain heightmap pairs
and collision meshes:
wowee_editor --diff-whm Z1/Z1_30_30 Z2/Z2_31_30
Diff: ...
a b
tile : ( 30, 30) ( 31, 30) DIFF
loadedChunks : 256 256
doodadPlace : 0 0
wmoPlace : 0 0
heightRange : [-1.50,1.50] [-1.50,1.50]
wowee_editor --diff-woc tile_a.woc tile_b.woc
Diff: ...
a b
tile : ( 30, 30) ( 31, 30) DIFF
triangles : 32768 32768
walkable : 32768 32768
steep : 0 0
--diff-whm: tile coord, loaded chunk count, doodad/WMO placement
counts, texture/name table sizes, height range (min/max with float
epsilon). Pointwise height compare intentionally not done — float
perturbation from format round-trips would false-flag.
--diff-woc: tile coord, total triangles, walkable + steep counts.
Catches whether a --regen-collision pass actually changed something.
Diff family is now exhaustively complete for every shippable open
format:
--diff-wcp archive vs archive
--diff-zone unpacked zone dir vs zone dir
--diff-glb glTF binary vs glTF binary
--diff-wom WOM model vs WOM model
--diff-wob WOB building vs WOB building
--diff-whm WHM/WOT terrain pair vs pair
--diff-woc WOC collision vs collision
Verified: tile (30,30) vs (31,30) reports tile DIFF + identical
counts (since both are flat scaffolds); same-vs-self reports
IDENTICAL with exit 0.
Designers receive JSON DBCs from various tools (older asset_extract
versions, third-party converters) that may omit fields the runtime
now expects. --validate-jsondbc tells you what's wrong; this fixes
the auto-fixable ones:
wowee_editor --migrate-jsondbc db/Spell.json
added: format = 'wowee-dbc-json-1.0'
added: source = 'Spell.dbc'
fixed: recordCount 99 -> 47882 (matches actual)
inferred: fieldCount = 234 (from first row)
Migrated db/Spell.json -> db/Spell.json
fixes applied: 4
Auto-fixes:
- Missing 'format' tag → add 'wowee-dbc-json-1.0'
- Missing 'source' field → derive from filename stem + '.dbc'
- Missing 'fieldCount' → infer from first row
- recordCount mismatch → recompute from actual records[] length
NOT auto-fixed (data loss risk — surfaced as warnings instead):
- Wrong-width rows (silently padding/truncating could mangle field
values; the user needs to inspect and decide)
In-place by default (writes back to the input path); accepts an
optional output path for non-destructive migration.
Verified: a JSON missing format/source/fieldCount with mismatched
recordCount=99 (actual 2) → migrate applies 4 fixes →
--validate-jsondbc reports PASSED on the result.
Generates a single-file HTML viewer next to the zone .glb. Anyone
with a modern browser can open it — no installs, no Blender, no
node_modules:
wowee_editor --bake-zone-glb custom_zones/MyZone # bakes .glb
wowee_editor --export-zone-html custom_zones/MyZone # writes .html
open custom_zones/MyZone/MyZone.html # any browser
Uses Google's <model-viewer> web component (loaded from unpkg with
^4.0.0 version pin so a unpkg 'latest' bump can't silently break
older HTML files). The HTML itself is ~1.1KB; the .glb sits beside
it for the viewer to load via relative URL.
Features baked into the page:
- Camera controls (orbit, zoom, pan)
- Auto-rotate at 15deg/sec for the headless preview case
- Shadow casting + neutral environment IBL for non-flat lighting
- Header strip showing display name, map slug, tile count, mapId
Refuses to run if the zone .glb doesn't exist yet (clear error
message points the user at --bake-zone-glb).
Verified: scaffolded zone -> --bake-zone-glb -> --export-zone-html.
1.1KB HTML opens cleanly in browsers, references the sibling .glb.
Missing-glb case correctly errors with exit 1 + helpful next-step
hint.
Companion to --diff-wom for buildings. Same count-based shape so
round-trips through OBJ/glTF can be validated without false
positives from float perturbation:
wowee_editor --diff-wob orig back
Diff: orig.wob vs back.wob
a b
groups : 5 5
portals : 3 3
doodads : 12 12
materials : 8 8
groupTex : 7 7
totalVerts : 4609 4609
totalIdx : 10950 10950
name : Stormwind Stormwind
boundRadius : match
IDENTICAL
Compares: groups, portals, doodads, aggregated materials count
(per-group materials summed), aggregated group-texture count,
total verts/indices across all groups, name, boundRadius (with
0.01 epsilon).
Diff family is now complete across every binary format that ships
in a content pack:
--diff-wcp archive vs archive
--diff-zone unpacked zone dir vs zone dir
--diff-glb glTF binary vs glTF binary
--diff-wom WOM model vs WOM model
--diff-wob WOB building vs WOB building
Verified: identical pair reports IDENTICAL exit 0; pair with extra
group + extra doodad + name change reports 6 DIFFs with exit 1.
Symmetric to --info-creature and --info-quest. Every PlacedObject
field for one entry:
wowee_editor --info-object $Z/objects.json 3
Object [3]
type : wmo
path : World/StaticObject/Stormwind/HumanInn.wmo
nameId : 3497721408
uniqueId : 4
position : (-8412.300, 633.200, 110.500)
rotation : (0.00, 90.00, 0.00) deg
scale : 1.000
Useful when debugging a misplaced object — quickly see the exact
position/rotation/scale + uniqueId + nameId without having to
grep through objects.json or run --list-objects through awk.
JSON mode emits the structured record. Inspector lineup is now
symmetric across all three content types:
--info-{creatures,objects,quests} aggregate counts
--list-{creatures,objects,quests} per-entry table
--info-{creature,object,quest} single-entry deep dive
The --list-* commands give table views; the --info-* (creatures/objects/
quests) give summary counts. Neither shows every field of a single
entry. These fill that gap:
wowee_editor --info-creature $Z/creatures.json 0
Creature [0] 'Captain'
id : 1
displayId : 11430
position : (100.00, 200.00, 50.00)
orientation : 0.00 deg
scale : 1.00
level : 12
health/mana : 100 / 0
damage : 5-10
armor : 0
faction : 0
behavior : stationary
wander rad : 10.0
aggro rad : 20.0
leash rad : 40.0
respawn ms : 300000
patrol points : 0
flags :
wowee_editor --info-quest $Z/quests.json 0
Quest [0] 'Hunt Wolves'
id : 1
required level : 5
giver NPC id : 100
turn-in NPC id : 100
next quest id : 0 (terminal)
reward : 250 XP, 0g 0s 0c, 1 item(s)
item[0] : Item:Sword
objectives : 1
[0] kill ×5 Wolf — Slay 5 Wolf
Useful for digging into 'why is this NPC not behaving like I expect?'
or reviewing one quest's full design in one screen instead of running
3-4 list-* commands. JSON mode emits every field as a structured
record for programmatic consumption.
Inspector lineup is now complete from aggregate to per-entry:
--info-{creatures,objects,quests} (counts)
--list-{creatures,objects,quests} (table)
--info-{creature,quest} (single entry, all fields)
Designers often prefer spreadsheets over JSON for read-only analysis
('which 5 quests give the most XP?', 'how many lvl 10+ creatures?',
'pivot table by faction'). This emits creatures.csv / objects.csv /
quests.csv in standard CSV that LibreOffice / Excel / Numbers / Python
pandas all consume natively:
wowee_editor --export-zone-csv custom_zones/MyZone
wrote custom_zones/MyZone/creatures.csv (47 rows)
wrote custom_zones/MyZone/objects.csv (12 rows)
wrote custom_zones/MyZone/quests.csv (8 rows)
CSV columns chosen to be designer-actionable:
- creatures: index/id/name/displayId/level/health/mana/faction/
position/orientation/scale + hostile/questgiver/vendor/trainer flags
- objects: index/type/path/position/rotation/scale
- quests: index/id/title/requiredLevel/giver/turnIn/reward fields/
objectiveCount + objectives/itemRewards joined by '; ' for keep-
one-row-per-quest sortability
RFC 4180 quoting: fields with comma/quote/newline get wrapped in
double quotes with internal quotes doubled. Verified on a creature
named 'Big, Bad Bear' — comes out as '"Big, Bad Bear"'.
Round-trip back into the editor isn't supported yet (would need
schema-aware CSV parsing); this is read-only-export for now.
Structural compare of two WOM models. Useful for verifying that
--migrate-wom or a round-trip through OBJ/glTF/STL preserved the
right counts:
wowee_editor --diff-wom orig back
Diff: orig.wom vs back.wom
a b
version : 1 1
vertices : 5 5
indices : 18 18
triangles : 6 6
textures : 0 0
bones : 0 0
animations : 0 0
batches : 0 0
name : Pyramid Pyramid
bounds : match
IDENTICAL
Compares sizes only (vertex / index / bone / animation / batch /
texture counts) plus name and bounds. Bounds match-with-epsilon
(0.01 unit slop, generous since positions are typically yards) so
text-format round-trips that perturb the last bit don't false-flag.
Pointwise vertex compare is intentionally not done — it would be
O(n²) and brittle to tiny float diffs from format conversions.
Diff family is now complete:
--diff-wcp (archive vs archive)
--diff-zone (unpacked zone dir vs zone dir)
--diff-glb (glTF binary vs glTF binary)
--diff-wom (WOM model vs WOM model)
Verified: identical pair reports IDENTICAL exit 0; pair with 1 extra
vertex + extra triangle + name change correctly reports 4 DIFFs
(verts/indices/triangles/name) with exit 1.
OBJ companion to --bake-zone-glb / --bake-zone-stl. Same multi-tile
WHM aggregation, this time as Wavefront OBJ — opens directly in
Blender / MeshLab / 3DS Max for hand-editing the terrain mesh:
wowee_editor --bake-zone-obj custom_zones/MyZone
# -> custom_zones/MyZone/MyZone.obj
Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.obj
2 tile(s), 41472 verts, 65536 tris
Each tile becomes its own 'g tile_TX_TY' block so designers can hide
tiles independently in Blender. Single global vertex pool with
per-tile vertex base indices for face emission (OBJ requires verts
before faces, so we collect per-tile face indices in memory then
emit them after all verts are streamed to disk).
Hole bits respected (cave-entrance quads dropped). Coords match
WoweeCollisionBuilder's outer-grid layout exactly so .obj/.glb/.stl
of the same source align spatially when overlaid.
Why three formats for full-zone export: glTF for on-screen 3D
viewers, STL for fabrication, OBJ for DCC editing. Three different
ecosystems, three different format sweet spots.
Verified: 2-tile zone (Z + added tile) baked correctly. 41472 verts
(2 × 20736), 65536 tris (2 × 32768), 2 'g' blocks (tile_30_30 +
tile_31_30) — matches what --bake-zone-glb reports for the same
input.
Cross-checks every position accessor's claimed min/max against the
actual data in the BIN chunk. glTF viewers use these bounds for
camera framing and frustum culling; stale values (e.g. from a tool
that edited geometry without recomputing) cause models to vanish
at certain angles or get framed wrong on load.
wowee_editor --check-glb-bounds Tree.glb
GLB bounds: Tree.glb
position accessors checked : 1
mismatched : 0
PASSED
wowee_editor --check-glb-bounds bad.glb
GLB bounds: bad.glb
position accessors checked : 1
mismatched : 1
FAILED — 1 error(s):
- accessor 0 bounds mismatch: claimed [-9999,-9999,-9999]-[9999,
9999,9999] vs actual [533.3,533.3,98.5]-[1066.7,1066.7,101.5]
Walks the meshes/primitives tree, dedups the POSITION attribute
accessors (multiple primitives can share one), then for each unique
accessor reads the BIN chunk via the bufferView+byteOffset chain
and recomputes the actual min/max. Compares with float epsilon
(1e-3) since perfect equality across float compilers isn't
guaranteed.
Also flags missing min/max — the glTF 2.0 spec REQUIRES position
accessors to declare bounds (validators like Khronos's reference
impl reject .glbs that omit them).
Verified: a fresh --export-whm-glb passes clean. After hand-editing
the JSON to claim bogus bounds (-9999 to 9999 for a 533-1067 range
mesh), --check-glb-bounds correctly reports the mismatch with full
claimed-vs-actual values, exit 1.
STL companion to --bake-zone-glb. Designers can 3D-print a miniature
of an entire multi-tile zone in one slicer load — useful for tabletop
RPG props, physical playtest references, or just the satisfaction of
holding a zone in your hand:
wowee_editor --bake-zone-stl custom_zones/MyZone
# -> custom_zones/MyZone/MyZone.stl
Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.stl
2 tile(s), 65536 facets, 0 hole quads skipped
Streams ASCII STL directly to disk (no in-memory accumulation —
relevant for large multi-tile zones). Per-triangle face normal
computed from cross product since slicers use it for orientation.
Hole bits respected (cave-entrance quads dropped) and counted
separately so users see how much got skipped.
Why STL alongside the existing glTF zone bake: glTF targets
on-screen 3D viewers; STL targets fabrication. Different ecosystems,
different file formats, both now reachable from the same WHM source
with one command each.
Verified: 2-tile zone (Z + added tile) baked correctly. 65536 facets
(2 tiles × 32768 each), 12MB ASCII STL, well-formed solid/endsolid
framing, normals computed (e.g. '0.305 -0.399 -0.864' for the first
sloped facet).
The CLI surface is now 103 commands. Tab completion is no longer a
nice-to-have:
source <(wowee_editor --gen-completion bash) # one-time setup
wowee_editor --info-<TAB> # all info-* offered
Two complementary commands:
--list-commands: parses printUsage's own output to extract every
'--flag' it documents, dedupes, sorts. Auto-tracks new commands
as they're added (no parallel list to maintain).
--gen-completion bash|zsh: emits a completion script that re-execs
--list-commands at completion time, so newly-added flags light up
without regenerating the script. Bash version uses compgen with
per-session caching in ; zsh version uses the
_arguments + compdef framework.
Both completion scripts also fall back to file-path completion in
arg slots (the common case for --info-/--validate-/--export-
commands that take a path).
Verified:
- --list-commands: 103 unique flags emitted alphabetically
- --gen-completion bash: well-formed script using full binary path
for the cached --list-commands invocation
- --gen-completion zsh: same shape, _arguments-based
- Unknown shell: clear error + exit 1
Structural compare of two glTF 2.0 binaries. Completes the diff
suite alongside --diff-wcp (archive-vs-archive) and --diff-zone
(unpacked-zone-vs-zone):
wowee_editor --diff-glb a.glb b.glb
Diff: a.glb vs b.glb
a b
meshes : 1 2 DIFF
primitives : 256 2 DIFF
accessors : 258 4 DIFF
bufferViews : 3 3
buffers : 1 1
BIN bytes : 890880 1781760 DIFF
Reports per-category counts side-by-side with a 'DIFF' marker on
mismatches. Compares structure (mesh/primitive/accessor counts +
BIN chunk size), NOT byte-level — JSON key ordering can vary
between tools so a byte diff would have false positives. JSON mode
emits the per-field {a, b} pair plus totalDiffs + identical bool
for CI consumption.
Useful for confirming alternate export paths produce equivalent
output (does --bake-zone-glb match concatenated --export-whm-glbs?
does a tool refactor preserve the same shape?).
Verified: same file vs itself reports IDENTICAL with exit 0.
Single-tile WHM .glb (1 mesh, 256 primitives, 890KB BIN) vs
2-tile bake (2 meshes, 2 primitives, 1.7MB BIN): correctly flags
4 DIFFs with exit 1.
Bakes every WHM tile in a multi-tile zone into ONE .glb so the
entire zone opens in three.js / model-viewer / Sketchfab with one
file. Each tile becomes its own mesh+node so they can be toggled
independently in the viewer:
wowee_editor --bake-zone-glb custom_zones/MyZone
# -> custom_zones/MyZone/MyZone.glb
Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.glb
3 tile(s), 62208 verts, 98304 tris, 3 meshes, 2672640-byte BIN
Implementation:
- Walks ZoneManifest::tiles, loads each tile's WHM/WOT pair via
WoweeTerrainLoader.
- Same per-chunk 9x9 outer-grid + 8x8 quads layout as
--export-whm-glb (so tiles align spatially with corresponding
--export-woc-obj output).
- All tiles share one global vertex+index pool packed into a single
BIN chunk; per-tile primitives slice their index range via
byteOffset on the indices accessor.
- One node per tile, named 'tile_TX_TY', so viewers can hide
individual tiles for area-by-area inspection of large zones.
v1 limitation: terrain only — object/WOB instances are a follow-up
that needs careful per-mesh bufferView slicing across many distinct
loaded models. Listed in --info-glb as 'meshes' so users see the
shape of the output before opening in a viewer.
Verified: 3-tile zone (Z + 2 added tiles) baked correctly. 62208
verts (3 × 20736), 98304 tris (3 × 32768), 3 meshes/primitives.
--validate-glb on the output: PASSED, all chunk types correct,
accessor bufferView refs in range.
Closes the WOM <-> STL bridge for the fabrication ecosystem (Cura,
PrusaSlicer, TinkerCAD, Meshmixer, SolidWorks, Fusion 360). Designers
can now edit prints in any CAD/3D-print tool and bring them back to
the engine:
asset_extract # M2 -> WOM (open binary)
--export-stl # WOM -> STL (universal fabrication format)
... edit in TinkerCAD / sculpt in Meshmixer ...
--import-stl # STL -> WOM (back to engine format)
--validate-wom # confirm consistency
Parser handles ASCII STL only (binary STL is a follow-up):
- 'solid <name>' header — preserves model name
- 'facet normal nx ny nz' — sets the face normal for following verts
- 'vertex x y z' — accumulates 3 per facet
- 'endfacet' — emits the triangle with the face normal applied to all
3 verts (STL doesn't carry per-vertex normals, so the round trip is
faceted-shading by design)
- Dedupes on (pos, normal) so shared vertices on the same face merge
(a 4-vert square base with 2 tris collapses to 4 verts), but verts
shared across faces with different normals stay distinct (correct
for faceted geometry)
- Computes bounds from positions for renderer culling
Round-trip cost: a 5-vert/6-tri pyramid round-trips to 16 verts/6
tris. The vertex inflation is structural (STL faceted-shading
semantics) — geometry preservation is exact. Verified via
WOM -> STL -> WOM -> --validate-wom (PASSED, bounds and tri count
match exactly).
ASCII STL is the universal 3D printing format — Cura, PrusaSlicer,
Bambu Studio, Slic3r, OctoPrint, MakerBot Print, and basically every
slicer made in the last 25 years opens it natively. Lets WOM models
drive physical prints with no conversion friction beyond one command:
wowee_editor --export-stl Tree # -> Tree.stl
wowee_editor --export-stl Tree out.stl
Per-spec STL ASCII output:
- 'solid <name>' header / 'endsolid <name>' footer (name sanitized
to alphanum + underscore for slicers that strict-parse)
- Per-triangle 'facet normal nx ny nz' with normal computed from
cross-product of edges 1 and 2 (most slicers use this for
orientation hints; falls back to (0,0,1) for degenerate triangles)
- 'outer loop' with three vertex lines per facet
- No shared vertex pool — STL stores every triangle independently
Why STL alongside OBJ + glTF: OBJ targets DCC tools (Blender etc.),
glTF targets web 3D viewers (Sketchfab, three.js), and STL targets
fabrication. Three different ecosystems, three different format
needs — wowee open formats now bridge to all three.
Verified on a 5-vert/6-tri pyramid: STL has 6 facets with correctly
computed normals (0 -1 0 for the bottom faces, computed slopes for
the side triangles), proper solid/endsolid framing, name preserved
('solid Pyramid').
Companion to --migrate-wom (single-file upgrade). Walks a zone dir
recursively and upgrades every legacy WOM (v1/v2 with no batches[])
to WOM3 in-place:
wowee_editor --migrate-zone custom_zones/MyZone
upgraded: custom_zones/MyZone/models/v1_0.wom
upgraded: custom_zones/MyZone/models/v1_1.wom
migrate-zone: custom_zones/MyZone
scanned : 3 WOM file(s)
upgraded : 2 (added single-batch entry)
already v3: 1 (no change needed)
Idempotent: already-migrated files become no-ops, so it's safe to
run inside --for-each-zone over a whole project. Useful when the
editor adds a new WOM3-only feature and legacy zones need a one-shot
upgrade pass.
Returns exit 1 if any file fails to write (separate from 'no upgrade
needed'), so CI can gate on it.
Verified: seeded a dir with 2 v1 WOMs + 1 v3 WOM. First run
upgraded 2, reported 1 as already-v3. Re-run reported all 3 as
already-v3 (no double-batching).
--info-jsondbc only verifies recordCount matches the actual records[]
array length. This goes deeper, validating the full sidecar schema
that --convert-json-dbc consumes:
wowee_editor --validate-jsondbc db/Spell.json
Checks:
- top-level value is a JSON object
- 'format' field exists, is a string, equals 'wowee-dbc-json-1.0'
- 'source' field present (so re-import knows the DBC slot)
- recordCount + fieldCount are non-negative integers
- 'records' is an array; recordCount matches actual length
- each record is an array exactly fieldCount cells wide
- each cell is string|number|bool|null (no nested objects/arrays)
Errors capped (3 per category) with '... and N more' tail so a
1000-row file with consistent breakage doesn't drown the report.
Exit 1 on any error so CI can gate.
Verified on a hand-rolled good JSON (passes clean) and a bad one
with: wrong format tag, missing source, wrong-width row, and an
object cell — all 4 issues reported with precise positions and
exit 1.
Format-validator lineup is now complete:
Open binary: WOM / WOB / WOC / WHM / GLB
Open text: JSON DBC
Every shippable open format has a CLI validator that gates on
schema/structure errors.
WOM3 is a strict superset of WOM1/WOM2: adds a batches[] array that
lets a single mesh carry multiple materials. Older content saved as
WOM1 (static) or WOM2 (animated) is missing this metadata, so
batch-aware tooling (--info-batches, --export-glb per-primitive
splits, future material-aware renderers) treats it as one implicit
batch. This makes that explicit:
wowee_editor --migrate-wom Tree # in-place upgrade
wowee_editor --migrate-wom Tree out/Tree # write to a different base
What it does:
- Loads the old WOM, adds a single Batch covering the entire index
range with textureIndex=0 and blend=opaque (matches the implicit
treatment).
- Saves — WoweeModelLoader::save picks WOM3 magic automatically once
batches.size() > 0, so the version field re-derives correctly.
Idempotent: running on an already-migrated v3 file is a no-op
('already had batches; no schema change') so it's safe to run
in for-each-zone batch passes.
Verified: built a v1 single-tri model, migrated to v3, --info reports
'version : 3 (multi-batch)', --info-batches shows '1 batches' with
the expected default fields. Re-running --migrate-wom on the v3 file
correctly says 'already had batches; no schema change'.
Two more sub-inspectors filling gaps in the format-coverage matrix:
wowee_editor --info-bones HumanMale.m2
M2 bones: HumanMale.m2 (54)
idx parent depth keyBone flags pivot (x, y, z)
0 -1 0 0 512 ( 0.00, 0.00, 0.95)
1 0 1 1 512 ( 0.00, 0.00, 1.05)
2 1 2 -1 512 ( 0.00, 0.05, 1.18)
...
The depth column is computed by walking parents (cycle-guarded by
boneCount cap) so the tree shape is visible at a glance. Useful
when debugging skeleton structure ('why is this finger bone not
following its hand?').
wowee_editor --list-zone-textures custom_zones/MyZone
Zone textures: custom_zones/MyZone
WOMs scanned : 47
unique textures : 23
refs path
8 Character/Human/Male/HumanMaleSkin00_00.blp
6 Character/Generic/Hair01.blp
3 Tileset/Generic/Stone.blp
...
Pairs with --list-zone-deps (which lists model paths) — this lists
the textures those models pull in. Each WOM contributes at most
one count per unique texture (so a model with the same texture in
3 batches doesn't inflate the ref count). Useful for verifying
every BLP/PNG ships with the zone before --pack-wcp.
Verified on a real WoW M2 (nexusraid_skya.m2): 44 bones reported
with parent indices, depths, and pivot offsets in the expected
ranges.
Three M2 sub-inspectors covering data fields the proprietary M2 format
carries that the open WOM doesn't yet (or carries differently). Useful
for understanding what gets lost when running --convert-m2 vs what
ships with the open conversion:
wowee_editor --info-attachments Character/Human/HumanMale.m2
M2 attachments: ... (15)
idx id bone pos (x, y, z)
0 0 5 ( 0.00, 0.50, 1.20) # head
1 1 27 ( 0.10, -0.20, 0.30) # right hand
...
wowee_editor --info-particles Spells/Fireball.m2
particles: 8, ribbons: 2
Particles:
idx id bone tex blend type pos (x, y, z)
...
Ribbons:
idx id bone tex mat pos (x, y, z)
...
wowee_editor --info-sequences Creature/Wolf/Wolf.m2
M2 sequences: ... (12)
idx id var duration flags speed blend
0 0 0 1733 32 0.00 150 # Stand
4 4 0 950 0 1.50 150 # Walk
5 5 0 625 0 3.20 150 # Run
...
Three commands share one entry point — they all need the same
M2Loader::load + skin-merge dance, then differ only in which sub-
array they iterate. Reduces duplicate boilerplate. JSON mode
emits per-entry records with index for programmatic consumption.
Why these matter: M2 carries scene-graph metadata (where to mount
weapons, where particles spawn, which animation is which) that
gameplay code reads at runtime. Surfacing it in a CLI lets
designers verify content without spinning up the renderer.
Companion to --validate-{wom,wob,woc,whm,all}: now the .glb files
we emit (and any third-party .glbs we receive) get the same deep-
check treatment.
wowee_editor --info-glb model.glb # show metadata, exit 0
wowee_editor --validate-glb model.glb # show + exit 1 on errors
Shared parser, different verdict policy. Checks:
- 12-byte header: magic = 'glTF' (0x46546C67), version = 2,
totalLength matches actual file size
- JSON chunk: type = 'JSON' (0x4E4F534A), length within file
- BIN chunk: type = 'BIN\0' (0x004E4942), length within file
- asset.version is '2.0' (per-spec required string)
- accessor.bufferView indices in range
- bufferView.byteOffset+byteLength fits within BIN chunk
Reports counts: meshes, primitives, accessors, bufferViews, buffers,
plus chunk sizes. JSON mode emits structured output for CI scripts.
Verified on a real --export-whm-glb output (256-primitive terrain,
929KB total): all checks pass clean. Synthesized a broken file
with corrupted magic bytes: --validate-glb correctly reports
"magic is not 'glTF'" and exits 1.
Visualizing quest chains in plain text rapidly becomes unreadable
past ~10 quests. This emits a DOT file that renders to a labeled
DAG you can paste into a wiki or PR description:
wowee_editor --export-quest-graph custom_zones/MyZone
dot -Tpng custom_zones/MyZone/quests.dot -o quests.png
Node coloring conveys completion-readiness at a glance:
- lightgreen: has objectives + reward (will complete in-game)
- lightyellow: has objectives but no reward (uncommon)
- lightgray: no objectives (won't complete — common authoring bug)
- mistyrose dashed: synthetic node for a quest ID referenced by
nextQuestId but missing from quests.json (broken chain)
Edges:
- solid black: valid chain link
- dashed red labeled 'missing': dangling nextQuestId
Labels include quest ID, title (DOT-escaped for safety), required
level, and XP reward — enough context that the graph stands alone
without needing to cross-reference quests.json.
Verified on a 3-quest zone with chain 1->2->3->999 (last broken):
DOT output has 3 colored nodes (lightgreen for the 2 quests with
objectives, lightgray for the third), 2 solid edges, 1 dashed-red
edge to a synthetic mistyrose <missing> node.
Catches dangling references that --validate (which only checks
open-format file presence) doesn't:
wowee_editor --check-zone-refs custom_zones/MyZone
Zone refs: custom_zones/MyZone
objects checked : 12 (2 missing)
quests checked : 5 (1 bad NPC refs)
FAILED — 3 issue(s):
- object[3] missing: World/Doodad/Tree.m2
- object[7] missing: World/Building/Inn.wmo
- quest[1] 'Hunt' giver 999 not in creatures.json
Two checks:
1. Every objects.json model path resolves on disk in any of the
conventional roots (zone-local, output/, custom_zones/, Data/),
trying both the open (.wom/.wob) and proprietary (.m2/.wmo)
variants, with a case-fold fallback for case-sensitive Linux
filesystems where extracted assets are usually lowercased.
2. Every quest's questGiverNpcId / turnInNpcId references a
creature in creatures.json (when the zone has any). Filter:
only flag IDs < 100000 since production wires upstream NPCs
with 6-digit IDs that legitimately reference outside content.
Errors capped at 30 listed (with full counts in the summary) so
a misconfigured zone with 500 broken refs doesn't drown the
output. Exit 1 on any issue so CI can gate.
Verified: zone with 1 creature, 2 dead model refs, 1 bad NPC
giver — all 3 issues reported with precise indices and exit 1.
Enumerates every external model path a zone references — both directly
placed (objects.json) and indirectly via WOB doodad placements. Useful
when packaging a content pack to confirm every asset will ship:
wowee_editor --list-zone-deps custom_zones/MyZone
Zone deps: custom_zones/MyZone
WOBs scanned : 3
unique paths total : 12
Direct M2 placements (5 unique):
World/Generic/Lamp.m2
World/Generic/Tree.m2 ×8
...
Direct WMO placements (2 unique):
World/StaticObject/Stormwind/HumanInn.wmo
WOB doodad M2 refs (5 unique):
World/Furniture/Chair.wom
World/Furniture/Table.wom
...
Aggregation:
- Counts duplicates so frequently-used assets show '×N' instead of
cluttering the list with N identical lines.
- Walks WOBs in the zone dir recursively (catches buildings/ subdir).
- Also recurses into WOBs referenced by direct WMO placements so
transitive doodad deps surface — matches the runtime's actual load
chain.
- Sorted output (std::map) so diffs across versions are stable.
JSON mode emits per-category arrays for programmatic consumption
(e.g. piping into a script that checks each path exists in Data/).
Verified on a synthesized zone with 3 M2 placements (one duplicated)
+ 1 WMO placement + no WOBs: 3 unique total, Tree.m2 correctly
deduped with ×2 count, JSON arrays well-formed.
Two more drill-down inspectors that complement --info-batches:
wowee_editor --info-textures HumanMale.wom
WOM textures: HumanMale.wom (3 textures)
idx blp png path
0 y y Character/Human/Male/HumanMaleHair.blp
1 y - Character/Human/Male/HumanMaleBody.blp
2 - - Character/Human/Male/HumanMaleEye.blp
wowee_editor --info-doodads Stormwind.wob
WOB doodads: Stormwind.wob (3 placements)
idx scale pos (x, y, z) rot (x, y, z) model
0 1.50 ( 1.0, 0.0, 1.0) ( 0.0, 90.0, 0.0) Furniture/Chair.wom
...
--info-textures: cross-checks each referenced texture against both
the proprietary BLP and the open PNG sidecar on disk (each lookup
tries the literal path AND falls back to Data/<lowercased path>
since WoW assets are case-sensitive on Linux but designers usually
type Mixed Case). The y/- columns let you spot which textures are
missing before --pack-wcp would fail at runtime.
--info-doodads: model path + position + rotation + scale per
instance. Companion to --info-textures (GPU resources) — this tracks
scene composition.
Both have --json mode that emits per-entry records with index for
programmatic consumption. Verified on synthesized model with 3
texture refs + building with 3 doodads; all fields render correctly.
Third and final glTF 2.0 binary exporter — terrain heightmaps now
ship to the modern web 3D ecosystem alongside WOM models and WOB
buildings:
wowee_editor --export-whm-glb custom_zones/MyZone/MyZone_30_30
Mesh layout matches --export-whm-obj exactly (9x9 outer vertex grid
per chunk, 8x8 quads -> 2 tris each, hole bits respected) so the
.glb and .obj of the same source align spatially when overlaid.
Per-chunk primitive scheme:
- One global vertex pool (positions + normals interleaved by section).
- One global index pool packed sequentially.
- Each loaded chunk gets its own primitive sliced from the shared
index bufferView via byteOffset.
- Designers can hide chunks individually in three.js for area-by-area
inspection of large terrains.
v1 simplifications:
- Normals synthesized as +Z (terrain Z-up). Real per-vertex normals
with cross-chunk smoothing is a follow-up; viewers can compute
their own from positions in the meantime.
- No UVs (no per-chunk material yet — that depends on splat alpha
layers which are a separate format problem).
Verified on a 256-chunk scaffolded zone: 20736 verts, 32768 tris,
256 primitives. First chunk's primitive has 384 indices (128 tris
× 3). Position accessor bounds match WoW coord system for tile
(30, 30): min ~(533, 533), max ~(1067, 1067). Total .glb 929KB,
all spec-compliant.
Pairs naturally with --copy-zone for the 'duplicate then re-populate'
templating workflow:
wowee_editor --copy-zone custom_zones/Base 'Variant'
wowee_editor --clear-zone-content custom_zones/Variant --all
# ... fresh content can now be added without seeing the source's
# creatures/objects/quests bleed through.
Selective wipe via flags so you can keep some content:
--clear-zone-content ZONE --creatures # wipe NPCs only
--clear-zone-content ZONE --quests --objects # quests + props
--clear-zone-content ZONE --all # nuke everything
Implementation:
- Deletes (not blank-writes) so subsequent --info-* commands report
'no content' rather than 'total: 0' which falsely implies the
file existed. Missing files are the canonical 'no content' state.
- Refuses to run with no flags (avoids ambiguous 'do you mean
everything?' silent foot-gun).
- Resets ZoneManifest::hasCreatures when wiping creatures so server
module gen doesn't expect an NPC table that's no longer there.
- Reports per-file action (removed / skipped) plus total count.
Verified end-to-end: scaffolded zone, populated all 3 content
files, --creatures wipes one (others remain), --all wipes the
rest while reporting the already-removed one as 'skipped'.
--info shows aggregate batch count; this drills into each one:
wowee_editor --info-batches HumanMale
WOM batches: HumanMale.wom (v3, 2 batches)
idx iStart iCount tris blend flags texture
0 0 3 1 opaque - Body.blp
1 3 3 1 alpha two-sided no-zwrite Hair.blp
Useful for debugging:
- 'why is this submesh transparent?' -> check blendMode column
- 'why is the back face missing?' -> check flags for two-sided
- 'why is depth wrong?' -> check no-zwrite flag
- 'which batch has the bad UV?' -> indexStart/indexCount range
- 'why does this material show wrong texture?' -> textureIndex + path
Decodes:
- Blend modes: 0=opaque, 1=alpha-test, 2=alpha, 3=add
- Flags: 0x01=unlit, 0x02=two-sided, 0x04=no-zwrite
Falls back to a 'no batches (WOM1/WOM2 single-material model)' note
when called on an older-version model that doesn't have a batch
table.
Verified on a synthesized 2-batch model (opaque body + alpha hair
with two-sided + no-zwrite flags): all fields decoded correctly,
table aligns, JSON output enumerable.
Symmetric to --clone-creature and --clone-quest. Common workflow:
place one tree just right (rotation, scale dialed in), then clone N
copies along a path:
for x in 100 110 120 130 140; do
wowee_editor --clone-object $Z 0 $x 0 0
done
Defaults match --clone-creature: 5-yard X offset prevents z-fighting.
Rotation and scale carry over verbatim (so a tilted barrel stays
tilted). uniqueId is reset so the new placement doesn't collide with
the source's identifier in any downstream system that dedups by it.
CRUD-clone surface is now complete:
--clone-quest <zone> <questIdx> [newTitle]
--clone-creature <zone> <idx> [newName] [dx dy dz]
--clone-object <zone> <idx> [dx dy dz]
Verified: scaffolded zone, added a tree at (100, 200, 50, scale=1.5).
Cloned twice (default offset, custom offset). Result: 3 trees with
correctly-shifted positions and preserved scale=1.50.
Mirrors --export-glb (WOM -> .glb) for the WOB format. Buildings now
also reach the modern web 3D viewer ecosystem with zero conversion:
wowee_editor --export-wob-glb House # -> House.glb
wowee_editor --export-wob-glb House out.glb
Mapping for multi-group buildings:
- Per-group vertex arrays merged into a single global pool packed
into the BIN chunk (positions, normals, UVs interleaved by section).
- Each group becomes one primitive in a single mesh.
- Per-group indices offset by the group's vertex base so the merged
pool indexing still resolves to the right vertices.
- Per-group indices accessor sliced from a shared bufferView via
byteOffset (no buffer duplication).
- mode=4 (TRIANGLES), uint32 indices, vec3 float positions/normals,
vec2 float UVs — same layout as --export-glb.
Verified on a 2-group building (4-vert floor + 3-vert wall, 9
indices total): output .glb has 7 verts, 2 primitives with the
right per-group index counts (6 floor, 3 wall) sliced from the
shared 36-byte index bufferView. BIN = 7*32 + 9*4 = 260 bytes.
OBJ is universal but ancient (1992) — it can't carry skinning,
animations, or PBR materials. glTF 2.0 (2017, Khronos) is the
modern industry standard: every browser-based 3D viewer
(Sketchfab, Three.js, Babylon.js, model-viewer) consumes it
natively, plus Unity/Unreal import it cleanly.
wowee_editor --export-glb Tree # -> Tree.glb
wowee_editor --export-glb Tree out.glb
Shipping WOM through .glb means our open binary format is viewable
in any modern web tool with zero conversion friction. Big win for
the open-format ecosystem reach.
Implementation (single-file binary .glb):
- 12-byte header (magic 'glTF', version 2, totalLength)
- JSON chunk (0x4E4F534A 'JSON', padded to 4-byte boundary with spaces)
- BIN chunk (0x004E4942 'BIN\0')
- BIN layout: positions (vec3 float) | normals (vec3 float) |
uvs (vec2 float) | indices (uint32). 32 bytes/vert keeps the
index region naturally 4-byte aligned for free.
- Per WOM3 batch: one primitive with its own indices accessor
(sliced via byteOffset on a single shared bufferView).
- Position accessor includes min/max bounds for viewer auto-framing.
v1 limitations (deliberate):
- Bones / animations not yet emitted. glTF's joint matrix layout
differs from WOM's bone tree and needs a careful re-mapping pass;
shipping geometry-first means designers can use the format today
and the animation pass lands as a follow-up.
- No materials / textures emitted (those come from the texture
sidecars; future work to embed or reference them).
Verified: WOM(3 verts, 1 tri) -> .glb(108-byte BIN, 856-byte JSON,
1116-byte total). JSON is spec-compliant glTF 2.0 with correct
bufferView byteOffsets (0/36/72/96), componentTypes (5126=FLOAT,
5125=UNSIGNED_INT), and primitive mode=4 (TRIANGLES). Will open in
any glTF viewer without modification.
Shell-script-equivalent of looping over every zone in a project dir,
in a single editor invocation. Substitutes '{}' in the command with
each zone's path (find -exec convention):
wowee_editor --for-each-zone custom_zones -- \
wowee_editor --validate-all {}
wowee_editor --for-each-zone custom_zones -- \
wowee_editor --info-creatures {}/creatures.json
Implementation:
- Walks projectDir for child dirs containing zone.json (canonical
'is this a zone?' test the rest of the editor uses).
- Sorts results so output is deterministic across runs.
- Each token after '--' is shell-escaped via single-quote wrapping
with embedded ' replaced by '\'' — safe against names with spaces
and quotes.
- {} replacement done per-token so paths with spaces don't get split.
- fflush(stdout) before std::system so the [zone] header lands above
the child output instead of after (parent stdout buffered, child
unbuffered to terminal).
- Returns failed-run count as exit code (capped at 255 for POSIX).
Verified: scaffolded 2 zones, ran --info-creatures across both via
for-each-zone. Output ordered correctly with [zone] headers + child
output, exit 0 when all succeed.
Designer workflow: archetype one 'patrol guard' with stats + faction
+ behavior + equipment dialed in, then stamp it across spawn points
around a town:
for x in 100 110 120 130; do
wowee_editor --clone-creature $Z 0 "Guard $x" $x 200 50
done
Defaults give safe behavior:
- Position offset by (5, 0, 0) yards so the copy doesn't z-fight
the original when no offset is passed.
- Name appended with ' (copy)' if no newName given.
- id reset (auto-assigned by NpcSpawner).
- Patrol path NOT offset — patrol points are world-space waypoints,
not relative to the spawn; cloning that vector unchanged matches
designer intent. Re-author the path on the clone if needed.
Verified: scaffolded zone, added Guard, cloned twice (default
offset, custom name + 3D offset). list-creatures shows 3 spawns
with correctly differentiated positions and names.
Completes the proprietary-format inspector lineup. Pairs naturally
with --info-wot / --info-whm (the open WOT/WHM equivalents) so users
can verify the conversion preserves chunk counts, doodad placements,
and WMO references:
wowee_editor --info-adt World/Maps/Azeroth/Azeroth_32_48.adt
ADT: ...
version : 18
file bytes : 450192
coord : (0, 0)
chunks loaded : 256/256
height range : [-0.00, 0.00]
hole chunks : 0 (with cave/gap masks)
water chunks : 0
textures : 0
doodad names : 0 (0 placements)
wmo names : 1 (1 placements)
Reports loaded chunk count (out of fixed 256), height min/max across
all loaded chunks (with NaN guard so corrupted heights don't poison
the range), hole chunks (cave/gap masks), water chunks, texture and
doodad/WMO name table sizes, and placement counts.
Verified on a real WoW ADT (ahnqirajtemple_29_46.adt, 450KB):
correctly reports 256/256 chunks loaded, 1 WMO name + 1 placement.
JSON mode emits structured output for CI scripts.
The format-inspector lineup is now complete:
Proprietary: BLP / DBC / M2 / WMO / ADT
Open: PNG / JSON DBC / WOM / WOB / WOC / WOT/WHM / WCP
Every format on both sides of the open-format bridge has an inspector.
Common designer workflow: build a base quest with objectives + rewards
once, then make N variants ('Slay Wolves' / 'Slay Bears' / 'Slay
Tigers') with the same shape. Doing this through individual --add-*
commands means re-typing all objectives + items each time. --clone-quest
deep-copies an existing quest in one shot:
wowee_editor --clone-quest $Z 0 # 'Foo' -> 'Foo (copy)'
wowee_editor --clone-quest $Z 0 'Slay Bears' # custom title
Carries:
- All objectives (deep copy via vector value-copy)
- All item rewards
- XP / coin reward fields
- requiredLevel, giver, turnIn
Resets:
- id (set to 0; addQuest auto-assigns a fresh one)
- nextQuestId (cleared — chaining the clone to the same next quest
would corrupt chain semantics; user can re-set it explicitly)
Verified: scaffolded zone, added quest with 2 objectives + 1 item +
xp=250. Cloned twice (default name, custom name). Result: 3 quests
in list, both clones have full objective list and reward intact.
Round out the format-inspector lineup. The wowee open formats had
inspectors (--info-wom, --info-wob); these are the proprietary
counterparts that pair with --convert-m2 / --convert-wmo so users
can verify what the conversion preserves vs drops:
wowee_editor --info-m2 Character/Human/Male/HumanMale.m2
wowee_editor --info-wmo World/wmo/Stormwind/Stormwind.wmo
--info-m2 reports verts/tris, bones, sequences (animations),
batches, textures, materials, attachments, particles, ribbons,
collision tris, and bound radius. Auto-merges <base>00.skin if
present (WotLK+ M2s store geometry there) so vertex/index counts
match what gets rendered.
--info-wmo reports group count + portals + lights + doodads +
materials + textures + total verts/tris across loaded groups.
Auto-merges matching <base>_NNN.wmo group files; pre-resizes the
groups vector so loadGroup populates the right slots.
Verified against real WoW assets:
nexusraid_skya.m2: v264, 20917 verts, 22940 tris, 44 bones,
1 sequence, 44 batches, 28 textures, 42 materials.
ed_zd_ziggurat.wmo: v17, 1 group (1 loaded), 8 materials, 7
textures, 4609 verts, 3650 tris from the group file.
Bug caught during testing: initial snprintf used an 8-byte buffer
for '_NNN.wmo' (which is 8 chars + NUL = 9), silently truncating
to '_000.wm' and failing every group lookup. Bumped to 16 bytes
with a comment so the trap doesn't get re-stepped.
The --info-* suite covers wowee open formats and their JSON/PNG
sidecars. BLP itself was the gap — the proprietary source format
that asset_extract converts FROM. Useful for verifying source
files before --convert-blp-png:
wowee_editor --info-blp Texture.blp
BLP: Texture.blp (BLP2)
size : 128 x 128
channels : 4
format : BLP2
compression: DXT5
mip levels : 16
file bytes : 23044
decoded RGBA bytes: 65536
Magic-checks the first 4 bytes (BLP1 or BLP2) before invoking the
loader so feeding a non-BLP gets a clear 'not a BLP1/BLP2 file'
message instead of a confusing 'invalid' from the decoder. Reports
both file size (compressed-on-disk) and decoded RGBA byte count
(width*height*4) so users can see the compression ratio at a glance.
JSON mode emits the same fields for CI/scripting.
Verified on a real WoW BLP (pvp-banner-emblem-1.blp): correctly
identifies as BLP2 / DXT5 / 128x128 / 16 mips. Total format-
inspector lineup is now complete (BLP/PNG/DBC-bin/DBC-json plus
all 5 wowee binary formats).
--list-quests shows the quest roster but doesn't drill into objectives
or item rewards (--info-quests just counts). These complete the
detail picture and unblock --remove-quest-objective by exposing the
0-based objIdx the user needs:
wowee_editor --list-quest-objectives custom_zones/Z/quests.json 0
Quest 0 ('Hunt'): 3 objective(s)
idx type count target description
0 kill 5 Wolf Slay 5 Wolf
1 collect 3 Pelt Collect 3 Pelt
2 talk 1 Mayor Talk to Mayor
wowee_editor --list-quest-rewards custom_zones/Z/quests.json 0
Quest 0 ('Hunt') rewards:
xp : 999
coin : 5g 30s 99c
items : 2
[0] Item:Sword
[1] Item:Shield
Both have --json mode for CI/scripting. Range-check questIdx and
exit 1 on out-of-range with a precise message.
Verified: scaffolded zone, added quest with 3 objectives + 2 items
+ full coin reward; both listings show the right data, JSON output
is well-formed and enumerable.
Round out the tile lifecycle CLI. --add-tile shipped earlier; these
are the symmetric counterparts:
wowee_editor --list-tiles custom_zones/MyZone
wowee_editor --list-tiles custom_zones/MyZone --json
wowee_editor --remove-tile custom_zones/MyZone 31 30
--list-tiles: shows tile coords AND on-disk presence of .whm/.wot/.woc
per tile, so you can spot manifest-vs-disk drift before pack-wcp would
complain. JSON mode emits per-tile records for programmatic checks.
--remove-tile: drops the manifest entry AND deletes the .whm/.wot/.woc
files for that tile so the zone stays consistent (no orphan sidecars).
Refuses to remove the last tile — server module gen and pack-wcp both
expect at least one, so a zero-tile zone would just be a footgun. Use
'rm -rf custom_zones/X/' for total removal.
Verified end-to-end: scaffolded zone, added 2 more tiles, built WOC
on one. --list-tiles shows 3 tiles with correct file presence (y/y/y
on the built one, y/y/- on the others). Removed (31, 30) — 2 files
deleted, 2 tiles remaining. Tried removing last tile after dropping
to 1: correctly refused with exit 1.
Companion to --convert-dbc-json and --convert-json-dbc. Refresh one
PNG sidecar without re-running asset_extract --emit-open across the
whole tree:
wowee_editor --convert-blp-png Texture.blp # -> Texture.png
wowee_editor --convert-blp-png Texture.blp out.png # custom path
Same code path as the asset_extract emitter (BLPLoader::load +
stbi_write_png), with the same dimension/buffer guards (rejects
0x0 or >8192x8192, rejects images where data is shorter than
width*height*4).
Verified on a real WoW BLP (Data/interface/pvp-banner-emblem-1.blp,
128x128 RGBA): converted cleanly, --info-png on the result reports
'128 x 128 / 8-bit / rgba (4 channels) / 24136 file bytes' (PNG
zlib compression on a sparse banner texture).
Symmetric counterpart to --add-quest-objective. Quest design is
iterative — when an objective gets reworked or trimmed, you need
to remove the wrong one without nuking the entire quest.
wowee_editor --remove-quest-objective <zoneDir> <questIdx> <objIdx>
Both indices are 0-based and reported by --info-quests / --list-quests.
Range-checks both: questIdx against quest count, objIdx against the
selected quest's objective count. Each errors with a precise out-of-
range message.
Verified: scaffolded zone, added quest with 3 objectives (kill /
collect / talk), removed objective at index 1 (the collect),
--info-quests confirms '1 kill, 0 collect, 1 talk' afterward.
Bad quest and bad objective indices both rejected with exit 1.
Quest-authoring CLI is now fully symmetric:
--add-quest --remove-quest
--add-quest-objective --remove-quest-objective
--add-quest-reward-item (additive only — items are dedup'd)
--set-quest-reward (idempotent field-level update)