Commit graph

3877 commits

Author SHA1 Message Date
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
Kelsi
67b719a2d9 feat(editor): add --validate-all + extract validator helpers
Walks a directory recursively and runs the deep validators on every
.wom and .wob it finds. Single CI gate for an entire zone tree:

  wowee_editor --validate-all custom_zones/MyZone --json

Reports per-file failures (capped at first 20 to keep output bounded)
plus aggregate counts so you know which file to drill into with
--validate-wom or --validate-wob individually.

Refactor: pulled the validation bodies out of --validate-wom and
--validate-wob into static helpers (validateWomErrors / validateWobErrors)
returning vector<string>. The per-file commands now share the same
logic as --validate-all — fix one, fix all three. ~200 lines of
duplicate validation code consolidated.

Verified end-to-end: seeded /tmp dir with 2 WOMs (1 with DAG bone
violation) + 1 valid WOB, --validate-all reports 'WOM: 2 total, 1
failed' / 'WOB: 1 total, 0 failed' with the bad file's full path and
error printed below. JSON mode emits per-file failure list for CI.
2026-05-06 11:54:54 -07:00
Kelsi
542a3217f7 feat(editor): add --validate-wob for deep building consistency checks
Companion to --validate-wom; same load-is-lenient story for buildings:

  wowee_editor --validate-wob House [--json]

Cross-references checked per group:
- indices.size() divisible by 3
- every index < vertices.size()
- material textures non-empty
- group boundMin <= boundMax per axis

Building-level:
- portal groupA/groupB references real groups (or -1 = exterior)
- portal polygon has >= 3 vertices
- doodad modelPath non-empty
- doodad scale finite and > 0
- boundRadius >= 0

Errors batched (caps to 3 listed plus '... and N more') so a thousand
bad indices in one group don't drown the report. JSON mode emits the
same error list for CI consumption. Exit 1 on any failure so shell
scripts can gate on it.

Verified against a synthesized 2-group building with intentional
flaws across every category — caught all 4 (group indices count,
empty material texture, short portal polygon, empty doodad model)
and exited 1.
2026-05-06 11:51:26 -07:00
Kelsi
15f9cbb50c feat(editor): add --validate-wom for deep WOM consistency checks
The WOM loader is intentionally lenient — it clamps out-of-range
indices to 0, resets bad bone parents to -1, and skips invalid
batches. That keeps broken files from crashing the renderer, but
also hides corruption that authoring scripts should catch BEFORE
the file ships.

  wowee_editor --validate-wom Tree [--json]

Cross-references checked (none auto-fixed by load()):
- bone DAG order: parent must be strictly less than self index
- animation boneKeyframes count == bone count when both nonzero
- batch indexStart+Count <= total indexCount
- batch indexCount divisible by 3
- batch textureIndex < texturePaths size
- boundMin <= boundMax per axis, boundRadius >= 0
- header version in [1,3], indices count divisible by 3

Verified against a synthesized 3-bone model with parent=self+1
(invalid DAG order) — load() preserves it as written, validator
reports 'bone 1 parent=2 not strictly less (DAG order)' and
exits 1. JSON mode emits errorCount + errors[] + passed boolean
for CI scripts to gate on.
2026-05-06 11:49:30 -07:00
Kelsi
1c4c5a97fa feat(editor): add --copy-zone CLI for templating zones
Duplicate an existing zone to a new slug:

  wowee_editor --copy-zone custom_zones/Original "My New Zone"

Workflow this enables: scaffold one base zone, populate it with
creatures/objects/quests, then copy-zone N times to create variants
without re-scaffolding each. Designers can template a 'forest base'
zone and stamp it into Dark Forest, Frozen Forest, etc.

What it does:
- Recursive copy preserves any subdirs (e.g. data/ for DBC sidecars)
- Reads source slug from zone.json (not the dir name) to know what
  prefix to rewrite — handles users who renamed dirs without
  touching the manifest
- Renames slug-prefixed files (Original_28_30.whm -> NewSlug_28_30.whm,
  matches both _-suffixed and .-suffixed forms)
- Saves a fresh zone.json via ZoneManifest::save which rebuilds the
  files-block from mapName, so the manifest references the renamed
  files correctly

