Commit graph

596 commits

Author SHA1 Message Date
Kelsi
dadefab64e feat(editor): add --check-zone-content for content data-quality validation
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.
2026-05-06 15:31:12 -07:00
Kelsi
c9e8ad9930 feat(editor): add --diff-extract for asset-extract directory comparison
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.
2026-05-06 15:25:18 -07:00
Kelsi
0f275cfee7 feat(editor): add --info-cli-stats meta command for surface tracking
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.
2026-05-06 15:21:24 -07:00
Kelsi
1f20d0c5a2 feat(editor): add --gen-makefile for incremental zone-output rebuilds
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.
2026-05-06 15:17:53 -07:00
Kelsi
5ebd04a953 feat(editor): add --info-extract-tree for hierarchical extract-dir overview
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.).
2026-05-06 15:10:45 -07:00
Kelsi
d3b7a085c2 feat(editor): add --validate-blp for proprietary BLP structural check
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)
2026-05-06 15:07:46 -07:00
Kelsi
a98e6e79c4 feat(editor): add --export-project-html for multi-zone web index
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.
2026-05-06 15:04:43 -07:00
Kelsi
f73b377b4d feat(editor): add --info-tilemap for project-wide ADT grid visualization
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.
2026-05-06 15:01:32 -07:00
Kelsi
1718c2333f feat(editor): add --repair-zone to auto-fix manifest/disk drift
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.
2026-05-06 14:58:08 -07:00
Kelsi
b82f9827d4 feat(editor): add --info-zone-bytes for per-file size breakdown
Drills into one zone's contents with categorized + sorted file
sizes. --zone-stats aggregates across multiple zones; this answers
'which file is 80% of THIS zone?' and 'how much would --strip-zone
free?':

  wowee_editor --info-zone-bytes custom_zones/MyZone

  Zone bytes: custom_zones/MyZone
    total: 2282178 bytes (2228.7 KB) across 6 file(s)

    Per-file (largest first):
    path                                bytes  category
    Z_30_30.woc                       1212456  terrain
    Z.glb                              891736  3D export (derived)
    Z_30_30.whm                        150540  terrain
    Z_30_30.wot                         26680  terrain
    zone.json                              446  json (source)
    quests.json                            320  json (source)

    Per-category:
    3D export (derived)    1 files    891736 bytes  ( 39.1%)
    json (source)          2 files       766 bytes  (  0.0%)
    terrain                3 files   1389676 bytes  ( 60.9%)

Categories: terrain / model (open|proprietary) / building (open|
proprietary) / texture (open|proprietary) / DBC / json (source) /
3D export (derived) / doc (derived) / other.

Source vs derived split surfaces what --strip-zone would clean up
(any 'derived' category) so capacity planning shows both 'what's
mine' (source) and 'what's regeneratable' (derived).

Recursive walk so subdirs (data/) are included with relative paths.
JSON mode emits per-file records + per-category aggregate for
programmatic consumption.
2026-05-06 14:54:29 -07:00
Kelsi
7dfc0b6333 feat(editor): add --strip-zone for derived-output cleanup
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.
2026-05-06 14:51:36 -07:00
Kelsi
8f5a3b3d95 feat(editor): add --info-glb-tree for hierarchical glTF structure view
--info-glb gives counts; --info-glb-tree shows the actual scene ->
node -> mesh -> primitive hierarchy with names and accessor refs.
Useful when debugging 'why is this imported model showing up empty
in three.js?' (often the scene's nodes[] points to the wrong node):

  wowee_editor --info-glb-tree Z.glb

  Z.glb
  ├─ asset (v2.0, wowee_editor --bake-zone-glb)
  ├─ buffers (1)
  │  └─ [0] 1781760 bytes
  ├─ bufferViews (3)
  │  ├─ [0] off=0 len=497664 (vertex)
  │  ├─ [1] off=497664 len=497664 (vertex)
  │  └─ [2] off=995328 len=786432 (index)
  ├─ accessors (4)
  │  ├─ [0] f32 VEC3 ×41472 (bv=0)
  │  ├─ [1] f32 VEC3 ×41472 (bv=1)
  │  ├─ [2] u32 SCALAR ×98304 (bv=2)
  │  └─ [3] u32 SCALAR ×98304 (bv=2)
  ├─ meshes (2)
  │  ├─ [0] (1 primitives)
  │  │  └─ [0] TRIANGLES indices=acc#2
  │  └─ [1] (1 primitives)
  │     └─ [0] TRIANGLES indices=acc#3
  ├─ nodes (2)
  │  ├─ [0] tile_30_30 -> mesh#0
  │  └─ [1] tile_31_30 -> mesh#1
  └─ scenes (1, default=0)
     └─ [0] nodes=[0,1] (2 nodes)

