Commit graph

102 commits

Author SHA1 Message Date
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
Kelsi
bbdbce2ec4 feat(editor): add --export-zone-summary-md for markdown documentation
Renders a human-readable markdown page summarizing a zone's manifest +
content. Useful for designers tracking changes between versions, PR
reviews, or generating GitHub Pages docs without cracking open the GUI:

  wowee_editor --export-zone-summary-md custom_zones/MyZone
  # -> custom_zones/MyZone/ZONE.md (default path)

  wowee_editor --export-zone-summary-md custom_zones/MyZone docs/Zone.md
  # -> custom Markdown destination

Sections rendered:
- Manifest (table): mapName, displayName, mapId, biome, baseHeight,
  tile count, gameplay flags (flying/PvP/indoor/sanctuary), audio
  (music/ambient day/ambient night), description.
- Tiles (table): every (tx, ty).
- Creatures (table): index, name, level, displayId, position, flags.
- Objects (table): index, m2/wmo, path, position, scale.
- Quests (sections): title, level, giver/turnIn IDs, XP/coin reward,
  objectives bulleted with type/target/count/description, item
  rewards bulleted.

Designed to be diff-friendly so PR review highlights actual changes
(adding 'Bear' creature shows up as one row added, not whole-file
churn). Output is GitHub-Flavored Markdown so it renders cleanly on
GitHub Pages and in PR previews.

Verified end-to-end: scaffolded zone, populated 2 creatures + 1
object + 1 quest with full objectives + item reward; ZONE.md
generated correctly with all tables and sections, quest details
including bulleted objective + item list.
2026-05-06 13:02:36 -07:00
Kelsi
605af53c98 feat(editor): add --for-each-zone batch runner
Shell-script-equivalent of looping over every zone in a project dir,
in a single editor invocation. Substitutes '{}' in the command with
each zone's path (find -exec convention):

  wowee_editor --for-each-zone custom_zones -- \
    wowee_editor --validate-all {}

  wowee_editor --for-each-zone custom_zones -- \
    wowee_editor --info-creatures {}/creatures.json

Implementation:
- Walks projectDir for child dirs containing zone.json (canonical
  'is this a zone?' test the rest of the editor uses).
- Sorts results so output is deterministic across runs.
- Each token after '--' is shell-escaped via single-quote wrapping
  with embedded ' replaced by '\'' — safe against names with spaces
  and quotes.
- {} replacement done per-token so paths with spaces don't get split.
- fflush(stdout) before std::system so the [zone] header lands above
  the child output instead of after (parent stdout buffered, child
  unbuffered to terminal).
- Returns failed-run count as exit code (capped at 255 for POSIX).

Verified: scaffolded 2 zones, ran --info-creatures across both via
for-each-zone. Output ordered correctly with [zone] headers + child
output, exit 0 when all succeed.
2026-05-06 13:00:15 -07:00
Kelsi
6927c19d72 feat(editor): add --clone-creature symmetric to --clone-quest
Designer workflow: archetype one 'patrol guard' with stats + faction
+ behavior + equipment dialed in, then stamp it across spawn points
around a town:

  for x in 100 110 120 130; do
    wowee_editor --clone-creature $Z 0 "Guard $x" $x 200 50
  done

Defaults give safe behavior:
- Position offset by (5, 0, 0) yards so the copy doesn't z-fight
  the original when no offset is passed.
- Name appended with ' (copy)' if no newName given.
- id reset (auto-assigned by NpcSpawner).
- Patrol path NOT offset — patrol points are world-space waypoints,
  not relative to the spawn; cloning that vector unchanged matches
  designer intent. Re-author the path on the clone if needed.

Verified: scaffolded zone, added Guard, cloned twice (default
offset, custom name + 3D offset). list-creatures shows 3 spawns
with correctly differentiated positions and names.
2026-05-06 12:57:31 -07:00
Kelsi
ff444e93f8 feat(editor): add --info-adt proprietary terrain inspector
Completes the proprietary-format inspector lineup. Pairs naturally
with --info-wot / --info-whm (the open WOT/WHM equivalents) so users
can verify the conversion preserves chunk counts, doodad placements,
and WMO references:

  wowee_editor --info-adt World/Maps/Azeroth/Azeroth_32_48.adt

  ADT: ...
    version          : 18
    file bytes       : 450192
    coord            : (0, 0)
    chunks loaded    : 256/256
    height range     : [-0.00, 0.00]
    hole chunks      : 0 (with cave/gap masks)
    water chunks     : 0
    textures         : 0
    doodad names     : 0 (0 placements)
    wmo names        : 1 (1 placements)