Verified end-to-end: scaffolded Original, added creature + quest,
copied to 'My New Zone'. Result: 2 files renamed, zone.json
mapName/displayName/files all updated, creatures.json + quests.json
copied verbatim.
2026-05-06 11:44:31 -07:00
Kelsi
070919ef51 feat(editor): add --add-quest CLI for headless quest creation
Third headless authoring command, finishing the trio:

  wowee_editor --add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]

Optional positional fields are read in order; omit from the right.
Bare 'wowee_editor --add-quest zone Title' produces a valid quest
with default values, matching the editor GUI's New Quest behavior.

Verified: appended 3 quests (250+100+0 XP) to a scaffolded zone,
--info-quests --json reports total=3, totalXp=450, withReward=3.
2026-05-06 11:41:11 -07:00
Kelsi
1017ab4751 feat(editor): add --add-object CLI for headless M2/WMO placement
Mirrors --add-creature for the object placer:

  wowee_editor --add-object <zoneDir> <m2|wmo> <gamePath> <x> <y> <z> [scale]

Lets shell scripts populate objects/buildings without the GUI:

  for tree in 100,200 150,250 200,300; do
    x=${tree%,*}; y=${tree#*,}
    wowee_editor --add-object "$zone" m2 'World/Doodad/Tree.m2' $x $y 0
  done

Loads existing objects.json first then appends, so multiple
invocations build up. Optional scale slot lets the caller set
non-default size (clamped to >0 by the load-time guard).
Verified end-to-end: scaffolded zone → added m2 + wmo → info-objects
reports total=2 with the correct scale range.
2026-05-06 11:37:10 -07:00
Kelsi
83d20180c3 feat(editor): add --add-creature CLI for headless creature placement
Appends a single creature spawn to a zone's creatures.json. First
real authoring tool that doesn't need the GUI placement system —
useful for batch-populating zones via shell script:

  for npc in goblin spider wolf; do
    wowee_editor --add-creature "$zone" "$npc" 100 200 50
  done

Args: <zoneDir> <name> <x> <y> <z> [displayId] [level]
- displayId 0 → SQL exporter substitutes 11707 (generic humanoid)
- level defaults to 1
- Coordinates are render-space (renderX=wowY, renderY=wowX)

Loads any existing creatures.json first then appends, so multiple
invocations build up the file. The standard NPC spawner caps
(50k creatures) protect against runaway scripts.
2026-05-06 11:35:07 -07:00
Kelsi
57b81a2344 feat(extract): --purge-proprietary --json for machine-readable purge report
Schema:

  asset_extract --purge-proprietary Data --json
  {
    "dir":         "Data",
    "confirmed":   false,
    "candidates":  21570,
    "removed":     0,
    "totalBytes":  17175328768
  }

"candidates" = files where a sidecar exists and is at least as
new (would be deleted on --confirm-purge). "removed" reflects the
actual delete count when --confirm-purge is set; 0 in dry-run.

Lets CI scripts gate on the dry-run candidate count before actually
purging:

  count=$(asset_extract --purge-proprietary Data --json | jq .candidates)
  [ "$count" -gt 1000 ] && asset_extract --purge-proprietary Data --confirm-purge

The asset_extract --json flag now applies to both upgrade and purge
modes.
2026-05-06 11:32:42 -07:00
Kelsi
47f3f6c55b feat(extract): --upgrade-extract --json for machine-readable upgrade summary
Mirrors the wowee_editor --info-extract --json schema so CI scripts
can use a single jq filter for both pre-check and post-upgrade:

  asset_extract --upgrade-extract Data --json
  {
    "dir": "Data",
    "elapsedSeconds": 47.2,
    "skipped": 0,
    "png":     { "ok": 12340, "failed": 0 },
    "jsonDbc": { "ok": 240,   "failed": 0 },
    "wom":     { "ok": 8500,  "failed": 0 },
    "wob":     { "ok": 730,   "failed": 0 },
    "terrain": { "ok": 5660,  "failed": 0 }
  }

Now both wowee_editor and asset_extract have JSON-mode summaries
covering every command CI is likely to gate on. The extraction
pipeline is fully scriptable end-to-end.
2026-05-06 11:31:01 -07:00
Kelsi
65d867c035 feat(editor): --json mode on --info-wot and --info-zone
Last two inspectors to gain --json mode. Schemas show off the
nested structure these commands surface (chunk counts, audio
sub-object, tile array):

  --info-wot  → { base, tileX, tileY,
                  chunks: { withHeightmap, withLayers, withWater },
                  textures, doodads, wmos, heightMin, heightMax }
  --info-zone → { file, mapName, displayName, mapId, biome, baseHeight,
                  hasCreatures, description, tiles: [[x,y], ...],
                  flags: { allowFlying, pvpEnabled, isIndoor, isSanctuary },
                  audio?: { music, musicVolume, ambienceDay,
                            ambienceVolume, ambienceNight } }

Every CLI inspector in the wowee_editor binary now supports --json.
Total: 14 commands with machine-readable output (extract/zone/wcp
inspectors, validate, diff-wcp, list-zones, info-wom/wob/woc/wot/
zone/creatures/objects/quests, zone-summary).
2026-05-06 11:27:53 -07:00
Kelsi
02227d209e feat(editor): --json mode on remaining binary inspectors
Adds --json to --info (WOM), --info-wob, --info-woc. Each emits a
structured object matching the labelled fields from the human-
readable output:

  --info <wom>   → { wom, version, name, vertices, indices, triangles,
                     textures, bones, animations, batches, boundRadius }
  --info-wob     → { wob, name, groups, portals, doodads, boundRadius,
                     totalVerts, totalTris, totalMats }
  --info-woc     → { woc, tileX, tileY, triangles, walkable, steep,
                     boundsMin: [x,y,z], boundsMax: [x,y,z] }

Twelve inspectors now support --json; only --info-wot and --info-zone
left without (they have nested structures and are less commonly
gated by CI).
2026-05-06 11:25:31 -07:00
Kelsi
dbb9879a6e feat(editor): --json mode on remaining content inspectors
Adds --json to --info-creatures, --info-objects, --info-quests so
the per-content inspectors match the per-zone aggregator. Schemas
match the existing --zone-summary --json sub-objects.

Now 9 inspectors support --json:
  --info-extract     --info-wcp        --validate
  --info-creatures   --info-objects    --info-quests
  --zone-summary     --list-zones      --diff-wcp

CI can now drill into per-content reports the same way as the
top-level summary, e.g. fail a build if a zone's quests have any
chain errors:

  wowee_editor --info-quests "$zone/quests.json" --json \
    | jq -e '.chainErrors | length == 0'
2026-05-06 11:23:02 -07:00
Kelsi
39a9f224a0 feat(editor): --diff-wcp --json for machine-readable archive diff
Sixth and last commonly-used inspector to gain --json mode. Schema:

  {
    "a": "path/to/A.wcp", "b": "path/to/B.wcp",
    "identical": 1, "changed": 0, "onlyA": 2, "onlyB": 2,
    "changedFiles": [
      {"path": "foo.dbc", "aSize": 1024, "bSize": 1280}
    ],
    "onlyAFiles": ["Da_30_30.whm", "Da_30_30.wot"],
    "onlyBFiles": ["Db_31_31.whm", "Db_31_31.wot"]
  }

Exit code matches the human path: 0 if identical, 1 otherwise.
Lets CI verify two builds produce byte-identical archives:

  if ! wowee_editor --diff-wcp old.wcp new.wcp --json | jq -e \
    '.changed == 0 and .onlyA == 0 and .onlyB == 0'; then
    echo "WCP layout drift detected"; exit 1
  fi
2026-05-06 11:19:29 -07:00
Kelsi
987dc81f13 feat(editor): --list-zones --json for machine-readable zone discovery
Adds JSON mode to the zone discovery scanner. Returns an array of
zone objects, each with name/dir/mapId/author/description/tiles/
hasCreatures/hasQuests.

Lets CI scripts iterate every available zone and run a per-zone
gate, e.g.:

  for zone in $(wowee_editor --list-zones --json | jq -r '.[].directory'); do
    wowee_editor --validate "$zone" --json | jq -e '.score == 7'
  done

Fifth and last commonly-used inspector to gain --json mode (after
--info-extract, --validate, --info-wcp, --zone-summary).
2026-05-06 11:16:08 -07:00
Kelsi
81cc146d58 feat(editor): --zone-summary --json for unified machine-readable report
Adds --json output to the one-shot zone-summary aggregator. Refactor
also moves creature/object/quest data reads to a shared step before
either branch so both human and JSON outputs use the same numbers.

Schema:

  {
    "zone": "custom_zones/Foo",
    "score": 3, "maxScore": 7,
    "formats": "WOT WHM zone.json ",
    "counts": { "wot":1, "whm":1, "wom":0, "wob":0, "woc":0, "png":0 },
    "creatures": { "total":N, "hostile":N, "questgiver":N, "vendor":N },
    "objects":   { "total":N, "m2":N, "wmo":N },
    "quests":    { "total":N, "chainWarnings":N }
  }

Now CI can gate on any combination — open-format coverage, NPC
counts, quest chain health — from a single command. Fourth and
last commonly-CI'd inspector to gain --json mode (after
--info-extract, --validate, --info-wcp).
2026-05-06 11:14:41 -07:00
Kelsi
89f4b57e99 feat(editor): --info-wcp --json for machine-readable WCP metadata
Mirrors --info-extract --json and --validate --json. Schema:

  {
    "wcp": "/path/to/zone.wcp",
    "name": "Wj",
    "author": "...",
    "description": "...",
    "version": "1.0",
    "format": "wcp-1.0",
    "mapId": 9000,
    "fileCount": 3,
    "totalBytes": 177671,
    "categories": { "terrain": 2, "data": 1 }
  }