Decodes glTF componentTypes (5120-5126 -> i8/u8/i16/u16/u32/f32),
bufferView targets (34962=vertex, 34963=index), primitive modes
(0=POINTS / 1=LINES / 4=TRIANGLES). Node sub-line shows mesh
reference so the scene-graph wiring is visible at a glance.

Pairs with --info-zone-tree (zone content tree) — both use the
same UTF-8 box-drawing pattern for visual consistency.
2026-05-06 14:48:37 -07:00
Kelsi
2904fa0560 feat(editor): add --export-zone-deps-md for shareable dependency tables
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.
2026-05-06 14:44:53 -07:00
Kelsi
84f897f0ec feat(editor): add --diff-jsondbc completing the diff family for the last text format
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.
2026-05-06 14:41:32 -07:00
Kelsi
e531901de8 feat(editor): add --validate-png for full PNG structural + CRC validation
Goes beyond --info-png (header sniff only) to validate every chunk
and verify CRC32. Catches corruption that browsers silently skip:

  wowee_editor --validate-png Texture.png

  PNG: Texture.png
    size       : 256 x 256
    bit depth  : 8 (color type 6)
    chunks     : 3 (0 CRC mismatches)
    file bytes : 142336
    PASSED

  wowee_editor --validate-png corrupted.png
    chunks     : 1 (1 CRC mismatches)
    FAILED — 4 error(s):
      - chunk 'IHDR' at offset 8: CRC mismatch (stored=0xC33E61CB
        actual=0x8A716D80)
      - missing required IDAT chunk
      - missing required IEND chunk

Checks:
- 8-byte PNG signature (89 50 4E 47 0D 0A 1A 0A)
- Per-chunk length doesn't exceed file
- CRC32 of (chunk type + data) matches stored CRC (PNG spec)
- IHDR is the first chunk
- IHDR / IDAT / IEND all present
- No trailing bytes after IEND

CRC32 implementation uses the standard polynomial 0xEDB88320 with a
runtime-built lookup table — no zlib dependency since we already
have to compute it ourselves anyway.

Verified on real PNG (BLP→PNG conversion of pvp-banner emblem):
PASSED. Hand-corrupted IHDR byte: correctly flags the CRC mismatch
with stored vs actual hex values + cascading missing-chunk errors.
Format-validator lineup is now exhaustive across both proprietary
and open formats:
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL
  Sidecar:     PNG (proprietary BLP -> PNG bridge)
2026-05-06 14:38:17 -07:00
Kelsi
f8f5735d9b feat(editor): add --diff-whm and --diff-woc completing the open-format diff suite
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.
2026-05-06 14:35:03 -07:00
Kelsi
cc3e85be5a feat(editor): add --migrate-jsondbc to auto-fix common JSON DBC issues
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.
2026-05-06 14:31:35 -07:00
Kelsi
84902316e2 feat(editor): add --export-zone-html for browser-viewable zone preview
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.
2026-05-06 14:28:42 -07:00
Kelsi
d618d6a517 feat(editor): add --diff-wob completing the binary-format diff suite
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.
2026-05-06 14:25:40 -07:00
Kelsi
a01b5e5e89 feat(editor): add --info-object completing the single-entity inspector trio
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
2026-05-06 14:22:08 -07:00
Kelsi
b7696d1aa9 feat(editor): add --info-creature and --info-quest single-entity inspectors
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)
2026-05-06 14:19:17 -07:00
Kelsi
17b83858c1 feat(editor): add --export-zone-csv for spreadsheet design workflows
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.
2026-05-06 14:16:00 -07:00
Kelsi
8dc91adc52 feat(editor): add --diff-wom completing the diff suite for models
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.
2026-05-06 14:13:07 -07:00
Kelsi
468a1b8ede feat(editor): add --validate-stl for STL structural sanity check
Pairs with --export-stl / --import-stl / --bake-zone-stl. Catches
the corruption modes that crash slicer mesh analyzers:

  wowee_editor --validate-stl Tree.stl

  STL: Tree.stl
    solid name : Tree
    facets     : 6
    vertices   : 18
    PASSED

  wowee_editor --validate-stl truncated.stl

  STL: truncated.stl
    solid name : Truncated
    facets     : 1
    vertices   : 2
    FAILED — 3 error(s):
      - missing 'endsolid' footer
      - 1 unclosed 'facet' (missing 'endfacet')
      - vertex count 2 != 3 * facet count 1

