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)
--scaffold-zone creates a zone with one tile; some zones (continent
fragments, large dungeons) span multiple ADT tiles. This extends an
existing zone:
wowee_editor --add-tile custom_zones/MyZone 29 30 # default baseHeight=100
wowee_editor --add-tile custom_zones/MyZone 28 31 250.5 # custom baseHeight
What it does:
- Generates a fresh blank-flat WHM/WOT pair via the same factory
--scaffold-zone uses, so output is consistent.
- Appends (tx, ty) to ZoneManifest::tiles. Save() rebuilds the
files-block from tiles, so the new adt_TX_TY entry appears
automatically in zone.json.
Safety:
- Tile coord must be in WoW grid [0, 64) per axis; rejects 99,99.
- Refuses if the tile is already in the manifest (catches typos).
- Refuses if the .whm/.wot files exist on disk but aren't in the
manifest (catches manifest-out-of-sync drift from hand edits).
- Optional baseHeight allows seeding flat terrain at a non-default
elevation.
Verified end-to-end: scaffolded 1-tile zone, added 2 more tiles
(one with custom height). Result: 3 tiles in manifest, 6 files on
disk, files-block has all 3 adt_TX_TY entries. Duplicate and
out-of-range cases both rejected with exit 1.
Standalone DBC <-> JSON converters. asset_extract --emit-open already
does this in bulk during extraction, but designers often need to
refresh ONE sidecar after a hand-edit, or push an edited JSON back to
binary DBC for private-server compat:
wowee_editor --convert-dbc-json Spell.dbc # -> Spell.json
wowee_editor --convert-json-dbc Spell.json # -> Spell.dbc
wowee_editor --convert-dbc-json Spell.dbc out.json # custom output
DBC -> JSON: uses the wowee-dbc-json-1.0 schema (matches asset_extract
output exactly so the editor's runtime overlay loader picks it up).
Same string/float/uint heuristic the existing emitter uses.
JSON -> DBC: builds a Blizzard-format-compatible binary DBC with:
- 20-byte WDBC header (magic + recordCount + fieldCount + recordSize +
stringBlockSize)
- Records as little-endian uint32 fields (always LE per spec)
- Deduped string block with leading NUL so offset=0 = empty string
- Tolerates JSON numeric types (string/float/int/bool/null) and
derives fieldCount from row size if header omits it
Verified with a hand-built 3-record/2-field DBC: DBC->JSON->DBC->JSON
round-trips with identical records[] arrays, no data loss. Output
is byte-loadable by the existing DBC parser.
Completes the open-format -> universal-text bridge for the last
binary geometry format. WHM was the missing one; designers now have
OBJ exports for all four (WOM models, WOB buildings, WOC collision,
WHM terrain).
wowee_editor --export-whm-obj custom_zones/MyZone/MyZone_30_30
Mesh layout:
- 9x9 outer vertex grid per chunk (skips the 8x8 inner verts the
engine uses for 4-tri fans). That's 81 verts and 128 tris per
chunk; full ADT = 20736 verts + 32768 tris.
- One OBJ 'g chunk_X_Y' per MapChunk so designers can hide chunks
individually in Blender (e.g. to inspect a single problem area).
- Hole bits respected — cave-entrance quads correctly disappear.
- Coords match WoweeCollisionBuilder's outer-grid layout exactly,
so an --export-whm-obj and --export-woc-obj of the same source
align spatially when overlaid in Blender. (Verified: first vertex
of both is (1066.67, 1066.67, ~98.5) for a tile (30, 30) export.)
- UVs are simply row/8, col/8 in [0,1] per chunk so a checker
texture renders at the canonical scale for size reference.
Verified: scaffolded zone -> WHM/WOT auto-built -> --export-whm-obj
produces 256 chunks loaded, 20736 verts, 32768 faces, 256 'g'
blocks. Counts exactly match the chunk × outer-grid math.
--copy-zone duplicates and renames; --rename-zone does it in place
without doubling disk usage. Useful when fixing typos or rebranding
a zone without touching its data:
wowee_editor --rename-zone custom_zones/Old 'Brand New Name'
What changes:
- zone.json mapName: Old -> Brand_New_Name
- zone.json displayName: 'Old' -> 'Brand New Name'
- zone.json files block (adt_NN_NN, wdt): regenerated from new slug
- slug-prefixed files: Old_28_30.whm -> Brand_New_Name_28_30.whm
- the directory itself: custom_zones/Old/ -> custom_zones/Brand_New_Name/
Order of operations matters for crash safety: slug-prefixed files
get renamed first (atomic per-file via fs::rename), then the
manifest is rewritten in the still-named source dir, then the
directory is moved last. If the dir-move fails we surface the
manifest-already-updated state so the user can recover.
Refuses to run if target dir already exists (avoids silent merge),
or if both slug AND displayName already match the target (no-op).
Verified end-to-end: scaffolded 'Old' with 1 creature, renamed to
'Brand New Name'. Result: dir renamed, .whm/.wot files renamed,
zone.json fully updated, creatures.json preserved with 1 entry.
WOC is the open collision format used for movement queries. When the
player gets stuck or walks somewhere they shouldn't, you need to SEE
the mesh — eyeballing flag bits in --info-woc only goes so far.
wowee_editor --export-woc-obj custom_zones/Z/Z_30_30.woc
Each flag class gets its own OBJ 'g' block so designers can hide
categories independently in Blender to debug:
- walkable / steep / water / indoor / nonwalkable
- and combinations like 'walkable_water' for shallow swimable areas
Implementation notes:
- Triangle-soup topology preserved (no vertex dedupe). Adjacent
triangles often have different flags; deduping would merge
categories and lose the boundary.
- Bucket by exact flag value (uint8) so combination flags become
their own group rather than counting twice.
- Index math: triangle t vertex k -> OBJ index (t*3 + k + 1).
- Header comment preserves source path, triangle counts, walkable/
steep counts, and tile (x, y) for provenance.
Verified on a freshly-scaffolded zone's auto-built WOC (32768
triangles, all walkable on flat terrain): export reports
'1 flag class(es)', single 'g walkable' block in the OBJ, last
face index correctly = 32768*3 = 98304.
Closes the WOB <-> universal-format round trip, mirroring what
--import-obj does for WOM. Workflow now works end-to-end for
buildings:
asset_extract # WMO -> WOB (open binary)
--export-wob-obj # WOB -> OBJ (universal text)
... edit in Blender / MeshLab / Maya ...
--import-wob-obj # OBJ -> WOB (back to engine format)
--validate-wob # confirm consistency
Mapping handles:
- Each OBJ 'g name' starts a new WOB group; faces under it become
that group's indices. Default 'imported' group catches faces
before any 'g' directive (raw OBJ files from non-DCC sources).
- Per-group dedupe table on (pos, uv, normal) triples — each WOB
group has its own local vertex array, so the global OBJ index
pool gets remapped per group.
- '_outdoor' suffix stripped + isOutdoor flag set, mirroring
--export-wob-obj's naming convention.
- Doodad placements recovered from the # doodad ... comment lines
--export-wob-obj writes; round-trip preserves them via re-parse.
- UV V flipped back (1.0 - v) so the round trip is exact.
- Per-group bounds + global bound radius computed from positions.
Verified: WOB(2 groups, 7 verts, 3 tris, 1 doodad) -> OBJ ->
WOB(2 groups, 7 verts, 3 tris, 1 doodad). validate-wob clean,
info-wob shows the same counts. Materials and portals don't
survive the OBJ trip (no semantic equivalent) — that's documented.
Closes the quest-authoring CLI: --add-quest creates a quest, then
--add-quest-objective adds completion conditions, and now these add
the rewards.
wowee_editor --add-quest-reward-item $Z 0 'Item:Sword' 'Item:Shield' 'Item:Potion'
wowee_editor --set-quest-reward $Z 0 --xp 999 --gold 5 --silver 30
--add-quest-reward-item greedy-consumes any trailing positional args
that don't start with '-', so a whole loot table can be added in one
invocation rather than N round-trips through load+save.
--set-quest-reward updates only the fields explicitly passed (--xp /
--gold / --silver / --copper) — avoids the round-trip-and-clobber
footgun of a 'replace whole reward' command. Field flags are
order-independent. At least one flag is required (else error).
Both validate questIdx against quest count and bail with exit 1 on
out-of-range / missing file / bad numeric value.
Verified: scaffolded zone, added quest with default xp=100, batch-
added 3 item rewards in one shot, then set xp=999/gold=5/silver=30
in one shot. --info-quests reports total XP=999, with-items=1.
PNG (BLP sidecar) and JSON DBC (DBC sidecar) didn't have inspectors —
the existing --info-* suite only covered the binary native formats
(WOM/WOB/WOC/WOT/WHM/WCP). These fill the gap so debugging the
asset_extract --emit-* output doesn't need GIMP / a JSON pretty-printer:
wowee_editor --info-png Textures/Sky01.png
PNG: Textures/Sky01.png
size : 256 x 256
bit depth : 8
color : rgba (4 channels)
file bytes: 142336
wowee_editor --info-jsondbc db/Spell.json
JSON DBC: db/Spell.json
format : wowee-jsondbc-1
source : Spell.dbc
records : 47882 (header) / 47882 (actual)
fields : 234
PNG inspector reads only the IHDR chunk (24 bytes total) — no pixel
decode — so it works instantly on huge files. Validates the
8-byte signature, parses big-endian width/height, derives channel
count from PNG color type (grayscale/rgb/palette/grayscale+alpha/
rgba per spec table 11.1).
JSON DBC inspector parses with nlohmann::json, reports the schema
fields (format/source/recordCount/fieldCount), and cross-checks
recordCount against actual records[] array length. Exits 1 on
count mismatch — catches truncated extracts where the header lies
about how much data follows. Verified with hand-rolled 2x3 RGBA
PNG (correct dims) and JSON DBC files (one matched, one mismatched
99 vs 1).
WOM (the open M2 replacement) already round-trips through OBJ; this
extends the universal-format bridge to WOB (the open WMO replacement)
so buildings can also be edited in Blender / MeshLab / Maya / etc.
wowee_editor --export-wob-obj House # writes House.obj
wowee_editor --export-wob-obj House out.obj # custom path
Mapping decisions:
- Each WOB group becomes one OBJ 'g' block (named after the group;
outdoor groups get an '_outdoor' suffix). Preserves the room/floor
structure for downstream selection and per-area editing.
- Single global vertex pool with per-group offsets (OBJ requires v
indices to be globally 1-based; we track a running vertOffset).
- UV V flipped (1.0 - v) so texturing matches Blender bottom-left
convention, same as --export-obj for WOM.
- Doodad placements written as # comment lines at the end. OBJ has
no native concept for instanced models, but emitting them as
structured comments keeps the placement data recoverable for
tools that want to re-instance them.
- Portals and material flags drop on the floor — OBJ has no
semantics for either. The native WOB always remains canonical.
Verified on a synthesized 2-group house (4-vert floor + 3-vert wall,
1 doodad): output OBJ has 7 verts / 7 vt / 7 vn entries, 2 'g'
blocks with proper index offsetting, doodad comment line preserved.
--add-quest creates a quest shell but quests with no objectives never
complete in-game. This fills the gap:
wowee_editor --add-quest-objective <zoneDir> <questIdx> \
<kill|collect|talk|explore|escort|use> <targetName> [count]
Workflow:
Z=custom_zones/MyZone
wowee_editor --add-quest $Z 'Hunt Wolves' 100 100 250 5
wowee_editor --add-quest-objective $Z 0 kill 'Wolf' 5
wowee_editor --add-quest-objective $Z 0 collect 'Wolf Pelt' 3
Auto-generates a description from type+count+name so addons and
tooltips show something sensible ('Slay 5 Wolf', 'Collect 3 Wolf
Pelt', 'Talk to Mayor'). Designers can hand-edit quests.json after
the fact for bespoke prose; the auto-text is the floor, not the
ceiling.
Quest index matches --list-quests output (0-based). Out-of-range
index, unknown type, or missing quests.json all error with clear
messages and exit 1.
Verified: scaffolded zone, added 2 quests + 3 objectives across
them; --info-quests reports '1 kill, 1 collect, 1 talk'. Bad type
and bad index both rejected.
The natural counterpart to --diff-wcp (which compares two .wcp
archives) — operates on the unpacked side of the workflow. Shows
exactly what changed across zone.json fields, creature roster,
object placements, and quest list:
wowee_editor --diff-zone custom_zones/Base custom_zones/Variant
Diff: custom_zones/Base vs custom_zones/Variant
manifest : 2 field diff(s)
~ mapName: 'Base' -> 'Variant'
~ displayName: 'Base' -> 'Variant'
creatures : 2 vs 2
- Bear
+ Tiger
quests : 1 vs 2
+ Defeat the Tiger
Pairs naturally with --copy-zone: template a base zone, fork a
variant, then diff to see exactly what was customized. Useful for
PR review when a designer modifies a zone — diff against the
upstream version to scope the change.
Comparison strategy: sorted set diff on stable identifying fields
(creature.name, object.path, quest.title). This intentionally hides
position/orientation changes since those are continuous and would
flag every pixel-perfect tweak as a diff — content-level changes
are the signal here.
Exit 0 if identical, 1 otherwise (so CI can gate). JSON mode emits
per-category onlyA/onlyB arrays + manifestDiffs list + totalDiffs
count for programmatic consumption.
Verified: diffed two zones forked from a common base (one with
creature swap + new quest); reported 5 diffs across manifest +
creatures + quests with exit 1. Same zone vs itself reports
IDENTICAL with exit 0.
Closes the open-format authoring loop:
asset_extract # M2 -> WOM (open binary)
--export-obj # WOM -> OBJ (universal text)
... edit in Blender / MeshLab / ZBrush / Maya ...
--import-obj # OBJ -> WOM (back to engine format)
The same WOM file ships in custom zones, gets validated by
--validate-wom, and renders identically through the existing pipeline
— no proprietary M2 ever needs to touch the authoring path.
Parser handles:
- v / vt / vn pools, deduped on (pos, uv, normal) triples so the
resulting WOM vertex buffer stays compact
- 1-based AND negative (relative) face indices
- f tokens in v, v/t, v//n, and v/t/n forms
- Triangles, quads, and convex n-gons (fan-triangulated)
- CRLF line endings
- Reverses --export-obj's V flip (1.0 - v) so UVs round-trip exactly
- Auto-computes boundMin/Max/Radius from positions (renderer culls
by these — wrong values make the model disappear)
- Output WOM is WOM1 (static); bones/anims/material flags don't
exist in OBJ and stay empty by design
Verified end-to-end: WOM -> OBJ -> WOM -> validate-wom yields a
5-vert, 6-tri pyramid back identical to the input. Bounds, vertex
count, index count, and name all preserved.