--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.
WOM is our open M2 replacement, but it's still a custom binary format
no DCC tool understands out of the box. OBJ is the universally
supported text format that opens directly in Blender, MeshLab,
ZBrush, Maya — basically every 3D tool ever made:
wowee_editor --export-obj Tree # writes Tree.obj
wowee_editor --export-obj Tree out.obj # custom output path
This closes the loop for content authors:
asset_extract -> WOM (open binary) -> OBJ (universal text) ->
edit in Blender -> back to WOM via a future --import-obj.
Layout details that matter for downstream tools:
- 1-based face indices (OBJ standard)
- UV V flipped (1.0 - v) so texturing matches between Vulkan
top-left and Blender bottom-left conventions
- Per-batch groups when WOM3 batches exist, named with the
texture basename so material assignment carries through
- Single 'mesh' group for WOM1/WOM2 models
- Header comment preserves provenance (source, version, counts)
Verified on a synthesized 5-vert pyramid (4 base + apex, 6 tris):
output OBJ has 5 v / 5 vt / 5 vn entries, 6 f lines, opens cleanly
in MeshLab. Build green, ctest 31/31.
Verbose enumeration of every entry, complementing the existing
--info-* (summary counts) and unblocking --remove-* (which takes a
0-based index).
wowee_editor --list-creatures custom_zones/Z/creatures.json
creatures.json: custom_zones/Z/creatures.json (2 total)
idx name lvl display pos (x, y, z)
0 Wolf 7 11430 (100.0, 200.0, 50.0)
1 Black Bear 12 11445 (110.0, 210.0, 55.0)
The 'idx' column is exactly what --remove-creature/object/quest
takes, closing the workflow loop:
wowee_editor --list-creatures $Z/creatures.json |
grep -i 'wolf' | awk '{print $1}' |
xargs -I{} wowee_editor --remove-creature $Z {}
Each command has matching --json mode that emits per-entry records
with index, key fields, and position. Verified on a synthesized
zone with 2 creatures + 2 objects + 2 quests; tables align, indices
match, JSON is well-formed and round-trips through jq cleanly.
Symmetric counterparts to --add-{creature,object,quest} from earlier
batches:
wowee_editor --remove-creature <zoneDir> <index>
wowee_editor --remove-object <zoneDir> <index>
wowee_editor --remove-quest <zoneDir> <index>
Index is 0-based and is reported in the corresponding --info-* output;
nothing identifies entries reliably across reloads, so name-based
removal would silently delete the wrong row when duplicates exist.
Pair them in scripts:
idx=$(wowee_editor --info-creatures $Z/creatures.json --json |
jq '.spawns | map(.name) | index("Wolf")')
wowee_editor --remove-creature $Z $idx
Each prints the removed entry's name/path and the new total so
scripts can verify the right row went away. Out-of-range indices
exit 1 with a clear message.
Verified end-to-end: scaffolded zone, added 3 creatures + 2 objects
+ 2 quests, removed one of each by index, totals correctly went
3->2/2->1/2->1. Bad index 99 properly errors out.
--info-extract reports sidecar coverage % per format, but doesn't say
which files are missing. This makes it actionable:
wowee_editor --list-missing-sidecars Data/
Output: one path per line, prefixed with the missing extension so
shell tools can filter:
png Data/Textures/Skybox/Sky01.blp
json Data/DBFilesClient/SoundEntries.dbc
wom Data/Character/Human/Male/HumanMale.m2
Pipe into xargs to drive a targeted re-extract:
wowee_editor --list-missing-sidecars Data/ |
awk '/^png/ {print $2}' |
xargs asset_extract --emit-png-only
Skips WMO group files (Foo_NNN.wmo) since only the parent file gets
a .wob sidecar — they would otherwise inflate the missing list with
hundreds of false positives per WMO.
Exit 1 when anything is missing (so CI can gate). JSON mode emits
arrays per format type for programmatic consumption. Verified
against synthetic dir with 5 files (4 lacking sidecars + 1 with
.png present): all 4 reported, the one with sidecar correctly
omitted.
Round out the per-format validator suite. Open-format zone validation
now covers all four binary formats:
--validate-wom Tree
--validate-wob House
--validate-woc terrain.woc
--validate-whm Zone_28_30
--validate-all custom_zones/Zone1 # runs everything
WOC checks: finite vertex coords on every triangle, no degenerate
triangles (two verts identical), known flag bits only (0x0F mask),
tile coords within WoW grid (< 64), bounds.min <= bounds.max.
WHM/WOT checks: finite heights across all 145 verts/chunk, finite
chunk position vectors, tile coord in [0, 64), reasonable height
envelope ([-10000, 10000] is a generous outer bound — beyond that
suggests units confusion), placements have finite positions and
nameId within doodadNames/wmoNames table size.
validate-all now reports all four format counts (WOM/WOB/WOC/WHM)
and aggregates errors. Verified end-to-end: a fresh scaffolded zone
with --build-woc yields 256/256 chunks loaded, 32768 walkable
triangles, validate-all PASSED. Synthesized WOC with 0xFF flags
correctly fails with 'unknown flag bits 0xFF' and exit 1.
Walks a directory recursively and runs the deep validators on every
.wom and .wob it finds. Single CI gate for an entire zone tree:
wowee_editor --validate-all custom_zones/MyZone --json
Reports per-file failures (capped at first 20 to keep output bounded)
plus aggregate counts so you know which file to drill into with
--validate-wom or --validate-wob individually.
Refactor: pulled the validation bodies out of --validate-wom and
--validate-wob into static helpers (validateWomErrors / validateWobErrors)
returning vector<string>. The per-file commands now share the same
logic as --validate-all — fix one, fix all three. ~200 lines of
duplicate validation code consolidated.
Verified end-to-end: seeded /tmp dir with 2 WOMs (1 with DAG bone
violation) + 1 valid WOB, --validate-all reports 'WOM: 2 total, 1
failed' / 'WOB: 1 total, 0 failed' with the bad file's full path and
error printed below. JSON mode emits per-file failure list for CI.
Companion to --validate-wom; same load-is-lenient story for buildings:
wowee_editor --validate-wob House [--json]
Cross-references checked per group:
- indices.size() divisible by 3
- every index < vertices.size()
- material textures non-empty
- group boundMin <= boundMax per axis
Building-level:
- portal groupA/groupB references real groups (or -1 = exterior)
- portal polygon has >= 3 vertices
- doodad modelPath non-empty
- doodad scale finite and > 0
- boundRadius >= 0
Errors batched (caps to 3 listed plus '... and N more') so a thousand
bad indices in one group don't drown the report. JSON mode emits the
same error list for CI consumption. Exit 1 on any failure so shell
scripts can gate on it.
Verified against a synthesized 2-group building with intentional
flaws across every category — caught all 4 (group indices count,
empty material texture, short portal polygon, empty doodad model)
and exited 1.
The WOM loader is intentionally lenient — it clamps out-of-range
indices to 0, resets bad bone parents to -1, and skips invalid
batches. That keeps broken files from crashing the renderer, but
also hides corruption that authoring scripts should catch BEFORE
the file ships.
wowee_editor --validate-wom Tree [--json]
Cross-references checked (none auto-fixed by load()):
- bone DAG order: parent must be strictly less than self index
- animation boneKeyframes count == bone count when both nonzero
- batch indexStart+Count <= total indexCount
- batch indexCount divisible by 3
- batch textureIndex < texturePaths size
- boundMin <= boundMax per axis, boundRadius >= 0
- header version in [1,3], indices count divisible by 3
Verified against a synthesized 3-bone model with parent=self+1
(invalid DAG order) — load() preserves it as written, validator
reports 'bone 1 parent=2 not strictly less (DAG order)' and
exits 1. JSON mode emits errorCount + errors[] + passed boolean
for CI scripts to gate on.
Duplicate an existing zone to a new slug:
wowee_editor --copy-zone custom_zones/Original "My New Zone"
Workflow this enables: scaffold one base zone, populate it with
creatures/objects/quests, then copy-zone N times to create variants
without re-scaffolding each. Designers can template a 'forest base'
zone and stamp it into Dark Forest, Frozen Forest, etc.
What it does:
- Recursive copy preserves any subdirs (e.g. data/ for DBC sidecars)
- Reads source slug from zone.json (not the dir name) to know what
prefix to rewrite — handles users who renamed dirs without
touching the manifest
- Renames slug-prefixed files (Original_28_30.whm -> NewSlug_28_30.whm,
matches both _-suffixed and .-suffixed forms)
- Saves a fresh zone.json via ZoneManifest::save which rebuilds the
files-block from mapName, so the manifest references the renamed
files correctly
Verified end-to-end: scaffolded Original, added creature + quest,
copied to 'My New Zone'. Result: 2 files renamed, zone.json
mapName/displayName/files all updated, creatures.json + quests.json
copied verbatim.
Third headless authoring command, finishing the trio:
wowee_editor --add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]
Optional positional fields are read in order; omit from the right.
Bare 'wowee_editor --add-quest zone Title' produces a valid quest
with default values, matching the editor GUI's New Quest behavior.
Verified: appended 3 quests (250+100+0 XP) to a scaffolded zone,
--info-quests --json reports total=3, totalXp=450, withReward=3.
Mirrors --add-creature for the object placer:
wowee_editor --add-object <zoneDir> <m2|wmo> <gamePath> <x> <y> <z> [scale]
Lets shell scripts populate objects/buildings without the GUI:
for tree in 100,200 150,250 200,300; do
x=${tree%,*}; y=${tree#*,}
wowee_editor --add-object "$zone" m2 'World/Doodad/Tree.m2' $x $y 0
done
Loads existing objects.json first then appends, so multiple
invocations build up. Optional scale slot lets the caller set
non-default size (clamped to >0 by the load-time guard).
Verified end-to-end: scaffolded zone → added m2 + wmo → info-objects
reports total=2 with the correct scale range.
Appends a single creature spawn to a zone's creatures.json. First
real authoring tool that doesn't need the GUI placement system —
useful for batch-populating zones via shell script:
for npc in goblin spider wolf; do
wowee_editor --add-creature "$zone" "$npc" 100 200 50
done
Args: <zoneDir> <name> <x> <y> <z> [displayId] [level]
- displayId 0 → SQL exporter substitutes 11707 (generic humanoid)
- level defaults to 1
- Coordinates are render-space (renderX=wowY, renderY=wowX)
Loads any existing creatures.json first then appends, so multiple
invocations build up the file. The standard NPC spawner caps
(50k creatures) protect against runaway scripts.
Adds --json to --info-creatures, --info-objects, --info-quests so
the per-content inspectors match the per-zone aggregator. Schemas
match the existing --zone-summary --json sub-objects.
Now 9 inspectors support --json:
--info-extract --info-wcp --validate
--info-creatures --info-objects --info-quests
--zone-summary --list-zones --diff-wcp
CI can now drill into per-content reports the same way as the
top-level summary, e.g. fail a build if a zone's quests have any
chain errors:
wowee_editor --info-quests "$zone/quests.json" --json \
| jq -e '.chainErrors | length == 0'
Adds JSON mode to the zone discovery scanner. Returns an array of
zone objects, each with name/dir/mapId/author/description/tiles/
hasCreatures/hasQuests.
Lets CI scripts iterate every available zone and run a per-zone
gate, e.g.:
for zone in $(wowee_editor --list-zones --json | jq -r '.[].directory'); do
wowee_editor --validate "$zone" --json | jq -e '.score == 7'
done
Fifth and last commonly-used inspector to gain --json mode (after
--info-extract, --validate, --info-wcp, --zone-summary).
Adds --json output to the one-shot zone-summary aggregator. Refactor
also moves creature/object/quest data reads to a shared step before
either branch so both human and JSON outputs use the same numbers.
Schema:
{
"zone": "custom_zones/Foo",
"score": 3, "maxScore": 7,
"formats": "WOT WHM zone.json ",
"counts": { "wot":1, "whm":1, "wom":0, "wob":0, "woc":0, "png":0 },
"creatures": { "total":N, "hostile":N, "questgiver":N, "vendor":N },
"objects": { "total":N, "m2":N, "wmo":N },
"quests": { "total":N, "chainWarnings":N }
}
Now CI can gate on any combination — open-format coverage, NPC
counts, quest chain health — from a single command. Fourth and
last commonly-CI'd inspector to gain --json mode (after
--info-extract, --validate, --info-wcp).
Mirrors --info-extract --json and --validate --json. Schema:
{
"wcp": "/path/to/zone.wcp",
"name": "Wj",
"author": "...",
"description": "...",
"version": "1.0",
"format": "wcp-1.0",
"mapId": 9000,
"fileCount": 3,
"totalBytes": 177671,
"categories": { "terrain": 2, "data": 1 }
}
Lets CI scripts inspect packed zones — e.g. fail a release if a
zone's WCP doesn't include a creature category, or auto-tag a
release with the totalBytes field.
Adds an optional --json flag that emits a structured nlohmann JSON
object instead of the human-readable text. Schema:
{
"dir": "...",
"totalBytes": N, "proprietaryBytes": N, "openBytes": N,
"overallCoverage": 100.0,
"blp_png": { "proprietary": N, "sidecar": N, "coverage": % },
"dbc_json": { ... },
"m2_wom": { ... },
"wmo_wob": { ... },
"adt_whm": { ... }
}
Lets CI scripts gate on coverage:
cov=$(wowee_editor --info-extract Data --json | jq .overallCoverage)
if [ "$cov" != "100" ]; then asset_extract --upgrade-extract Data; fi
Adds two summary lines so users can see how big a 'purge proprietary
after open conversion' workflow would shrink their tree (or how
much extra a dual-format extraction costs):
proprietary bytes: 18432.4 MB
open-format bytes: 21340.7 MB (115.8% of proprietary)
Counts every BLP/DBC/M2/WMO/ADT into the proprietary bucket and
every PNG/JSON/WOM/WOB/WHM/WOT/WOC into the open bucket. The
ratio surfaces things like 'PNG is bigger than DXT-compressed BLP'
or 'JSON DBC is much smaller than the binary' without the user
having to run du themselves.
Walks an extracted asset tree and reports per-format counts plus
how many proprietary files have a wowee open-format sidecar.
Lets users (or CI) see at a glance whether asset_extract was run
with --emit-open and how complete the open-format coverage is:
BLP textures : 12340 (12340 PNG sidecar = 100.0% open)
DBC tables : 240 (240 JSON sidecar = 100.0% open)
M2 models : 8500 (0 WOM sidecar = 0.0% open)
...
overall open-format coverage: 41.2%
(run `asset_extract --emit-open` to fill missing sidecars)
Skips _NNN group sub-files when counting WMOs (only the root WMO
ships with a WOB sidecar). The headless CLI is now at 22 commands.
Loads + immediately re-saves zone.json, creatures.json,
objects.json, and quests.json. The load-time scrubs (NaN,
out-of-range, oversize) and save-time caps fire on the round-trip,
producing a cleaned-up zone without ever opening the GUI.
Useful when an old zone was created before recent hardening
batches — running this once normalizes the on-disk state to match
what the current loaders expect.
Mirrors the other --info-* family inspectors. Accepts either a
zone directory or the zone.json path directly. Prints every
manifest field: name, mapId, biome, baseHeight, tiles, flags,
audio config. Useful when diffing two zones or auditing the
audio/flag setup before packing.
Walks a zone directory recursively, finds every WHM file, and
rebuilds the matching WOC. Useful after batch terrain edits when
you want to refresh collision for many tiles in one shot. Reports
per-tile triangle counts and exits 1 if any rebuild failed.
Combines validate + creature/object/quest counts in a single
output. Useful for CI reports and quick sanity checks. Exits 0
if open-format score is 7/7 (full coverage), 1 otherwise.
Compares two WCP archives file-by-file from their info JSON: lists
added (+), removed (-), and size-changed (~) entries. Useful for
verifying that an authoring tweak changed only what it claimed to
change, and for editor-version regression detection. Exit code 0
if identical, 1 otherwise.
Renders heightmap, normal-map, and zone-map PNGs alongside a
WHM/WOT terrain pair. Useful for portfolio screenshots, ground-
truth map comparison, and quick visual validation without
launching the GUI.
Loads a WHM/WOT terrain pair and writes a .woc collision mesh
alongside it. Terrain triangles only (no WMO overlays — those need
the asset manager) but enough for first-pass walkability while
authoring.
Verified end-to-end: scaffold-zone → build-woc → info-woc reports
32k triangles for a flat 256-chunk tile.
Mirrors --unpack-wcp. Accepts either a zone name (auto-resolved
under custom_zones/ then output/) or a directory path. Default
output is <name>.wcp in the current directory. Combined with
--scaffold-zone and --unpack-wcp, the editor can do the full
zone authoring round-trip from the command line.
Mirrors --info-wcp / --list-wcp. Default destination is
custom_zones/ (matches the GUI's preferred location). With both
this and --scaffold-zone, the editor binary can fully bootstrap
a zone install without launching the GUI.
Creates custom_zones/<slug>/ with a flat-terrain WHM, default WOT,
and minimal zone.json — score 3/7 on --validate, ready to open in
the GUI for further authoring. Saves the round-trip of launching
the GUI just to make a starter directory.
Previously '--info-wcp' (no path) silently dropped into the GUI
because the option-parse loop's i+1<argc guard hid the typo. Pre-
scan and bail out with a helpful message before trying to start
the editor, so users get fast feedback on bad invocations.