Checks:
- 'solid' header present
- 'endsolid' footer present
- Every 'facet' has matching 'endfacet' (no leaks)
- Every facet has exactly 3 vertices
- Total vertex count = 3 × facet count
- All facet normals + vertex coords are finite (no NaN/inf)
- 'facet normal' has 'normal' subtoken + 3 floats
- 'vertex' has 3 floats

Errors capped (30 listed) so a giant corrupt file with consistent
breakage doesn't drown the report. Exit 1 on any error so CI can
gate. Format-validator lineup is now complete:
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL
Every shippable open format has a CLI validator.
2026-05-06 14:10:07 -07:00
Kelsi
a3333b7b4d feat(editor): add --bake-zone-obj completing the bake-zone trio
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.
2026-05-06 14:07:22 -07:00
Kelsi
6113582a7d feat(editor): add --check-glb-bounds for stale-bounds detection
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.
2026-05-06 14:04:48 -07:00
Kelsi
06b21884ad feat(editor): add --bake-zone-stl for full-zone 3D-printable terrain
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).
2026-05-06 14:02:13 -07:00
Kelsi
b7b600c177 feat(editor): add --list-commands and --gen-completion for shell ergonomics
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
2026-05-06 13:59:54 -07:00
Kelsi
d82f90dd82 feat(editor): add --diff-glb completing the diff-* family
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.
2026-05-06 13:57:25 -07:00
Kelsi
901e48b659 feat(editor): add --info-zone-tree for hierarchical zone overview
At-a-glance comprehension of a zone's contents in a single `tree`-
style view. The other --info-* commands focus on one category each;
this composes them into a unified picture:

  wowee_editor --info-zone-tree custom_zones/MyZone

  MyZone/
  ├─ Manifest
  │  ├─ mapName     : MyZone
  │  ├─ mapId       : 9000
  │  ├─ baseHeight  : 100.0
  │  ├─ biome       : (unset)
  │  └─ flags       :
  ├─ Tiles (1)
  │  └─ (30, 30)
  ├─ Creatures (2)
  │  ├─ lvl 7  Wolf
  │  └─ lvl 12  Bear
  ├─ Objects (1)
  │  └─ m2   World/Tree.m2
  ├─ Quests (1)
  │  └─ [1] Hunt Wolves (lvl 5, 250 XP)
  │     ├─ kill ×5 Wolf
  │     └─ reward: Item:Sword
  └─ Files (6)
     ├─ Z_30_30.whm
     ├─ Z_30_30.wot
     ├─ creatures.json
     ├─ objects.json
     ├─ quests.json
     └─ zone.json

Quest sub-tree includes objectives + rewards bulleted underneath.
Files section shows what physically lives in the zone dir so users
spot orphan files (e.g. .obj exports) at a glance.

