Per-tile content density. Catches sparse zones (5 mobs across 16
tiles → boring) and over-stuffed ones (200 mobs in 1 tile → frame-
rate bomb). Reports overall averages plus per-tile bucket counts:
wowee_editor --info-zone-density custom_zones/MyZone
Zone density: custom_zones/MyZone
tiles : 4
totals : 47 creatures, 23 objects, 8 quests
per-tile : 11.75 creatures, 5.75 objects, 2.00 quests
Per-tile breakdown:
tile creatures objects
(28, 30) 12 7
(29, 30) 18 4
(29, 31) 9 8
(30, 30) 8 4
Spawn-to-tile bucketing reverses the WoW grid transform from world
position back to tile (tx, ty) coords. Out-of-zone spawns silently
drop (they show up in --check-zone-refs / --check-zone-content as
their own warning class).
Useful for difficulty-curve work ('how packed is this hub vs this
zone-edge?'), perf budgeting ('which tile do I need to lod-out
first?'), and content-pacing reviews ('is the early game too empty?').
JSON mode emits per-tile records for dashboards. Verified on a
1-tile mvp-zone: 1 creature + 1 object + 1 quest, all bucketed
into the correct tile (28, 30).
Object-side counterparts to --info-creatures-by-faction/-level. Two
analytics commands for placement audits:
wowee_editor --info-objects-by-path $Z/objects.json
Objects by path: ... (47 total, 12 unique)
count share path
18 38.3% World/Generic/Tree.m2
9 19.1% World/Generic/Lamp.m2
4 8.5% World/Building/Inn.wmo
...
wowee_editor --info-objects-by-type $Z/objects.json
Objects by type: ...
M2 : 38 (scale 0.80-2.50, avg 1.12)
WMO : 9 (scale 1.00-1.00, avg 1.00)
--info-objects-by-path: most-used model paths first. Catches
'this looks repetitive, diversify the doodads' design feedback
and surfaces texture-budget hot spots (one model used 50× pulls
its textures into the working set 50×).
--info-objects-by-type: M2 vs WMO split + per-type scale stats.
Catches scale outliers ('this WMO is at 0.001 scale, did you mean
1.0?') and gives composition sense (mostly props vs mostly
buildings).
JSON modes emit per-path / per-type records for dashboards.
Verified on a 4-object seed (3 M2 + 1 WMO with mixed scales):
correctly reports Tree.m2 used 2× (50%), M2 scale range 0.80-1.50
avg 1.10.
Aggregates water-layer stats across every tile in a zone. Useful for
confirming a 'lake zone' actually has water, budgeting water-heavy
zones, or auditing what liquid types appear (water vs ocean vs magma
vs slime affects gameplay rules):
wowee_editor --info-zone-water custom_zones/Z
Zone water: custom_zones/Z
loaded tiles : 1
water chunks : 0 (out of 256 possible)
total layers : 0
(no water in this zone)
wowee_editor --info-zone-water custom_zones/Stranglethorn
Zone water: custom_zones/Stranglethorn
loaded tiles : 4
water chunks : 387 (out of 1024 possible)
total layers : 412
height range : 12.40 to 18.50
By liquid type:
water (0): 380 layer(s)
ocean (1): 28 layer(s)
magma (2): 4 layer(s)
Per-chunk water can have multiple layers (different liquid types or
height regions overlapping). Liquid types: 0=water, 1=ocean, 2=magma,
3=slime — different gameplay rules apply (oceans are swimable, magma
is damage-over-time, etc.).
JSON mode emits per-type layer counts + height range for programmatic
audit. Verified on a freshly-scaffolded zone: correctly reports 0
water chunks.
Multi-zone wrapper around --validate-all. Walks every zone in a
project and runs the per-format validators (WOM/WOB/WOC/WHM).
Aggregates pass/fail per zone with file-level breakdown:
wowee_editor --validate-project custom_zones
validate-project: custom_zones
zones : 12 (1 failed)
zone files failed errors status
AshenForest 47 0 0 PASS
BoneOasis 31 0 0 PASS
CrystalCaverns 28 2 14 FAIL
...
1 zone(s) failed validation
Designed for CI gates before --pack-wcp (or before tagging a release):
one command checks the whole project's binary integrity, exits 1 if
anything's broken with a clear breakdown of which zone went wrong.
Pairs with --validate-all (single-zone, all formats) and the
per-format validators (--validate-wom etc.). Three levels of
granularity now:
--validate-{wom,wob,woc,whm} single file
--validate-all single zone (or any dir)
--validate-project entire project
JSON mode emits per-zone records (totalFiles + failedFiles +
totalErrors) for dashboard consumption. Verified on a 2-zone
project with one tile having .woc built and one without — both
zones PASS, exit 0.
Two analytics commands for combat-balance work. Where --info-creatures
gives totals + behavior counts, these give the distributions:
wowee_editor --info-creatures-by-faction $Z/creatures.json
Creatures by faction: ... (47 total)
faction count share
7 12 25.5%
14 29 61.7%
35 6 12.8%
(factions: 7=human, 14=monster, 35=neutral, etc.)
wowee_editor --info-creatures-by-level $Z/creatures.json
Creatures by level: ... (47 total)
range : 5 to 32 (avg 14.2)
level count bar
5 4 ████████████████████████████████████████
6 3 ██████████████████████████████
...
30 1 ██████████
Faction histogram catches single-faction zones (one giant melee) and
mixed-faction tuning issues. Level histogram catches difficulty-curve
problems (cluster at 5, gap, cluster at 30) and outlier spawns
(level-60 boss accidentally placed in starter area).
ASCII bar chart for level distribution since gameplay tuning is
visual — '60% of mobs are levels 8-12 with a long tail' is more
intuitive as a bar than as numbers. Bars scale to longest bin so
small zones still get usable visualization.
JSON mode emits per-faction / per-level records for dashboards.
Verified on a 4-creature seed (3×faction-14 + 1×faction-35; levels
7/8/12/30): faction percentages and level range/avg both correct.
Self-check that every flag in kArgRequired (the master list of
commands needing positional args) appears in the help text emitted
by printUsage. Catches drift where a handler+arg-check pair gets
added but the help line is forgotten:
wowee_editor --validate-cli-help
CLI help self-check
kArgRequired entries : 126
PASSED — every kArgRequired flag is documented
If it ever fails, the missing flags are listed:
FAILED — 2 flag(s) missing from help text:
- --new-thing-i-forgot-to-document
- --another-undocumented-flag
CI integration: add this to the test target so a PR that adds a
new command without docs can't merge. The --info-cli-stats command
gives the surface-size view; this gives the surface-completeness
view.
Verified: ran on the current binary (126 kArgRequired entries),
PASSED — every flag is documented.
Project-level tree view: every zone with quick counts + bake/viewer
artifact status. --info-zone-tree drills into one zone; this gives
the bird's-eye view across the whole project:
wowee_editor --info-project-tree custom_zones
custom_zones/ (2 zones, 2 tiles, 1 creatures, 1 objects, 1 quests)
├─ Empty/ (tiles=1, creat=0, obj=0, quest=0)
│ ├─ name : Empty
│ ├─ mapName : Empty
│ ├─ artifacts : (none)
│ └─ status : empty (only terrain)
└─ Forest/ (tiles=1, creat=1, obj=1, quest=1)
├─ name : Forest
├─ mapName : Forest
├─ artifacts : .glb
└─ status : populated
Per-zone summary line shows counts; sub-tree shows display name,
map slug, which derived artifacts have been baked (.glb / .obj /
.stl / .html / ZONE.md), and a populated/empty status.
Project-header line aggregates totals across all zones for the
'how big is my project?' answer in one glance.
Pairs with --info-tilemap (spatial coverage) and --zone-stats
(quantitative aggregate) — three different lenses on the project:
spatial, quantitative, and structural.
Markdown counterpart to --export-project-html. Generates a README.md
indexing every zone with counts + bake/viewer/doc artifact status.
GitHub renders it natively at the project root:
wowee_editor --export-project-md custom_zones
# Wowee Project — Zone Index
*Auto-generated. 2 zone(s) discovered in `custom_zones`.*
## Summary
| Metric | Total |
|---|---:|
| Zones | 2 |
| Tiles | 2 |
| Creatures | 2 |
| ...
## Zones
| Zone | Tiles | Creatures | Objects | Quests | Bake | Viewer | Docs |
|---|---:|---:|---:|---:|:---:|:---:|:---:|
| Desert | 1 | 1 | 1 | 1 | — | — | — |
| [Forest](Forest/ZONE.md) | 1 | 1 | 1 | 1 | ✓ | [view](Forest/Forest.html) | [md](Forest/ZONE.md) |
Per-zone row links to its ZONE.md (if --export-zone-summary-md was
run) and its HTML viewer (if --export-zone-html was run). The Bake
column shows ✓ if .glb exists. Status columns make it instantly
visible which zones are bake-ready vs documentation-only.
Pairs with --export-project-html (interactive viewer index) — same
data, different presentation: HTML for browsers, Markdown for
GitHub Pages READMEs and PR descriptions.
Verified on a 2-zone project where one zone had been baked +
viewer-exported + doc-exported and the other hadn't: README.md
correctly shows ✓/links for the baked zone, em-dashes for the
unbaked one.
Computes the zone's spatial bounding box: XY world coords from
manifest tile coords (each tile is 533.33 yards), Z height range
across all loaded chunks. Useful for sizing camera frustums,
planning where new tiles can fit contiguously, or quick sanity
checks ('this zone is 4km across? something's wrong'):
wowee_editor --info-zone-extents custom_zones/MyZone
Zone extents: custom_zones/MyZone
tile count : 3 (3 loaded, 0 missing on disk)
tile range : x=[30, 31] y=[30, 31]
world box : (0.0, 0.0, 98.5) - (1066.7, 1066.7, 101.5) yards
size : 1066.7 x 1066.7 x 3.0 yards (975m x 975m x 2.7m)
WoW grid math: tile (32, 32) is at world origin; +X tile = -X world
(north convention), +Y tile = -Y world (west convention). The
displayed world coords use the same transform the renderer uses
so they line up with --bake-zone-glb output bounds.
Per-axis size in yards + meters (0.9144 conversion) since some
designers think in metric, others in WoW-canonical yards.
Tracks loaded vs missing tiles in case the manifest references a
tile whose .whm got deleted — surfaces silently bad zones early.
JSON mode emits full bounding box + tile range + size for
programmatic consumption (camera autofit, layout planning).
Compares two SHA256SUMS files (from --export-zone-checksum). Reports
added / removed / changed entries between two zone snapshots — much
faster than walking the filesystem to recompute hashes of unchanged
content:
wowee_editor --export-zone-checksum custom_zones/Z /tmp/before.sha256
... edits happen ...
wowee_editor --export-zone-checksum custom_zones/Z /tmp/after.sha256
wowee_editor --diff-checksum /tmp/before.sha256 /tmp/after.sha256
Diff: /tmp/before.sha256 vs /tmp/after.sha256
added : 1
removed : 0
changed : 0
+ creatures.json
Standard diff-style markers (+/-/~) for added/removed/changed,
sorted within each category alphabetically.
Use cases:
- Audit what an editing session actually touched (snapshot before,
snapshot after, diff)
- Verify a zone bundle re-extracts identically (transfer integrity
beyond the per-file PASS/FAIL of sha256sum -c)
- CI gate: fail build if a refactor touches files it shouldn't
Diff family for content/integrity formats:
--diff-zone unpacked zone dir vs zone dir (high-level)
--diff-extract per-extension counts in two extracts
--diff-checksum per-file hash diff between two snapshots <- new
Verified: scaffold + export checksum, add creature, re-export,
diff correctly reports '+ creatures.json' (added) and exit 1.
Quick-start: scaffold a zone AND populate one of each content type
(1 creature, 1 object, 1 quest with objective + XP reward) in a
single command. Goes from empty filesystem to 'something to look at'
without 7 chained --add-* commands:
wowee_editor --mvp-zone 'Demo Land' 30 30
Created demo zone: custom_zones/Demo_Land
tile : (30, 30)
contents : 1 creature, 1 object, 1 quest (with objective + reward)
next : wowee_editor --info-zone-tree custom_zones/Demo_Land
Demo Land/
├─ Manifest ...
├─ Tiles (1) — (30, 30)
├─ Creatures (1) — lvl 5 Demo Wolf
├─ Objects (1) — m2 World/Generic/Tree.m2
├─ Quests (1) — [1] Welcome to Demo Land (lvl 1, 100 XP)
│ └─ kill ×1 Demo Wolf
Demo content is positioned roughly at tile center (533.33-yard
intervals from origin tile 32/32). Quest references the demo
creature's auto-id so --check-zone-refs passes immediately.
Use cases:
- Smoke-testing the bake/validate pipeline
- Screenshot bait for docs / blog posts
- Editor onboarding (open a zone in the GUI to see the format)
- CI sanity check (does our editor still produce a viewable zone?)
Verified end-to-end: --mvp-zone 'Demo Land' → --info-zone-tree
shows all 4 sections populated correctly, file list matches
expected 6 files.
Where --export-quest-graph visualizes the quest dependency graph,
this quantifies it. Useful for spotting authoring issues (orphan
quests that only appear as one-offs, broken chains via cycles)
and getting a sense of quest density:
wowee_editor --info-quest-graph-stats $Z/quests.json
Quest graph: $Z/quests.json
total quests : 4
roots : 2 (no inbound chain — entry points)
leaves : 2 (no outbound chain — terminal)
orphans : 1 (root AND leaf — one-shot)
cycles : 0
max depth : 3
avg depth : 2.00 (chain length per root)
Definitions:
roots = quests no other quest chains TO (player entry points)
leaves = quests with no nextQuestId or nextQuestId pointing to
a missing quest (terminal — chain ends here)
orphans = root AND leaf (one-shot quests with no neighbors)
cycles = number of roots whose forward walk hits a node twice
maxDepth = longest path from any root forward through the chain
avgDepth = mean path length across all roots
Cycle-guarded forward walk uses a visited-set per root, so the
cycle count is bounded even on intentionally-broken inputs.
Exit 1 if cycles > 0 so CI can gate before shipping a broken
chain. JSON mode emits all six stats for dashboard consumption.
Verified on 4-quest zone (Q1→Q2→Q3 chain + Loner orphan):
correctly reports 2 roots, 2 leaves, 1 orphan, 0 cycles, max
depth 3, avg depth 2.00.
Three project-bake formats now match the three zone-bake formats —
full project terrain reachable from every universal-3D ecosystem:
wowee_editor --bake-project-obj custom_zones # DCC tools
wowee_editor --bake-project-stl custom_zones # 3D printing
wowee_editor --bake-project-glb custom_zones # web viewers
Shared per-zone walking pass collects vertex+index pools per zone,
then the format-specific tail emits:
STL → per-triangle 'facet normal'+'outer loop'+vertex×3
GLB → packed BIN chunk + JSON describing per-zone meshes
GLB output gives one mesh+node per zone (named 'zone_NAME') so
viewers can toggle zones independently — same pattern as
--bake-zone-glb but at project scope. STL is single-solid since
slicers don't have a useful concept of multi-part STL.
Coords align across all three exporters and across zone vs project
scope, so:
- A zone .obj overlaid with its containing project .obj lines up
- A project .glb opened in three.js shows zones at the same coords
the renderer uses
Verified on a 2-zone project (Forest + Desert):
- project.stl: 2 zones, 2 tiles, 65536 facets
- project.glb: 2 zones, 2 tiles, 41472 verts, 65536 tris, 1.78MB BIN
- --validate-glb on project.glb: PASSED
Bake granularity matrix complete:
OBJ STL GLB
single model --export-obj --export-stl --export-glb
single zone --bake-zone-obj --bake-zone-stl --bake-zone-glb
whole project --bake-project-obj --bake-project-stl --bake-project-glb
Project-level OBJ bake — combines every zone's terrain into one
giant OBJ with one 'g zone_NAME' block per zone. Useful for
previewing an entire multi-zone project's terrain in MeshLab/
Blender at once, or for printing the full map:
wowee_editor --bake-project-obj custom_zones
Baked custom_zones -> custom_zones/project.obj
2 zone(s), 3 tiles, 62208 verts, 98304 tris
Layout: single global vertex pool (so OBJ indexing stays valid),
per-zone face groups so designers can hide individual zones in
their viewer for area-by-area inspection. Hole bits respected.
Coords match WoweeCollisionBuilder's outer-grid layout exactly so
zones spatially line up at WoW grid boundaries — adjacent tiles
across zones connect seamlessly.
Pairs with the existing --bake-zone-* family (single zone) and
--export-project-html (web index of per-zone viewers). Three
levels of granularity now available:
--export-glb / --export-obj / --export-stl single model/file
--bake-zone-glb / -obj / -stl single zone
--bake-project-obj entire project <- new
Verified: 2-zone project (Forest 2 tiles + Desert 1 tile) baked
to project.obj with 62208 verts (3 × 20736), 98304 tris (3 ×
32768), 2 'g' blocks correctly named (zone_Desert, zone_Forest).
Emits a SHA-256 manifest of every source file in a zone in the
standard sha256sum format. Lets users verify zone integrity after
download/transfer using the standard system tool — no custom
verifier needed:
wowee_editor --export-zone-checksum custom_zones/MyZone
3298c35a... Z_30_30.whm
f81e3d37... Z_30_30.wot
6a49519f... creatures.json
4625e30b... zone.json
sha256sum -c custom_zones/MyZone/SHA256SUMS
Z_30_30.whm: OK
Z_30_30.wot: OK
...
Source-only by design — derived outputs (.glb/.obj/.stl/.html/.png/
ZONE.md/DEPS.md/quests.dot/SHA256SUMS itself/Makefile) are excluded
since they're regeneratable and would invalidate the checksum on
every rebuild.
Includes a self-contained 90-LoC SHA-256 (FIPS 180-4 / RFC 6234)
in an internal namespace — no OpenSSL/Crypto++ dependency added.
Streaming hash (16KB chunks) so it scales to giant terrain WHMs
without holding the whole file in memory.
Verified end-to-end: scaffolded zone with 1 creature → checksum
manifest of 4 source files (zone.json, creatures.json, .whm, .wot)
in standard format → sha256sum -c reports all 4 OK.
130+ commands. 'Is there a thing for X?' is a common question.
Scrolling --help to find out is slow. This searches:
wowee_editor --info-cli-help quest
--add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]
Append one quest to <zoneDir>/quests.json and exit
--add-quest-objective <zoneDir> <questIdx> <kill|collect|...> ...
Append one objective to a quest by index
--remove-quest-objective <zoneDir> <questIdx> <objIdx>
Remove the objective at given 0-based index from a quest
--clone-quest <zoneDir> <questIdx> [newTitle]
Duplicate a quest (with all objectives + rewards)
...
32 line(s) matched 'quest'
Case-insensitive substring match. Continuation lines (the indented
description right after a flag) are emitted along with their flag
line for context. Match-count summary on stderr so it doesn't
contaminate piped output.
Pairs with --list-commands (full list) and --info-cli-stats (counts
by category). Three meta commands now cover the discovery loop:
'how many?' (--info-cli-stats), 'which ones?' (--list-commands),
'what does X do?' (--info-cli-help X).
Pairs with --gen-makefile (per-zone) — generates a project-level
Makefile that delegates to each zone's per-zone Makefile, enabling
parallel rebuilds across zones:
wowee_editor --gen-project-makefile custom_zones
make -C custom_zones -j$(nproc) # all zones in parallel
make Forest-bake # one zone
make clean # strip every zone
make validate # validate every zone
make index # rebuild project HTML index
make stats / tilemap # project-level reports
Per-zone targets (one set per zone):
ZONE-bake -> ensures ZONE/Makefile exists then `make -C ZONE all`
ZONE-clean -> --strip-zone ZONE
ZONE-validate -> --validate-all ZONE
Auto-generates the per-zone Makefile if missing (so the project
Makefile bootstraps a fresh project without an extra setup step).
Top-level utility targets reuse the existing project-level commands:
- index -> --export-project-html (HTML zone index)
- stats -> --zone-stats (aggregate counts)
- tilemap -> --info-tilemap (ADT grid visualization)
Verified: 2-zone project (Forest + Desert) generates a Makefile
with 6 zone-level targets (3 per zone) + 5 top-level targets, sorted
alphabetically by zone name.
Sanity-checks creature/object/quest fields for plausible values.
Where --check-zone-refs catches dangling references, this catches
data-quality issues that pass technical validation but break
gameplay:
wowee_editor --check-zone-content custom_zones/MyZone
Zone content: custom_zones/MyZone
creature warnings: 1
object warnings : 0
quest warnings : 2
FAILED — 3 total warning(s):
- creature[0] 'Wolf' has displayId=0 (will render invisibly)
- quest[0] 'Hunt' has no objectives (uncompletable)
- quest[0] 'Hunt' has no reward at all
Per-type checks:
Creatures:
- empty name
- 0 health (dies on spawn)
- level 0
- minDamage > maxDamage (broken combat math)
- non-positive or non-finite scale
- displayId=0 (invisible at runtime)
Objects:
- empty path
- non-positive or non-finite scale
- non-finite position
Quests:
- empty title
- no objectives (player can never complete)
- no reward at all (XP=0, items=[], coins all 0)
- requiredLevel=0
Both --check-zone-refs (link integrity) and --check-zone-content
(data quality) needed — a quest can have valid NPC IDs (refs OK)
AND no objectives (content broken). Run both before --pack-wcp.
Verified end-to-end: zone with displayId=0 creature + objective-
less + rewardless quest reports 3 warnings; after fixing all three,
PASSED.
Compares two extracted asset directories side-by-side per file
extension. Useful for diffing a fresh asset_extract run against
a previous baseline (did the new MPQ add files? did any get
dropped?), or comparing what each WoW expansion contributes:
wowee_editor --diff-extract baseline/ new/
Diff: baseline/ vs new/
totals: 4 files / 0.0 MB vs 4 files / 0.0 MB
Per-extension (count then bytes):
ext a count b count a bytes b bytes status
.blp 2 2 0 0
.dbc 1 0 0 0 -A
.m2 1 2 0 0 DIFF
2 extension(s) differ
Status column flags imbalance:
-A only in A (extension dropped going B-ward)
+B only in B (extension added)
DIFF count differs but both sides have some
Recursive walk so subdirectories aggregate into the parent's
extension counts. JSON mode emits per-extension {count,bytes}
pairs for both sides plus union diff count for CI consumption.
Diff family for directory-shaped formats:
--diff-zone unpacked zone dir vs zone dir
--diff-extract extracted asset dir vs extract dir <- new
Verified on synthesized 4-file dirs (a: 2 blp + 1 dbc + 1 m2;
b: 2 blp + 0 dbc + 2 m2): correctly flags -A on .dbc, DIFF on
.m2, exit 1.
130 commands and counting — surface inspection has graduated from
'nice to have' to 'necessary to plan'. This drops a per-category
breakdown:
wowee_editor --info-cli-stats
CLI surface stats
total commands : 130
longest flag : 24 chars
Categories (by verb prefix, sorted by count):
--info 33
--export 15
--list 12
--validate 11
--diff 8
--add 6
--remove 5
--convert 5
--bake 3
--migrate 3
--clone 3
...
Pulls the command list via the same printUsage-parser as
--list-commands so it auto-tracks new flags. Buckets by the verb
prefix (text between '--' and the next '-') so '--info-zone-tree'
counts under 'info'. JSON mode emits the histogram as an object
for dashboard consumption.
Useful for spotting category imbalances ('we have 33 inspectors
but only 5 add commands — should consider more authoring CRUD?'),
tracking growth over releases, and planning where to invest.
Generates a Makefile that rebuilds every derived output for a zone
with proper dependency tracking. Designers can `make` to refresh
glb/obj/stl/html/csv/md from sources after editing creatures.json
or terrain — without remembering which wowee_editor flag does what,
and without rebuilding outputs that haven't changed:
wowee_editor --gen-makefile custom_zones/MyZone
cd custom_zones/MyZone && make
make all # rebuild everything that's stale
make glb # just the glTF bake
make clean # nuke derived outputs (calls --strip-zone)
make validate # run --validate-all on the zone
Targets generated:
glb obj stl html docs csv graph all clean validate
Dependency tracking:
- terrain bakes (.glb/.obj/.stl) depend on zone.json + WHM tiles
- HTML viewer depends on the .glb (forces glb rebuild first)
- docs (ZONE.md/DEPS.md) depend on content JSONs
- csv/graph use '-' prefix so missing-content failures don't
block 'make all' (zone with no quests still bakes terrain)
Uses /proc/self/exe absolute path so the Makefile works from any
cwd (run via `make -C custom_zones/MyZone` from anywhere). Falls
back to PATH lookup if /proc not available.
Verified end-to-end: scaffolded zone, generated Makefile, ran
`make all` from inside the zone dir — all derived outputs (.glb,
.obj, .stl, .html, ZONE.md, DEPS.md) generated; csv+graph
gracefully skipped due to no content; make exit 0.
After asset_extract finishes, '142k files across 17 dirs' is hard
to reason about. This groups them visually by top-level subdir +
file format with byte totals — instant orientation:
wowee_editor --info-extract-tree Data
Data/ (13 dirs, 284612 files, 31451.1 MB)
├─ expansions/ (85141 files, 12871.2 MB)
│ ├─ .adt 5469 files 6226626.7 KB
│ ├─ .wav 19517 files 3567936.9 KB
│ ├─ .m2 26354 files 1477702.1 KB
│ ├─ .wmo 5195 files 744940.1 KB
│ ├─ .blp 26911 files 691248.5 KB
│ ├─ .dbc 282 files 92373.6 KB
│ ├─ ...
├─ terrain/ (...)
├─ character/ (...)
Top-level dirs sorted by total bytes descending (heaviest first
so the high-impact directories surface immediately). Per-dir
extension breakdown also sorted by bytes. Walks recursively into
each top-level dir so 'expansions/wotlk/world/...' rolls up into
the expansions/ row.
Companion to --info-extract (counts + sidecar coverage) — that
one's wide-format JSON-friendly; this one's a tall tree-style
quick-look. Verified on a real 31GB extract of Data/ — output
makes the size distribution immediately clear (ADT files are
6GB of 13GB total for expansions/, etc.).
Companion to --validate-png — validates BLP textures without paying
the DXT decompress cost. Useful for spot-checking thousands of BLPs
in an extract dir:
wowee_editor --validate-blp Texture.blp
BLP: Texture.blp
magic : BLP2
size : 128 x 128
valid mips : 8
file bytes : 23044
PASSED
Checks (header-only, no pixel decode):
- 4-byte magic is 'BLP1' or 'BLP2'
- Width/height non-zero and within 8192 (texture exporter cap)
- mipOffsets[16] / mipSizes[16] tables: each non-zero pair refers
to a byte range within the file; mismatched (off=0 with size!=0
or vice versa) flagged
- Stops at first zero offset (BLP convention for unused slots)
Per-version layout differences (BLP1 has compression+alphaBits as
uint32; BLP2 has compression/alphaDepth/alphaEncoding/hasMips as
uint8) handled inline.
Verified on real BLP (pvp-banner-emblem-1.blp): 8 valid mips,
PASSED. Non-BLP file (manifest.json starting with '{'): correctly
flags 'magic is {' error and exits 1.
Format-validator lineup is now exhaustive across both proprietary
and open formats:
Proprietary: BLP / DBC (via --convert-dbc-json round-trip)
Open binary: WOM / WOB / WOC / WHM / GLB
Open text: JSON DBC / STL / PNG (BLP sidecar)
Generates an index.html linking to every zone's HTML viewer in a
project. Pairs with --export-zone-html (per-zone) and --bake-zone-glb
(terrain bake). Designed for github-pages / static-hosting style
'all my zones' showcase:
wowee_editor --export-project-html custom_zones
# -> custom_zones/index.html
Each zone gets a card showing:
- Display name (or mapName fallback)
- Counts: tiles · creatures · objects · quests (singular/plural
agreement)
- 'Open viewer →' link if HTML exists
- Helpful nudge if HTML missing ('No HTML viewer (run --export-zone-html)')
- Or 'No .glb (run --bake-zone-glb)' if not even baked yet
Self-contained CSS (dark theme matching --export-zone-html), no
external dependencies. Responsive grid layout (300px-min cards
auto-flowing across viewport).
Verified on a 2-zone project (Forest with .glb+.html + Desert
without): index lists both, Forest gets a working link, Desert
gets the 'run --bake-zone-glb' hint.
Renders the WoW 64x64 ADT coordinate grid as ASCII art showing which
tiles are claimed by which zones. Useful for spotting tile-coord
collisions before two zones ship overlapping content, and for
'where am I working?' overview of multi-zone projects:
wowee_editor --info-tilemap custom_zones
Tilemap: custom_zones
zones : 2
tiles used : 5
collisions : 0 (multiple zones claiming same tile)
legend : D=Desert F=Forest
0 1 2 3 4 5 6
0123456789012345678901234567890123456789012345678901234567890123
y=30 ..............................FFDD..............................
y=31 ...............................F................................
Glyphs are first-letter-of-zone uppercased; collision tiles render
as '*'. Empty rows are skipped to keep output bounded for projects
in one corner of the map (skipping all 60+ blank rows of the full
64x64 grid).
Exit 1 on collisions so CI can gate before merging two PRs that
both add tiles in the same area. JSON mode emits per-tile claims
for programmatic consumption.
Verified on a 2-zone project: Forest (3 tiles) + Desert (2 tiles)
correctly rendered, no collisions detected, glyphs F/D placed at
the right grid coords.
When a zone is hand-edited, partially copied, or modified by tools
that don't re-write zone.json, the manifest can fall out of sync
with the on-disk reality. --repair-zone reconciles them:
wowee_editor --repair-zone custom_zones/MyZone --dry-run
would add tile (31, 30) to manifest
would set hasCreatures: false -> true
repair-zone: custom_zones/MyZone (dry-run)
fixes : 2
warnings : 0 (manual decision needed)
re-run without --dry-run to apply
Auto-fixes:
- WHM files on disk matching <mapName>_TX_TY.whm pattern but not
in manifest tiles[] -> add to tiles
- hasCreatures flag mismatched against actual creatures.json
presence + non-empty -> sync
Warns (no auto-fix — needs manual decision):
- Tiles in manifest but no .whm on disk (could be in-progress
work or genuinely deleted; user decides)
--dry-run flag previews changes. Pairs with --strip-zone (cleanup
derived) and --validate (open-format coverage) for the trio of
zone-health-maintenance commands.
Verified: scaffolded zone, hand-copied an extra .whm/.wot pair to
simulate disk-without-manifest drift, added a creature then flipped
hasCreatures=false in zone.json. --repair-zone correctly identifies
2 fixes, dry-run lists them, real run applies them, manifest now
shows correct tiles array + hasCreatures=true.
Removes the derived outputs (everything --bake/--export-* generates)
leaving only source files in a zone directory:
wowee_editor --strip-zone custom_zones/MyZone
removed: MyZone.obj (1099177 bytes)
removed: MyZone.glb (891736 bytes)
removed: DEPS.md (357 bytes)
removed: ZONE.md (534 bytes)
removed: quests.dot (198 bytes)
strip-zone: custom_zones/MyZone
removed : 5 file(s)
freed : 1945.3 KB
What gets deleted:
- .glb / .obj / .stl (3D format exports)
- .html (browser viewer)
- .dot (Graphviz quest graph)
- .csv (spreadsheet exports)
- ZONE.md / DEPS.md (markdown documentation)
- .png at zone root (heightmap previews — NOT inside data/, those
are source BLP→PNG sidecars)
What stays:
- zone.json + creatures.json + objects.json + quests.json
- *.whm / *.wot / *.woc (terrain + collision)
- *.wom / *.wob (open binary models/buildings)
- data/*.json (DBC sidecars — source, not derived)
Top-level only — does not recurse into subdirectories so source
sidecars under data/ are untouched.
--dry-run flag previews what would be removed without deleting.
Useful before committing to git so derived blobs don't bloat
history, or before --pack-wcp so the archive doesn't carry
redundant exports.
Verified: scaffolded zone, generated 5 derived files (glb/obj/
ZONE.md/DEPS.md/quests.dot), --dry-run lists all 5 with sizes,
real run deletes them and frees 1945 KB. Source files (whm/wot/
woc/zone.json/quests.json) all preserved.
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).