Lets CI scripts inspect packed zones — e.g. fail a release if a
zone's WCP doesn't include a creature category, or auto-tag a
release with the totalBytes field.
2026-05-06 11:12:18 -07:00
Kelsi
bed7e4b892 feat(editor): --validate --json for machine-readable zone scoring
Mirrors --info-extract --json. Schema:

  {
    "zone": "custom_zones/Foo",
    "score": 3, "maxScore": 7,
    "formats": "WOT WHM zone.json ",
    "wot": { "present": true,  "count": 1, "valid": true },
    "whm": { "present": true,  "count": 1, "valid": true },
    "wom": { "present": false, "count": 0, "valid": false },
    "wob": { ... },
    "woc": { ... },
    "png": { "present": true,  "count": 12 },
    "zoneJson": true,
    "creatures": false, "quests": false, "objects": false
  }

Exit code is still 0 if score == 7 (full open coverage), 1 otherwise,
so CI gates work the same way:

  if ! wowee_editor --validate "$zone" --json | jq -e '.score == 7'; then
    echo "zone incomplete"; exit 1
  fi
2026-05-06 11:09:59 -07:00
Kelsi
25d68d5a6a feat(editor): --info-extract --json for machine-readable coverage output
Adds an optional --json flag that emits a structured nlohmann JSON
object instead of the human-readable text. Schema:

  {
    "dir": "...",
    "totalBytes": N, "proprietaryBytes": N, "openBytes": N,
    "overallCoverage": 100.0,
    "blp_png":  { "proprietary": N, "sidecar": N, "coverage": % },
    "dbc_json": { ... },
    "m2_wom":   { ... },
    "wmo_wob":  { ... },
    "adt_whm":  { ... }
  }