UTF-8 box-drawing characters for connectors. No --json mode by
design — the structured equivalent is just running --info-* per
category and concatenating, which already exists.
2026-05-06 13:54:48 -07:00
Kelsi
d65c315465 feat(editor): add --bake-zone-glb for whole-zone glTF export
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.
2026-05-06 13:52:09 -07:00
Kelsi
a212479424 feat(editor): add --import-stl to round-trip STL back into WOM
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).
2026-05-06 13:49:02 -07:00
Kelsi
9b24e0be8a feat(editor): add --export-stl for 3D-printer-compatible WOM export
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').
2026-05-06 13:46:25 -07:00
Kelsi
6d79963f24 feat(editor): add --migrate-zone for batch WOM upgrade across a zone
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).
2026-05-06 13:44:16 -07:00
Kelsi
b3e34e0edf feat(editor): add --validate-jsondbc for strict JSON DBC schema check
--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.
2026-05-06 13:41:45 -07:00
Kelsi
fab6238e64 feat(editor): add --migrate-wom for WOM1/WOM2 -> WOM3 schema upgrade
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'.
2026-05-06 13:39:18 -07:00
Kelsi
028a8011b0 feat(editor): add --info-bones and --list-zone-textures inspectors
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.
2026-05-06 13:36:59 -07:00
Kelsi
eca43cfefa feat(editor): add --info-attachments / --info-particles / --info-sequences
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.
2026-05-06 13:34:23 -07:00
Kelsi
749aa18f0d feat(editor): add --validate-glb and --info-glb for glTF inspection
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.
2026-05-06 13:31:31 -07:00
Kelsi
c1c1f1b2a8 feat(editor): add --export-quest-graph for Graphviz quest-chain visualization
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.
2026-05-06 13:29:23 -07:00
Kelsi
a183143002 feat(editor): add --check-zone-refs cross-reference validator
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.
2026-05-06 13:27:18 -07:00
Kelsi
e0360ee314 feat(editor): add --list-zone-deps for asset packaging audit
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.
2026-05-06 13:24:27 -07:00
Kelsi
3eea4350af feat(editor): add --zone-stats multi-zone aggregator
Walks every zone in <projectDir> and emits totals across the project
plus a per-zone breakdown table. Useful for content-pack release
notes ('this update adds 47 quests across 12 zones'), capacity
planning ('terrain bytes are 73% of pack size'), and PR diffs that
touch many zones at once:

  wowee_editor --zone-stats custom_zones

  Zone stats: custom_zones
    zones      : 2
    tiles      : 3 total
    creatures  : 3 (0 hostile)
    objects    : 0
    quests     : 1 (0 chained, 250 total XP)
    bytes      : 1706.5 KB total
      whm/wot  : 441.0 KB / 78.2 KB
      woc      : 1184.0 KB
      wom/wob  : 0.0 KB / 0.0 KB
      png/json : 0.0 KB / 3.2 KB

    per-zone breakdown:
      name                tiles  creat  obj  quest    bytes
      Desert                  1      1    0      0    174.2 KB
      Forest                  2      2    0      1   1532.3 KB

Aggregates:
- zone count (every dir with a zone.json)
- tile count (sum across all manifests)
- creature count + hostile subset
- object count
- quest count + chained subset + total XP awarded
- on-disk bytes bucketed by extension (.whm/.wot/.woc/.wom/.wob/
  .png/.json + 'other')

JSON mode emits per-zone records for programmatic consumption
(release-note generators, dashboards, etc.). Verified on a 2-zone
project: counts and byte totals match what individual --info-*
commands report per-zone, summed correctly.
2026-05-06 13:21:54 -07:00
Kelsi
82987af460 feat(editor): add --info-textures and --info-doodads detailed inspectors
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.
2026-05-06 13:19:45 -07:00
Kelsi
ac096c5c86 feat(editor): add --export-whm-glb completing the .glb exporter trio
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.
2026-05-06 13:17:37 -07:00
Kelsi
0ad99e4033 feat(editor): add --clear-zone-content for templating prep
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'.
2026-05-06 13:14:42 -07:00
Kelsi
3cebcab048 feat(editor): add --info-batches per-batch breakdown for WOM3 models
--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.
2026-05-06 13:12:28 -07:00
Kelsi
febf779ea7 feat(editor): add --clone-object completing the CRUD-clone trio
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.
2026-05-06 13:10:25 -07:00
Kelsi
4df5a367f8 feat(editor): add --export-wob-glb for buildings -> glTF 2.0 binary
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.
2026-05-06 13:08:31 -07:00
Kelsi
8375c47c4d feat(editor): add --export-glb for WOM -> glTF 2.0 binary
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.
2026-05-06 13:05:29 -07:00