Reports loaded chunk count (out of fixed 256), height min/max across
all loaded chunks (with NaN guard so corrupted heights don't poison
the range), hole chunks (cave/gap masks), water chunks, texture and
doodad/WMO name table sizes, and placement counts.

Verified on a real WoW ADT (ahnqirajtemple_29_46.adt, 450KB):
correctly reports 256/256 chunks loaded, 1 WMO name + 1 placement.
JSON mode emits structured output for CI scripts.

The format-inspector lineup is now complete:
  Proprietary: BLP / DBC / M2 / WMO / ADT
  Open:        PNG / JSON DBC / WOM / WOB / WOC / WOT/WHM / WCP
Every format on both sides of the open-format bridge has an inspector.
2026-05-06 12:55:31 -07:00
Kelsi
2c41f7804b feat(editor): add --clone-quest for templating quests with shared shape
Common designer workflow: build a base quest with objectives + rewards
once, then make N variants ('Slay Wolves' / 'Slay Bears' / 'Slay
Tigers') with the same shape. Doing this through individual --add-*
commands means re-typing all objectives + items each time. --clone-quest
deep-copies an existing quest in one shot:

  wowee_editor --clone-quest $Z 0                  # 'Foo' -> 'Foo (copy)'
  wowee_editor --clone-quest $Z 0 'Slay Bears'     # custom title

Carries:
- All objectives (deep copy via vector value-copy)
- All item rewards
- XP / coin reward fields
- requiredLevel, giver, turnIn

Resets:
- id (set to 0; addQuest auto-assigns a fresh one)
- nextQuestId (cleared — chaining the clone to the same next quest
  would corrupt chain semantics; user can re-set it explicitly)

Verified: scaffolded zone, added quest with 2 objectives + 1 item +
xp=250. Cloned twice (default name, custom name). Result: 3 quests
in list, both clones have full objective list and reward intact.
2026-05-06 12:53:23 -07:00
Kelsi
b6ce6b4fe9 feat(editor): add --info-m2 and --info-wmo proprietary inspectors
Round out the format-inspector lineup. The wowee open formats had
inspectors (--info-wom, --info-wob); these are the proprietary
counterparts that pair with --convert-m2 / --convert-wmo so users
can verify what the conversion preserves vs drops:

  wowee_editor --info-m2  Character/Human/Male/HumanMale.m2
  wowee_editor --info-wmo World/wmo/Stormwind/Stormwind.wmo

--info-m2 reports verts/tris, bones, sequences (animations),
batches, textures, materials, attachments, particles, ribbons,
collision tris, and bound radius. Auto-merges <base>00.skin if
present (WotLK+ M2s store geometry there) so vertex/index counts
match what gets rendered.

--info-wmo reports group count + portals + lights + doodads +
materials + textures + total verts/tris across loaded groups.
Auto-merges matching <base>_NNN.wmo group files; pre-resizes the
groups vector so loadGroup populates the right slots.

Verified against real WoW assets:
  nexusraid_skya.m2: v264, 20917 verts, 22940 tris, 44 bones,
    1 sequence, 44 batches, 28 textures, 42 materials.
  ed_zd_ziggurat.wmo: v17, 1 group (1 loaded), 8 materials, 7
    textures, 4609 verts, 3650 tris from the group file.

Bug caught during testing: initial snprintf used an 8-byte buffer
for '_NNN.wmo' (which is 8 chars + NUL = 9), silently truncating
to '_000.wm' and failing every group lookup. Bumped to 16 bytes
with a comment so the trap doesn't get re-stepped.
2026-05-06 12:51:40 -07:00
Kelsi
03c700b030 feat(editor): add --info-blp inspector for proprietary BLP textures
The --info-* suite covers wowee open formats and their JSON/PNG
sidecars. BLP itself was the gap — the proprietary source format
that asset_extract converts FROM. Useful for verifying source
files before --convert-blp-png:

  wowee_editor --info-blp Texture.blp

  BLP: Texture.blp (BLP2)
    size       : 128 x 128
    channels   : 4
    format     : BLP2
    compression: DXT5
    mip levels : 16
    file bytes : 23044
    decoded RGBA bytes: 65536

Magic-checks the first 4 bytes (BLP1 or BLP2) before invoking the
loader so feeding a non-BLP gets a clear 'not a BLP1/BLP2 file'
message instead of a confusing 'invalid' from the decoder. Reports
both file size (compressed-on-disk) and decoded RGBA byte count
(width*height*4) so users can see the compression ratio at a glance.

JSON mode emits the same fields for CI/scripting.

Verified on a real WoW BLP (pvp-banner-emblem-1.blp): correctly
identifies as BLP2 / DXT5 / 128x128 / 16 mips. Total format-
inspector lineup is now complete (BLP/PNG/DBC-bin/DBC-json plus
all 5 wowee binary formats).
2026-05-06 12:44:09 -07:00
Kelsi
8fb3717995 feat(editor): add --list-quest-objectives and --list-quest-rewards
--list-quests shows the quest roster but doesn't drill into objectives
or item rewards (--info-quests just counts). These complete the
detail picture and unblock --remove-quest-objective by exposing the
0-based objIdx the user needs:

  wowee_editor --list-quest-objectives custom_zones/Z/quests.json 0

  Quest 0 ('Hunt'): 3 objective(s)
    idx  type     count  target              description
      0  kill         5  Wolf                Slay 5 Wolf
      1  collect      3  Pelt                Collect 3 Pelt
      2  talk         1  Mayor               Talk to Mayor

  wowee_editor --list-quest-rewards custom_zones/Z/quests.json 0

  Quest 0 ('Hunt') rewards:
    xp     : 999
    coin   : 5g 30s 99c
    items  : 2
      [0] Item:Sword
      [1] Item:Shield

Both have --json mode for CI/scripting. Range-check questIdx and
exit 1 on out-of-range with a precise message.

Verified: scaffolded zone, added quest with 3 objectives + 2 items
+ full coin reward; both listings show the right data, JSON output
is well-formed and enumerable.
2026-05-06 12:42:17 -07:00
Kelsi
d4b789b811 feat(editor): add --remove-tile and --list-tiles for zone tile management
Round out the tile lifecycle CLI. --add-tile shipped earlier; these
are the symmetric counterparts:

  wowee_editor --list-tiles custom_zones/MyZone
  wowee_editor --list-tiles custom_zones/MyZone --json
  wowee_editor --remove-tile custom_zones/MyZone 31 30

--list-tiles: shows tile coords AND on-disk presence of .whm/.wot/.woc
per tile, so you can spot manifest-vs-disk drift before pack-wcp would
complain. JSON mode emits per-tile records for programmatic checks.

--remove-tile: drops the manifest entry AND deletes the .whm/.wot/.woc
files for that tile so the zone stays consistent (no orphan sidecars).
Refuses to remove the last tile — server module gen and pack-wcp both
expect at least one, so a zero-tile zone would just be a footgun. Use
'rm -rf custom_zones/X/' for total removal.

Verified end-to-end: scaffolded zone, added 2 more tiles, built WOC
on one. --list-tiles shows 3 tiles with correct file presence (y/y/y
on the built one, y/y/- on the others). Removed (31, 30) — 2 files
deleted, 2 tiles remaining. Tried removing last tile after dropping
to 1: correctly refused with exit 1.
2026-05-06 12:40:19 -07:00
Kelsi
86978b55eb feat(editor): add --convert-blp-png standalone texture converter
Companion to --convert-dbc-json and --convert-json-dbc. Refresh one
PNG sidecar without re-running asset_extract --emit-open across the
whole tree:

  wowee_editor --convert-blp-png Texture.blp          # -> Texture.png
  wowee_editor --convert-blp-png Texture.blp out.png  # custom path

Same code path as the asset_extract emitter (BLPLoader::load +
stbi_write_png), with the same dimension/buffer guards (rejects
0x0 or >8192x8192, rejects images where data is shorter than
width*height*4).

Verified on a real WoW BLP (Data/interface/pvp-banner-emblem-1.blp,
128x128 RGBA): converted cleanly, --info-png on the result reports
'128 x 128 / 8-bit / rgba (4 channels) / 24136 file bytes' (PNG
zlib compression on a sparse banner texture).
2026-05-06 12:38:14 -07:00
Kelsi
1328998ec5 feat(editor): add --remove-quest-objective for symmetric CRUD
Symmetric counterpart to --add-quest-objective. Quest design is
iterative — when an objective gets reworked or trimmed, you need
to remove the wrong one without nuking the entire quest.

  wowee_editor --remove-quest-objective <zoneDir> <questIdx> <objIdx>

Both indices are 0-based and reported by --info-quests / --list-quests.
Range-checks both: questIdx against quest count, objIdx against the
selected quest's objective count. Each errors with a precise out-of-
range message.

Verified: scaffolded zone, added quest with 3 objectives (kill /
collect / talk), removed objective at index 1 (the collect),
--info-quests confirms '1 kill, 0 collect, 1 talk' afterward.
Bad quest and bad objective indices both rejected with exit 1.

Quest-authoring CLI is now fully symmetric:
  --add-quest                   --remove-quest
  --add-quest-objective         --remove-quest-objective
  --add-quest-reward-item       (additive only — items are dedup'd)
  --set-quest-reward            (idempotent field-level update)
2026-05-06 12:35:10 -07:00
Kelsi
9c46d3aeeb feat(editor): add --add-tile to extend a zone with another ADT tile
--scaffold-zone creates a zone with one tile; some zones (continent
fragments, large dungeons) span multiple ADT tiles. This extends an
existing zone:

  wowee_editor --add-tile custom_zones/MyZone 29 30           # default baseHeight=100
  wowee_editor --add-tile custom_zones/MyZone 28 31 250.5     # custom baseHeight

What it does:
- Generates a fresh blank-flat WHM/WOT pair via the same factory
  --scaffold-zone uses, so output is consistent.
- Appends (tx, ty) to ZoneManifest::tiles. Save() rebuilds the
  files-block from tiles, so the new adt_TX_TY entry appears
  automatically in zone.json.

Safety:
- Tile coord must be in WoW grid [0, 64) per axis; rejects 99,99.
- Refuses if the tile is already in the manifest (catches typos).
- Refuses if the .whm/.wot files exist on disk but aren't in the
  manifest (catches manifest-out-of-sync drift from hand edits).
- Optional baseHeight allows seeding flat terrain at a non-default
  elevation.

Verified end-to-end: scaffolded 1-tile zone, added 2 more tiles
(one with custom height). Result: 3 tiles in manifest, 6 files on
disk, files-block has all 3 adt_TX_TY entries. Duplicate and
out-of-range cases both rejected with exit 1.
2026-05-06 12:33:32 -07:00
Kelsi
b04a3ede99 feat(editor): add --convert-dbc-json and --convert-json-dbc
Standalone DBC <-> JSON converters. asset_extract --emit-open already
does this in bulk during extraction, but designers often need to
refresh ONE sidecar after a hand-edit, or push an edited JSON back to
binary DBC for private-server compat:

  wowee_editor --convert-dbc-json Spell.dbc          # -> Spell.json
  wowee_editor --convert-json-dbc Spell.json         # -> Spell.dbc
  wowee_editor --convert-dbc-json Spell.dbc out.json # custom output

DBC -> JSON: uses the wowee-dbc-json-1.0 schema (matches asset_extract
output exactly so the editor's runtime overlay loader picks it up).
Same string/float/uint heuristic the existing emitter uses.

JSON -> DBC: builds a Blizzard-format-compatible binary DBC with:
- 20-byte WDBC header (magic + recordCount + fieldCount + recordSize +
  stringBlockSize)
- Records as little-endian uint32 fields (always LE per spec)
- Deduped string block with leading NUL so offset=0 = empty string
- Tolerates JSON numeric types (string/float/int/bool/null) and
  derives fieldCount from row size if header omits it

Verified with a hand-built 3-record/2-field DBC: DBC->JSON->DBC->JSON
round-trips with identical records[] arrays, no data loss. Output
is byte-loadable by the existing DBC parser.
2026-05-06 12:31:33 -07:00
Kelsi
92ea41f1ae feat(editor): add --export-whm-obj for terrain heightmap visualization
Completes the open-format -> universal-text bridge for the last
binary geometry format. WHM was the missing one; designers now have
OBJ exports for all four (WOM models, WOB buildings, WOC collision,
WHM terrain).

  wowee_editor --export-whm-obj custom_zones/MyZone/MyZone_30_30

Mesh layout:
- 9x9 outer vertex grid per chunk (skips the 8x8 inner verts the
  engine uses for 4-tri fans). That's 81 verts and 128 tris per
  chunk; full ADT = 20736 verts + 32768 tris.
- One OBJ 'g chunk_X_Y' per MapChunk so designers can hide chunks
  individually in Blender (e.g. to inspect a single problem area).
- Hole bits respected — cave-entrance quads correctly disappear.
- Coords match WoweeCollisionBuilder's outer-grid layout exactly,
  so an --export-whm-obj and --export-woc-obj of the same source
  align spatially when overlaid in Blender. (Verified: first vertex
  of both is (1066.67, 1066.67, ~98.5) for a tile (30, 30) export.)
- UVs are simply row/8, col/8 in [0,1] per chunk so a checker
  texture renders at the canonical scale for size reference.

Verified: scaffolded zone -> WHM/WOT auto-built -> --export-whm-obj
produces 256 chunks loaded, 20736 verts, 32768 faces, 256 'g'
blocks. Counts exactly match the chunk × outer-grid math.
2026-05-06 12:27:46 -07:00
Kelsi
0f05759027 feat(editor): add --rename-zone for in-place zone renames
--copy-zone duplicates and renames; --rename-zone does it in place
without doubling disk usage. Useful when fixing typos or rebranding
a zone without touching its data:

  wowee_editor --rename-zone custom_zones/Old 'Brand New Name'

What changes:
- zone.json mapName: Old -> Brand_New_Name
- zone.json displayName: 'Old' -> 'Brand New Name'
- zone.json files block (adt_NN_NN, wdt): regenerated from new slug
- slug-prefixed files: Old_28_30.whm -> Brand_New_Name_28_30.whm
- the directory itself: custom_zones/Old/ -> custom_zones/Brand_New_Name/

Order of operations matters for crash safety: slug-prefixed files
get renamed first (atomic per-file via fs::rename), then the
manifest is rewritten in the still-named source dir, then the
directory is moved last. If the dir-move fails we surface the
manifest-already-updated state so the user can recover.

Refuses to run if target dir already exists (avoids silent merge),
or if both slug AND displayName already match the target (no-op).

Verified end-to-end: scaffolded 'Old' with 1 creature, renamed to
'Brand New Name'. Result: dir renamed, .whm/.wot files renamed,
zone.json fully updated, creatures.json preserved with 1 entry.
2026-05-06 12:24:36 -07:00
Kelsi
a20e795ddb feat(editor): add --export-woc-obj for collision-mesh visualization
WOC is the open collision format used for movement queries. When the
player gets stuck or walks somewhere they shouldn't, you need to SEE
the mesh — eyeballing flag bits in --info-woc only goes so far.

  wowee_editor --export-woc-obj custom_zones/Z/Z_30_30.woc

Each flag class gets its own OBJ 'g' block so designers can hide
categories independently in Blender to debug:
  - walkable / steep / water / indoor / nonwalkable
  - and combinations like 'walkable_water' for shallow swimable areas

Implementation notes:
- Triangle-soup topology preserved (no vertex dedupe). Adjacent
  triangles often have different flags; deduping would merge
  categories and lose the boundary.
- Bucket by exact flag value (uint8) so combination flags become
  their own group rather than counting twice.
- Index math: triangle t vertex k -> OBJ index (t*3 + k + 1).
- Header comment preserves source path, triangle counts, walkable/
  steep counts, and tile (x, y) for provenance.

Verified on a freshly-scaffolded zone's auto-built WOC (32768
triangles, all walkable on flat terrain): export reports
'1 flag class(es)', single 'g walkable' block in the OBJ, last
face index correctly = 32768*3 = 98304.
2026-05-06 12:22:31 -07:00
Kelsi
a9789b0154 feat(editor): add --import-wob-obj to round-trip OBJ back into WOB
Closes the WOB <-> universal-format round trip, mirroring what
--import-obj does for WOM. Workflow now works end-to-end for
buildings:

  asset_extract        # WMO -> WOB (open binary)
  --export-wob-obj     # WOB -> OBJ (universal text)
  ... edit in Blender / MeshLab / Maya ...
  --import-wob-obj     # OBJ -> WOB (back to engine format)
  --validate-wob       # confirm consistency

Mapping handles:
- Each OBJ 'g name' starts a new WOB group; faces under it become
  that group's indices. Default 'imported' group catches faces
  before any 'g' directive (raw OBJ files from non-DCC sources).
- Per-group dedupe table on (pos, uv, normal) triples — each WOB
  group has its own local vertex array, so the global OBJ index
  pool gets remapped per group.
- '_outdoor' suffix stripped + isOutdoor flag set, mirroring
  --export-wob-obj's naming convention.
- Doodad placements recovered from the # doodad ... comment lines
  --export-wob-obj writes; round-trip preserves them via re-parse.
- UV V flipped back (1.0 - v) so the round trip is exact.
- Per-group bounds + global bound radius computed from positions.

Verified: WOB(2 groups, 7 verts, 3 tris, 1 doodad) -> OBJ ->
WOB(2 groups, 7 verts, 3 tris, 1 doodad). validate-wob clean,
info-wob shows the same counts. Materials and portals don't
survive the OBJ trip (no semantic equivalent) — that's documented.
2026-05-06 12:20:37 -07:00
Kelsi
2048496aaf feat(editor): add --add-quest-reward-item and --set-quest-reward
Closes the quest-authoring CLI: --add-quest creates a quest, then
--add-quest-objective adds completion conditions, and now these add
the rewards.

  wowee_editor --add-quest-reward-item $Z 0 'Item:Sword' 'Item:Shield' 'Item:Potion'
  wowee_editor --set-quest-reward      $Z 0 --xp 999 --gold 5 --silver 30

--add-quest-reward-item greedy-consumes any trailing positional args
that don't start with '-', so a whole loot table can be added in one
invocation rather than N round-trips through load+save.

--set-quest-reward updates only the fields explicitly passed (--xp /
--gold / --silver / --copper) — avoids the round-trip-and-clobber
footgun of a 'replace whole reward' command. Field flags are
order-independent. At least one flag is required (else error).

Both validate questIdx against quest count and bail with exit 1 on
out-of-range / missing file / bad numeric value.

Verified: scaffolded zone, added quest with default xp=100, batch-
added 3 item rewards in one shot, then set xp=999/gold=5/silver=30
in one shot. --info-quests reports total XP=999, with-items=1.
2026-05-06 12:18:20 -07:00
Kelsi
705899d6a7 feat(editor): add --info-png and --info-jsondbc sidecar inspectors
PNG (BLP sidecar) and JSON DBC (DBC sidecar) didn't have inspectors —
the existing --info-* suite only covered the binary native formats
(WOM/WOB/WOC/WOT/WHM/WCP). These fill the gap so debugging the
asset_extract --emit-* output doesn't need GIMP / a JSON pretty-printer:

  wowee_editor --info-png Textures/Sky01.png
  PNG: Textures/Sky01.png
    size      : 256 x 256
    bit depth : 8
    color     : rgba (4 channels)
    file bytes: 142336

  wowee_editor --info-jsondbc db/Spell.json
  JSON DBC: db/Spell.json
    format    : wowee-jsondbc-1
    source    : Spell.dbc
    records   : 47882 (header) / 47882 (actual)
    fields    : 234

PNG inspector reads only the IHDR chunk (24 bytes total) — no pixel
decode — so it works instantly on huge files. Validates the
8-byte signature, parses big-endian width/height, derives channel
count from PNG color type (grayscale/rgb/palette/grayscale+alpha/
rgba per spec table 11.1).

JSON DBC inspector parses with nlohmann::json, reports the schema
fields (format/source/recordCount/fieldCount), and cross-checks
recordCount against actual records[] array length. Exits 1 on
count mismatch — catches truncated extracts where the header lies
about how much data follows. Verified with hand-rolled 2x3 RGBA
PNG (correct dims) and JSON DBC files (one matched, one mismatched
99 vs 1).
2026-05-06 12:16:01 -07:00
Kelsi
23a2233852 feat(editor): add --export-wob-obj for buildings -> Wavefront OBJ
WOM (the open M2 replacement) already round-trips through OBJ; this
extends the universal-format bridge to WOB (the open WMO replacement)
so buildings can also be edited in Blender / MeshLab / Maya / etc.

  wowee_editor --export-wob-obj House         # writes House.obj
  wowee_editor --export-wob-obj House out.obj # custom path

Mapping decisions:
- Each WOB group becomes one OBJ 'g' block (named after the group;
  outdoor groups get an '_outdoor' suffix). Preserves the room/floor
  structure for downstream selection and per-area editing.
- Single global vertex pool with per-group offsets (OBJ requires v
  indices to be globally 1-based; we track a running vertOffset).
- UV V flipped (1.0 - v) so texturing matches Blender bottom-left
  convention, same as --export-obj for WOM.
- Doodad placements written as # comment lines at the end. OBJ has
  no native concept for instanced models, but emitting them as
  structured comments keeps the placement data recoverable for
  tools that want to re-instance them.
- Portals and material flags drop on the floor — OBJ has no
  semantics for either. The native WOB always remains canonical.

Verified on a synthesized 2-group house (4-vert floor + 3-vert wall,
1 doodad): output OBJ has 7 verts / 7 vt / 7 vn entries, 2 'g'
blocks with proper index offsetting, doodad comment line preserved.
2026-05-06 12:14:04 -07:00
Kelsi
caa0df7e5e feat(editor): add --add-quest-objective for headless objective creation
--add-quest creates a quest shell but quests with no objectives never
complete in-game. This fills the gap:

  wowee_editor --add-quest-objective <zoneDir> <questIdx> \
    <kill|collect|talk|explore|escort|use> <targetName> [count]

Workflow:

  Z=custom_zones/MyZone
  wowee_editor --add-quest $Z 'Hunt Wolves' 100 100 250 5
  wowee_editor --add-quest-objective $Z 0 kill    'Wolf'      5
  wowee_editor --add-quest-objective $Z 0 collect 'Wolf Pelt' 3

Auto-generates a description from type+count+name so addons and
tooltips show something sensible ('Slay 5 Wolf', 'Collect 3 Wolf
Pelt', 'Talk to Mayor'). Designers can hand-edit quests.json after
the fact for bespoke prose; the auto-text is the floor, not the
ceiling.

Quest index matches --list-quests output (0-based). Out-of-range
index, unknown type, or missing quests.json all error with clear
messages and exit 1.

Verified: scaffolded zone, added 2 quests + 3 objectives across
them; --info-quests reports '1 kill, 1 collect, 1 talk'. Bad type
and bad index both rejected.
2026-05-06 12:12:06 -07:00
Kelsi
fdc7ca7ee7 feat(editor): add --diff-zone for comparing two unpacked zone dirs
The natural counterpart to --diff-wcp (which compares two .wcp
archives) — operates on the unpacked side of the workflow. Shows
exactly what changed across zone.json fields, creature roster,
object placements, and quest list:

  wowee_editor --diff-zone custom_zones/Base custom_zones/Variant

  Diff: custom_zones/Base vs custom_zones/Variant
    manifest  : 2 field diff(s)
      ~ mapName: 'Base' -> 'Variant'
      ~ displayName: 'Base' -> 'Variant'
    creatures : 2 vs 2
      - Bear
      + Tiger
    quests    : 1 vs 2
      + Defeat the Tiger

Pairs naturally with --copy-zone: template a base zone, fork a
variant, then diff to see exactly what was customized. Useful for
PR review when a designer modifies a zone — diff against the
upstream version to scope the change.

Comparison strategy: sorted set diff on stable identifying fields
(creature.name, object.path, quest.title). This intentionally hides
position/orientation changes since those are continuous and would
flag every pixel-perfect tweak as a diff — content-level changes
are the signal here.

Exit 0 if identical, 1 otherwise (so CI can gate). JSON mode emits
per-category onlyA/onlyB arrays + manifestDiffs list + totalDiffs
count for programmatic consumption.

Verified: diffed two zones forked from a common base (one with
creature swap + new quest); reported 5 diffs across manifest +
creatures + quests with exit 1. Same zone vs itself reports
IDENTICAL with exit 0.
2026-05-06 12:10:22 -07:00
Kelsi
df4e0a30a7 feat(editor): add --import-obj to round-trip Wavefront OBJ back into WOM
Closes the open-format authoring loop:

  asset_extract     # M2 -> WOM (open binary)
  --export-obj      # WOM -> OBJ (universal text)
  ... edit in Blender / MeshLab / ZBrush / Maya ...
  --import-obj      # OBJ -> WOM (back to engine format)

The same WOM file ships in custom zones, gets validated by
--validate-wom, and renders identically through the existing pipeline
— no proprietary M2 ever needs to touch the authoring path.

Parser handles:
- v / vt / vn pools, deduped on (pos, uv, normal) triples so the
  resulting WOM vertex buffer stays compact
- 1-based AND negative (relative) face indices
- f tokens in v, v/t, v//n, and v/t/n forms
- Triangles, quads, and convex n-gons (fan-triangulated)
- CRLF line endings
- Reverses --export-obj's V flip (1.0 - v) so UVs round-trip exactly
- Auto-computes boundMin/Max/Radius from positions (renderer culls
  by these — wrong values make the model disappear)
- Output WOM is WOM1 (static); bones/anims/material flags don't
  exist in OBJ and stay empty by design

Verified end-to-end: WOM -> OBJ -> WOM -> validate-wom yields a
5-vert, 6-tri pyramid back identical to the input. Bounds, vertex
count, index count, and name all preserved.
2026-05-06 12:07:55 -07:00
Kelsi
5067432bae feat(editor): add --export-obj for WOM -> Wavefront OBJ conversion
WOM is our open M2 replacement, but it's still a custom binary format
no DCC tool understands out of the box. OBJ is the universally
supported text format that opens directly in Blender, MeshLab,
ZBrush, Maya — basically every 3D tool ever made:

  wowee_editor --export-obj Tree         # writes Tree.obj
  wowee_editor --export-obj Tree out.obj # custom output path

This closes the loop for content authors:
  asset_extract -> WOM (open binary) -> OBJ (universal text) ->
  edit in Blender -> back to WOM via a future --import-obj.

Layout details that matter for downstream tools:
- 1-based face indices (OBJ standard)
- UV V flipped (1.0 - v) so texturing matches between Vulkan
  top-left and Blender bottom-left conventions
- Per-batch groups when WOM3 batches exist, named with the
  texture basename so material assignment carries through
- Single 'mesh' group for WOM1/WOM2 models
- Header comment preserves provenance (source, version, counts)

Verified on a synthesized 5-vert pyramid (4 base + apex, 6 tris):
output OBJ has 5 v / 5 vt / 5 vn entries, 6 f lines, opens cleanly
in MeshLab. Build green, ctest 31/31.
2026-05-06 12:05:41 -07:00
Kelsi
78a2624159 feat(editor): add --list-{creatures,objects,quests} for indexed enumeration
Verbose enumeration of every entry, complementing the existing
--info-* (summary counts) and unblocking --remove-* (which takes a
0-based index).

  wowee_editor --list-creatures custom_zones/Z/creatures.json

  creatures.json: custom_zones/Z/creatures.json (2 total)
    idx  name                            lvl  display  pos (x, y, z)
      0  Wolf                             7    11430  (100.0, 200.0, 50.0)
      1  Black Bear                      12    11445  (110.0, 210.0, 55.0)

The 'idx' column is exactly what --remove-creature/object/quest
takes, closing the workflow loop:

  wowee_editor --list-creatures $Z/creatures.json |
    grep -i 'wolf' | awk '{print $1}' |
    xargs -I{} wowee_editor --remove-creature $Z {}

Each command has matching --json mode that emits per-entry records
with index, key fields, and position. Verified on a synthesized
zone with 2 creatures + 2 objects + 2 quests; tables align, indices
match, JSON is well-formed and round-trips through jq cleanly.
2026-05-06 12:03:55 -07:00
Kelsi
aa499b7462 feat(editor): add --remove-{creature,object,quest} CRUD-delete commands
Symmetric counterparts to --add-{creature,object,quest} from earlier
batches:

  wowee_editor --remove-creature <zoneDir> <index>
  wowee_editor --remove-object   <zoneDir> <index>
  wowee_editor --remove-quest    <zoneDir> <index>

Index is 0-based and is reported in the corresponding --info-* output;
nothing identifies entries reliably across reloads, so name-based
removal would silently delete the wrong row when duplicates exist.
Pair them in scripts:

  idx=$(wowee_editor --info-creatures $Z/creatures.json --json |
        jq '.spawns | map(.name) | index("Wolf")')
  wowee_editor --remove-creature $Z $idx

Each prints the removed entry's name/path and the new total so
scripts can verify the right row went away. Out-of-range indices
exit 1 with a clear message.

Verified end-to-end: scaffolded zone, added 3 creatures + 2 objects
+ 2 quests, removed one of each by index, totals correctly went
3->2/2->1/2->1. Bad index 99 properly errors out.
2026-05-06 12:01:52 -07:00
Kelsi
270fcd8e55 feat(editor): add --list-missing-sidecars for actionable open-format triage
--info-extract reports sidecar coverage % per format, but doesn't say
which files are missing. This makes it actionable:

  wowee_editor --list-missing-sidecars Data/

Output: one path per line, prefixed with the missing extension so
shell tools can filter:

  png   Data/Textures/Skybox/Sky01.blp
  json  Data/DBFilesClient/SoundEntries.dbc
  wom   Data/Character/Human/Male/HumanMale.m2

Pipe into xargs to drive a targeted re-extract:

  wowee_editor --list-missing-sidecars Data/ |
    awk '/^png/ {print $2}' |
    xargs asset_extract --emit-png-only

Skips WMO group files (Foo_NNN.wmo) since only the parent file gets
a .wob sidecar — they would otherwise inflate the missing list with
hundreds of false positives per WMO.

Exit 1 when anything is missing (so CI can gate). JSON mode emits
arrays per format type for programmatic consumption. Verified
against synthetic dir with 5 files (4 lacking sidecars + 1 with
.png present): all 4 reported, the one with sidecar correctly
omitted.
2026-05-06 12:00:12 -07:00
Kelsi
8f6315f155 feat(editor): add --validate-woc + --validate-whm and roll into validate-all
Round out the per-format validator suite. Open-format zone validation
now covers all four binary formats:

  --validate-wom Tree
  --validate-wob House
  --validate-woc terrain.woc
  --validate-whm Zone_28_30
  --validate-all custom_zones/Zone1   # runs everything

WOC checks: finite vertex coords on every triangle, no degenerate
triangles (two verts identical), known flag bits only (0x0F mask),
tile coords within WoW grid (< 64), bounds.min <= bounds.max.

WHM/WOT checks: finite heights across all 145 verts/chunk, finite
chunk position vectors, tile coord in [0, 64), reasonable height
envelope ([-10000, 10000] is a generous outer bound — beyond that
suggests units confusion), placements have finite positions and
nameId within doodadNames/wmoNames table size.

validate-all now reports all four format counts (WOM/WOB/WOC/WHM)
and aggregates errors. Verified end-to-end: a fresh scaffolded zone
with --build-woc yields 256/256 chunks loaded, 32768 walkable
triangles, validate-all PASSED. Synthesized WOC with 0xFF flags
correctly fails with 'unknown flag bits 0xFF' and exit 1.
2026-05-06 11:58:20 -07:00