Lets CI scripts gate on coverage:

  cov=$(wowee_editor --info-extract Data --json | jq .overallCoverage)
  if [ "$cov" != "100" ]; then asset_extract --upgrade-extract Data; fi
2026-05-06 11:07:11 -07:00
Kelsi
e547b4b82b feat(extract): add --purge-proprietary mode to free disk after open conversion
Walks an extracted tree and removes every BLP/DBC/M2/skin/WMO/ADT
that has a confirmed open-format sidecar at least as new. Dry-run
by default — requires --confirm-purge to actually delete:

  asset_extract --purge-proprietary Data/expansions/wotlk
  Dry-run: would purge proprietary files...
    would remove: 21570 files (16380.4 MB)
    (re-run with --confirm-purge to actually delete)

  asset_extract --purge-proprietary Data/expansions/wotlk --confirm-purge
  Purging...
    removed: 21570 files (16380.4 MB)

Pairing rules:
  .blp  → .png
  .dbc  → .json
  .m2   → .wom
  .skin → matching .m2's .wom (foo00.skin pairs with foo.wom)
  .wmo  → .wob (root)
  .wmo group sub-files (foo_NNN.wmo) → parent foo.wob
  .adt  → .whm

Servers can't run without proprietary files, so this only makes
sense for wowee-runtime-only setups. Files without a sidecar are
left untouched.
2026-05-06 11:05:10 -07:00
Kelsi
5799b5f88f feat(editor): --info-extract reports proprietary vs open-format byte totals
Adds two summary lines so users can see how big a 'purge proprietary
after open conversion' workflow would shrink their tree (or how
much extra a dual-format extraction costs):

  proprietary bytes: 18432.4 MB
  open-format bytes: 21340.7 MB (115.8% of proprietary)

Counts every BLP/DBC/M2/WMO/ADT into the proprietary bucket and
every PNG/JSON/WOM/WOB/WHM/WOT/WOC into the open bucket. The
ratio surfaces things like 'PNG is bigger than DXT-compressed BLP'
or 'JSON DBC is much smaller than the binary' without the user
having to run du themselves.
2026-05-06 11:03:23 -07:00
Kelsi
397034a750 feat(extract): incremental --upgrade-extract skips up-to-date sidecars
Compares the source file's mtime against the sidecar's; if the
sidecar is newer, the conversion is skipped and counted into
stats.skipped. Re-running --upgrade-extract on a fully-converted
tree is now nearly free (just an mtime check per file).

  asset_extract --upgrade-extract Data/expansions/wotlk
  Walking ... (first run)
    JSON (DBC→JSON)   : 240 ok
  asset_extract --upgrade-extract Data/expansions/wotlk
  Walking ... (second run, all sidecars up to date)
    up-to-date (skip) : 240
    JSON (DBC→JSON)   : 0 ok

emitOpenFormats() takes a new optional 'incremental' flag (default
false to preserve the asset_extract main-loop's overwrite behavior
since fresh extraction always wants new sidecars).

Verified end-to-end with a hand-built DBC: first run converts,
second run reports 'up-to-date (skip): 1'.
2026-05-06 11:00:20 -07:00
Kelsi
463a8cd751 feat(extract): expose --threads to upgrade-extract + report elapsed time
emitOpenFormats now takes an optional threadCount parameter (0 =
auto). The asset_extract --upgrade-extract path forwards opts.threads
so users can override the auto-detect when running on a CI machine
with limited cores or wanting deterministic timing.

Also wraps the upgrade pass with a chrono timer and prints elapsed
seconds so the parallelization payoff is visible at a glance:

  asset_extract --upgrade-extract Data/expansions/wotlk --threads 8
  Walking Data/expansions/wotlk for open-format upgrades...
    elapsed           : 47.2 s
    PNG (BLP→PNG)     : 12340 ok
    ...

Verified end-to-end: --threads 2 on 5 hand-built DBCs converts all
5 in well under a second.
2026-05-06 10:57:18 -07:00
Kelsi
cab1912441 perf(extract): parallelize open-format emit pass
Conversions are CPU-bound (BLP decode, M2/WMO parse, WOM/WOB
serialize) so the serial walk leaves cores idle. Now collects
every job into a vector during the directory walk, then dispatches
across hardware_concurrency() workers via an atomic next-index
queue. Stats use atomics to avoid the per-job mutex.

Expected ~5-8x speedup for full-tree --upgrade-extract on a
modern desktop. Existing test_open_format_emitter still passes
(it exercises both single-file emit*From* helpers and the parallel
emitOpenFormats walker).
2026-05-06 10:55:05 -07:00
Kelsi
30b15554a9 feat(extract): add --upgrade-extract for in-place sidecar generation
Standalone post-extract pass: walks an existing extracted asset
tree and writes open-format sidecars in place, without re-running
MPQ extraction.

  asset_extract --upgrade-extract Data/expansions/wotlk

Lets users with old extractions opt into the open-format pipeline
without losing the extracted state. Implies --emit-open if no
individual --emit-* flag is set.

Verified end-to-end: created a hand-built DBC in a temp dir, ran
--upgrade-extract, observed test.json appear with correct
metadata. Servers continue to read .dbc from manifest; the
runtime client picks up the new .json sidecar via the existing
pickup path.
2026-05-06 10:52:55 -07:00
Kelsi
5a816f104c feat(editor): add --info-extract CLI for extraction coverage report
Walks an extracted asset tree and reports per-format counts plus
how many proprietary files have a wowee open-format sidecar.
Lets users (or CI) see at a glance whether asset_extract was run
with --emit-open and how complete the open-format coverage is:

  BLP textures : 12340  (12340 PNG sidecar = 100.0% open)
  DBC tables   : 240    (240 JSON sidecar = 100.0% open)
  M2 models    : 8500   (0 WOM sidecar = 0.0% open)
  ...
  overall open-format coverage: 41.2%
  (run `asset_extract --emit-open` to fill missing sidecars)

Skips _NNN group sub-files when counting WMOs (only the root WMO
ships with a WOB sidecar). The headless CLI is now at 22 commands.
2026-05-06 10:50:17 -07:00