feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
#include "editor_app.hpp"
|
refactor(editor): extract gen-audio-* handlers into cli_gen_audio.cpp
main.cpp had grown past 28k lines, with each new procedural-
generation command adding 100-200 lines to the inline if/else
dispatch chain. This commit starts breaking that up by moving
the four audio-related handlers (--gen-audio-tone, -noise,
-sweep, --gen-zone-audio-pack) into their own translation unit.
Pattern established here for future family extractions:
- Family lives in cli_<family>.{hpp,cpp}
- Single dispatch entry point: bool handle<Family>(int& i, int argc,
char** argv, int& outRc) — true if matched (writes outRc), false
to fall through.
- main.cpp's argv loop calls each family's dispatcher first and
returns its outRc on match, before the legacy in-line chain.
Side-benefit: consolidated the duplicated 25-line WAV header
writer + 5ms attack/release envelope into shared helpers
(writeWavMono16, applyEdgeEnvelope) at the top of the new file.
main.cpp drops from 28,943 → 28,329 lines (-614). Audio family
is fully self-contained (~440 lines), behavior unchanged
(verified by re-running tone/noise/sweep + zone-audio-pack).
2026-05-08 16:19:30 -07:00
|
|
|
|
#include "cli_gen_audio.hpp"
|
2026-05-08 16:46:14 -07:00
|
|
|
|
#include "cli_zone_packs.hpp"
|
2026-05-08 17:12:10 -07:00
|
|
|
|
#include "cli_audits.hpp"
|
2026-05-08 17:36:10 -07:00
|
|
|
|
#include "cli_readmes.hpp"
|
2026-05-08 18:24:01 -07:00
|
|
|
|
#include "cli_zone_inventory.hpp"
|
2026-05-08 18:47:06 -07:00
|
|
|
|
#include "cli_project_inventory.hpp"
|
refactor(editor): extract printUsage into cli_help.cpp
Pulls the 597-line block of printf calls that emits the --help
text out of main.cpp into its own translation unit. printUsage
is the longest single function in main.cpp by far and was pure
boilerplate (no logic, just a flat list of help lines).
Function moved verbatim to wowee::editor::cli::printUsage; all
6 in-tree callers (--list-commands, --info-cli-stats,
--info-cli-categories, --info-cli-help, --validate-cli-help,
and the bare --help/-h handler) updated to the namespaced name.
The CLI-meta commands continue to capture printUsage's stdout
the same way, so behavior is identical (verified by re-running
--validate-cli-help, --info-cli-stats, --list-commands).
main.cpp drops 26,650 → 26,286 lines (-364 net; -597 from the
removal, +233 from the include and namespace-prefixing the
six call sites... wait, no, +6). Actually main.cpp net delta
matches the body extraction.
2026-05-08 20:12:15 -07:00
|
|
|
|
#include "cli_help.hpp"
|
2026-05-08 20:59:02 -07:00
|
|
|
|
#include "cli_gen_texture.hpp"
|
refactor(editor): extract 12 composite mesh primitives into cli_gen_mesh.cpp
Moves the recently-added composite-prop mesh handlers (rock,
pillar, bridge, tower, house, fountain, statue, altar, portal,
archway, barrel, chest) into their own translation unit. These
12 handlers were the most contiguous block of similar-shaped
mesh code in main.cpp.
Other mesh handlers (--gen-mesh dispatcher, fence, tree, grid,
stairs, disc, tube, capsule, arch, pyramid, from-heightmap,
textured) still live in main.cpp and may be migrated in
subsequent batches.
main.cpp drops 24,270 → 22,681 lines (-1,589). Behavior
verified by re-running rock/chest/archway/fountain.
2026-05-08 22:19:41 -07:00
|
|
|
|
#include "cli_gen_mesh.hpp"
|
2026-05-09 00:04:27 -07:00
|
|
|
|
#include "cli_mesh_io.hpp"
|
2026-05-09 00:36:51 -07:00
|
|
|
|
#include "cli_mesh_edit.hpp"
|
2026-05-09 01:18:09 -07:00
|
|
|
|
#include "cli_wom_info.hpp"
|
2026-05-09 01:57:37 -07:00
|
|
|
|
#include "cli_format_validate.hpp"
|
2026-05-09 02:25:05 -07:00
|
|
|
|
#include "cli_convert.hpp"
|
2026-05-09 02:48:58 -07:00
|
|
|
|
#include "cli_format_info.hpp"
|
2026-05-09 03:12:09 -07:00
|
|
|
|
#include "cli_pack.hpp"
|
2026-05-09 03:33:40 -07:00
|
|
|
|
#include "cli_content_info.hpp"
|
2026-05-09 03:52:44 -07:00
|
|
|
|
#include "cli_zone_info.hpp"
|
2026-05-09 04:14:32 -07:00
|
|
|
|
#include "cli_data_tree.hpp"
|
2026-05-09 04:35:08 -07:00
|
|
|
|
#include "cli_diff.hpp"
|
2026-05-09 05:05:22 -07:00
|
|
|
|
#include "cli_spawn_audit.hpp"
|
2026-05-09 05:19:04 -07:00
|
|
|
|
#include "cli_items.hpp"
|
2026-05-09 05:32:27 -07:00
|
|
|
|
#include "cli_extract_info.hpp"
|
2026-05-09 05:45:00 -07:00
|
|
|
|
#include "cli_export.hpp"
|
2026-05-09 05:57:25 -07:00
|
|
|
|
#include "cli_bake.hpp"
|
2026-05-09 06:13:41 -07:00
|
|
|
|
#include "cli_migrate.hpp"
|
2026-05-09 06:25:04 -07:00
|
|
|
|
#include "cli_convert_single.hpp"
|
refactor(editor): extract interop --validate-* into cli_validate_interop.cpp
Moves the four structural validators for INTEROP file formats
(--validate-stl, --validate-png, --validate-blp, --validate-jsondbc)
out of main.cpp into a new cli_validate_interop.{hpp,cpp} module.
These check files coming in/out of wowee from third-party tools,
distinct from cli_format_validate.cpp which validates the native
open formats (WOM, WOB, WOC, WHM).
main.cpp shrinks by 524 lines (10,644 to 10,121). Each validator
preserves its --json output mode for machine-readable reports.
2026-05-09 06:36:02 -07:00
|
|
|
|
#include "cli_validate_interop.hpp"
|
2026-05-09 06:46:02 -07:00
|
|
|
|
#include "cli_glb_inspect.hpp"
|
refactor(editor): extract WOM <-> OBJ/GLB/STL into cli_wom_io.cpp
Moves the four WOM interchange-format handlers (--export-obj,
--export-glb, --export-stl, --import-stl) out of main.cpp into
a new cli_wom_io.{hpp,cpp} module. WOM is our open M2
replacement; these are the bridge that lets it round-trip
through every external 3D tool — Blender, Three.js, slicers,
CAD packages — so the open format is actually useful.
main.cpp shrinks by 467 lines (9,464 to 8,997). The five WOB
and WHM exporters (--export-wob-glb, --export-whm-glb, etc.)
remain inline for a follow-up extraction.
2026-05-09 06:55:00 -07:00
|
|
|
|
#include "cli_wom_io.hpp"
|
refactor(editor): extract WOB/WHM/WOC IO into cli_world_io.cpp
Moves all six world-asset interchange handlers (--export-wob-glb,
--export-wob-obj, --import-wob-obj, --export-whm-glb,
--export-whm-obj, --export-woc-obj) out of main.cpp into a new
cli_world_io.{hpp,cpp} module. WOB / WHM / WOC are our open
replacements for proprietary WMO / ADT-heightmap / ADT-collision
data; these are the bridge that lets the open formats round-trip
through Blender, MeshLab, Three.js, and the rest of the standard
3D toolchain.
main.cpp shrinks by 858 lines (8,997 to 8,140). The single-mesh
--import-obj handler stays inline for now -- it shadow-mirrors
cli_wom_io's --import-stl semantics and will move there next.
2026-05-09 07:03:14 -07:00
|
|
|
|
#include "cli_world_io.hpp"
|
refactor(editor): extract --info-{zone,project}-tree into cli_info_tree.cpp
Moves the two tree-style content browser handlers
(--info-zone-tree, --info-project-tree) out of main.cpp into
a new cli_info_tree.{hpp,cpp} module. Both render
Unix-`tree`-style hierarchical views — one drilling into a
single zone (manifest, tiles, creatures, objects, quests,
files) and one giving a bird's-eye view of every zone in a
project with bake/viewer status.
main.cpp shrinks by 201 lines (8,140 to 7,939). The remaining
info-zone/-project-* pairs (bytes, extents, water, density,
audio) form the next natural extraction batch.
2026-05-09 07:10:12 -07:00
|
|
|
|
#include "cli_info_tree.hpp"
|
2026-05-09 07:16:27 -07:00
|
|
|
|
#include "cli_info_bytes.hpp"
|
2026-05-09 07:22:06 -07:00
|
|
|
|
#include "cli_info_extents.hpp"
|
2026-05-09 07:28:15 -07:00
|
|
|
|
#include "cli_info_water.hpp"
|
2026-05-09 07:33:40 -07:00
|
|
|
|
#include "cli_info_density.hpp"
|
2026-05-09 07:38:36 -07:00
|
|
|
|
#include "cli_info_audio.hpp"
|
2026-05-09 07:44:57 -07:00
|
|
|
|
#include "cli_world_info.hpp"
|
2026-05-09 07:56:16 -07:00
|
|
|
|
#include "cli_quest_objective.hpp"
|
2026-05-09 08:01:28 -07:00
|
|
|
|
#include "cli_quest_reward.hpp"
|
2026-05-09 08:06:20 -07:00
|
|
|
|
#include "cli_clone.hpp"
|
2026-05-09 08:11:50 -07:00
|
|
|
|
#include "cli_remove.hpp"
|
2026-05-09 08:16:52 -07:00
|
|
|
|
#include "cli_add.hpp"
|
2026-05-09 08:22:06 -07:00
|
|
|
|
#include "cli_random.hpp"
|
2026-05-09 08:26:52 -07:00
|
|
|
|
#include "cli_items_export.hpp"
|
2026-05-09 08:33:59 -07:00
|
|
|
|
#include "cli_items_mutate.hpp"
|
2026-05-09 08:42:49 -07:00
|
|
|
|
#include "cli_zone_create.hpp"
|
2026-05-09 08:47:32 -07:00
|
|
|
|
#include "cli_tiles.hpp"
|
2026-05-09 08:52:19 -07:00
|
|
|
|
#include "cli_zone_mgmt.hpp"
|
2026-05-09 08:56:47 -07:00
|
|
|
|
#include "cli_strip.hpp"
|
2026-05-09 08:59:51 -07:00
|
|
|
|
#include "cli_repair.hpp"
|
2026-05-09 09:04:44 -07:00
|
|
|
|
#include "cli_makefile.hpp"
|
2026-05-09 09:09:06 -07:00
|
|
|
|
#include "cli_zone_list.hpp"
|
2026-05-06 03:09:56 -07:00
|
|
|
|
#include "content_pack.hpp"
|
2026-05-06 07:14:42 -07:00
|
|
|
|
#include "npc_spawner.hpp"
|
2026-05-06 07:15:40 -07:00
|
|
|
|
#include "object_placer.hpp"
|
2026-05-06 07:16:27 -07:00
|
|
|
|
#include "quest_editor.hpp"
|
2026-05-06 08:17:33 -07:00
|
|
|
|
#include "wowee_terrain.hpp"
|
|
|
|
|
|
#include "zone_manifest.hpp"
|
|
|
|
|
|
#include "terrain_editor.hpp"
|
|
|
|
|
|
#include "terrain_biomes.hpp"
|
|
|
|
|
|
#include <filesystem>
|
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
|
|
|
|
#include <fstream>
|
2026-05-08 10:58:28 -07:00
|
|
|
|
#include <iomanip>
|
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
|
|
|
|
#include <sstream>
|
2026-05-05 10:50:36 -07:00
|
|
|
|
#include "pipeline/wowee_model.hpp"
|
2026-05-05 12:41:19 -07:00
|
|
|
|
#include "pipeline/wowee_building.hpp"
|
2026-05-06 03:17:10 -07:00
|
|
|
|
#include "pipeline/wowee_collision.hpp"
|
2026-05-06 06:58:34 -07:00
|
|
|
|
#include "pipeline/wowee_terrain_loader.hpp"
|
2026-05-05 12:41:19 -07:00
|
|
|
|
#include "pipeline/wmo_loader.hpp"
|
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
|
|
|
|
#include "pipeline/m2_loader.hpp"
|
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
|
|
|
|
#include "pipeline/adt_loader.hpp"
|
2026-05-05 10:50:36 -07:00
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
2026-05-05 12:18:31 -07:00
|
|
|
|
#include "pipeline/custom_zone_discovery.hpp"
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
|
#include <string>
|
2026-05-05 16:54:03 -07:00
|
|
|
|
#include <cstdio>
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
#include <cstring>
|
2026-05-06 04:36:40 -07:00
|
|
|
|
#include <unordered_map>
|
2026-05-06 13:27:18 -07:00
|
|
|
|
#include <unordered_set>
|
2026-05-06 13:24:27 -07:00
|
|
|
|
#include <map>
|
2026-05-06 13:59:54 -07:00
|
|
|
|
#include <set>
|
|
|
|
|
|
#include <cctype>
|
|
|
|
|
|
#include <cstdio>
|
2026-05-06 18:03:55 -07:00
|
|
|
|
#include <chrono>
|
2026-05-06 19:17:42 -07:00
|
|
|
|
#include <functional>
|
|
|
|
|
|
#include <memory>
|
2026-05-06 07:30:26 -07:00
|
|
|
|
#include <algorithm>
|
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
|
|
|
|
#include <nlohmann/json.hpp>
|
2026-05-06 12:38:14 -07:00
|
|
|
|
#include "stb_image_write.h"
|
2026-05-07 08:19:37 -07:00
|
|
|
|
#include "stb_image.h" // implementation in stb_image_impl.cpp
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
|
2026-05-06 11:54:54 -07:00
|
|
|
|
// ─── Open-format consistency checks ─────────────────────────────
|
|
|
|
|
|
// Both validators are called from the per-file CLI commands AND
|
|
|
|
|
|
// from --validate-all which walks a zone dir. Returning a vector
|
|
|
|
|
|
// of error strings (empty == passed) keeps callers simple.
|
2026-05-06 15:51:50 -07:00
|
|
|
|
// Minimal SHA-256 implementation (FIPS 180-4) used by --export-zone-checksum
|
|
|
|
|
|
// to produce hashes that interoperate with `sha256sum -c`. Not exposed beyond
|
|
|
|
|
|
// this file — about 90 LoC, no external deps. See RFC 6234 for the algorithm.
|
|
|
|
|
|
|
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
|
|
|
|
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
|
|
|
|
|
|
int main(int argc, char* argv[]) {
|
|
|
|
|
|
std::string dataPath;
|
|
|
|
|
|
std::string adtMap;
|
|
|
|
|
|
int adtX = -1, adtY = -1;
|
|
|
|
|
|
|
2026-05-06 08:08:05 -07:00
|
|
|
|
// Detect non-GUI options that are missing their argument and bail out
|
|
|
|
|
|
// with a helpful message instead of silently dropping into the GUI.
|
|
|
|
|
|
static const char* kArgRequired[] = {
|
2026-05-06 13:19:45 -07:00
|
|
|
|
"--data", "--info", "--info-batches", "--info-textures", "--info-doodads",
|
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
|
|
|
|
"--info-attachments", "--info-particles", "--info-sequences",
|
2026-05-08 01:01:18 -07:00
|
|
|
|
"--info-bones", "--export-bones-dot",
|
2026-05-08 02:38:55 -07:00
|
|
|
|
"--list-zone-meshes", "--list-zone-audio", "--list-zone-textures",
|
2026-05-08 06:10:18 -07:00
|
|
|
|
"--list-project-meshes", "--list-project-audio",
|
|
|
|
|
|
"--list-project-textures",
|
2026-05-06 20:25:00 -07:00
|
|
|
|
"--info-zone-models-total", "--info-project-models-total",
|
2026-05-08 19:28:18 -07:00
|
|
|
|
"--list-zone-meshes-detail", "--list-project-meshes-detail", "--info-mesh",
|
2026-05-07 09:00:47 -07:00
|
|
|
|
"--info-mesh-storage-budget",
|
2026-05-06 13:19:45 -07:00
|
|
|
|
"--info-wob", "--info-woc", "--info-wot",
|
2026-05-06 08:08:05 -07:00
|
|
|
|
"--info-creatures", "--info-objects", "--info-quests",
|
2026-05-06 18:33:29 -07:00
|
|
|
|
"--info-extract", "--info-extract-tree", "--info-extract-budget",
|
|
|
|
|
|
"--list-missing-sidecars",
|
2026-05-06 17:38:28 -07:00
|
|
|
|
"--info-png", "--info-jsondbc", "--info-blp", "--info-pack-budget",
|
2026-05-06 19:17:42 -07:00
|
|
|
|
"--info-pack-tree",
|
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
|
|
|
|
"--info-m2", "--info-wmo", "--info-adt",
|
2026-05-07 19:13:11 -07:00
|
|
|
|
"--info-zone", "--info-zone-overview", "--info-project-overview",
|
2026-05-07 19:31:09 -07:00
|
|
|
|
"--copy-project", "--info-wcp", "--list-wcp",
|
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
|
|
|
|
"--list-creatures", "--list-objects", "--list-quests",
|
2026-05-06 12:42:17 -07:00
|
|
|
|
"--list-quest-objectives", "--list-quest-rewards",
|
feat(editor): add --info-object completing the single-entity inspector trio
Symmetric to --info-creature and --info-quest. Every PlacedObject
field for one entry:
wowee_editor --info-object $Z/objects.json 3
Object [3]
type : wmo
path : World/StaticObject/Stormwind/HumanInn.wmo
nameId : 3497721408
uniqueId : 4
position : (-8412.300, 633.200, 110.500)
rotation : (0.00, 90.00, 0.00) deg
scale : 1.000
Useful when debugging a misplaced object — quickly see the exact
position/rotation/scale + uniqueId + nameId without having to
grep through objects.json or run --list-objects through awk.
JSON mode emits the structured record. Inspector lineup is now
symmetric across all three content types:
--info-{creatures,objects,quests} aggregate counts
--list-{creatures,objects,quests} per-entry table
--info-{creature,object,quest} single-entry deep dive
2026-05-06 14:22:08 -07:00
|
|
|
|
"--info-creature", "--info-quest", "--info-object",
|
2026-05-06 16:17:30 -07:00
|
|
|
|
"--info-quest-graph-stats",
|
2026-05-06 17:13:44 -07:00
|
|
|
|
"--info-creatures-by-faction", "--info-creatures-by-level",
|
2026-05-06 17:46:51 -07:00
|
|
|
|
"--info-objects-by-path", "--info-objects-by-type",
|
2026-05-06 18:20:37 -07:00
|
|
|
|
"--info-quests-by-level", "--info-quests-by-xp",
|
2026-05-06 10:50:17 -07:00
|
|
|
|
"--unpack-wcp", "--pack-wcp",
|
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
|
|
|
|
"--validate", "--validate-wom", "--validate-wob", "--validate-woc",
|
2026-05-06 17:21:57 -07:00
|
|
|
|
"--validate-whm", "--validate-all", "--validate-project",
|
2026-05-07 21:40:49 -07:00
|
|
|
|
"--validate-project-open-only", "--audit-project", "--bench-audit-project",
|
2026-05-06 18:50:20 -07:00
|
|
|
|
"--bench-validate-project", "--bench-bake-project",
|
2026-05-07 00:33:34 -07:00
|
|
|
|
"--bench-migrate-data-tree", "--list-data-tree-largest",
|
2026-05-07 02:32:43 -07:00
|
|
|
|
"--export-data-tree-md", "--gen-texture", "--gen-mesh", "--gen-mesh-textured",
|
2026-05-07 04:00:21 -07:00
|
|
|
|
"--add-texture-to-mesh", "--add-texture-to-zone",
|
2026-05-07 17:27:33 -07:00
|
|
|
|
"--gen-mesh-stairs", "--gen-mesh-grid", "--gen-mesh-disc",
|
2026-05-07 19:50:00 -07:00
|
|
|
|
"--gen-mesh-tube", "--gen-mesh-capsule", "--gen-mesh-arch",
|
2026-05-07 22:19:51 -07:00
|
|
|
|
"--gen-mesh-pyramid", "--gen-mesh-fence", "--gen-mesh-tree",
|
2026-05-08 03:29:03 -07:00
|
|
|
|
"--gen-mesh-rock", "--gen-mesh-pillar", "--gen-mesh-bridge",
|
2026-05-08 09:29:02 -07:00
|
|
|
|
"--gen-mesh-tower", "--gen-mesh-house", "--gen-mesh-fountain",
|
2026-05-08 14:42:56 -07:00
|
|
|
|
"--gen-mesh-statue", "--gen-mesh-altar", "--gen-mesh-portal",
|
2026-05-08 21:19:51 -07:00
|
|
|
|
"--gen-mesh-archway", "--gen-mesh-barrel", "--gen-mesh-chest",
|
2026-05-09 00:52:13 -07:00
|
|
|
|
"--gen-mesh-anvil", "--gen-mesh-mushroom", "--gen-mesh-cart",
|
2026-05-09 03:43:11 -07:00
|
|
|
|
"--gen-mesh-banner", "--gen-mesh-grave", "--gen-mesh-bench",
|
2026-05-09 05:25:39 -07:00
|
|
|
|
"--gen-mesh-shrine", "--gen-mesh-totem", "--gen-mesh-cage",
|
2026-05-09 06:39:39 -07:00
|
|
|
|
"--gen-mesh-throne", "--gen-mesh-coffin", "--gen-mesh-bookshelf",
|
2026-05-09 07:25:11 -07:00
|
|
|
|
"--gen-mesh-table", "--gen-mesh-lamppost", "--gen-mesh-bed",
|
2026-05-09 07:58:45 -07:00
|
|
|
|
"--gen-mesh-ladder", "--gen-mesh-well", "--gen-mesh-signpost",
|
2026-05-09 08:28:53 -07:00
|
|
|
|
"--gen-mesh-mailbox", "--gen-mesh-tombstone", "--gen-mesh-crate",
|
2026-05-09 09:02:04 -07:00
|
|
|
|
"--gen-mesh-stool", "--gen-mesh-cauldron", "--gen-mesh-gate",
|
2026-05-09 09:11:36 -07:00
|
|
|
|
"--gen-mesh-beehive",
|
2026-05-07 17:27:33 -07:00
|
|
|
|
"--gen-texture-gradient",
|
2026-05-07 08:46:50 -07:00
|
|
|
|
"--gen-mesh-from-heightmap", "--export-mesh-heightmap",
|
2026-05-07 16:53:36 -07:00
|
|
|
|
"--displace-mesh",
|
2026-05-07 06:09:46 -07:00
|
|
|
|
"--scale-mesh", "--translate-mesh", "--strip-mesh",
|
feat(editor): add --gen-texture-noise-color two-color noise blends
Same value-noise function as --gen-texture-noise but interpolated
between two RGB endpoints rather than emitted as grayscale.
Useful for terrain detail (grass+dirt mottle), magic fog, marble
veining, or any "natural variation" pass that shouldn't be
desaturated.
Args: <out.png> <colorAHex> <colorBHex> [seed] [W H]
Defaults: seed 1, 256x256.
11th procedural texture pattern (joining solid, BW checker, color
checker, grid, vertical/horizontal/radial gradients, grayscale
noise, stripes, dots, rings).
2026-05-07 21:22:33 -07:00
|
|
|
|
"--gen-texture-noise", "--gen-texture-noise-color", "--rotate-mesh",
|
feat(editor): add --mirror-mesh for axis mirroring with winding flip
Mirrors every vertex position and normal across the chosen axis
(x, y, or z). Negating just one position component reverses face
winding (the triangle's signed area flips), so the second and
third index of every triangle is also swapped to keep front-faces
forward and lighting correct. Bone pivots mirror too.
Useful for "I have a left arm, mirror it for the right arm" content
reuse — the open-format mesh ecosystem can build symmetric content
from one half-asset without artist round-trips.
Verified: cube translated to (+5,0,0) mirrored across X → bounds
correctly land at (-5.5,-0.5,-0.5)..(-4.5,0.5,0.5); 24 vertices
touched / 12 triangles flipped reported; bad axis 'w' rejected
exit 1. Brings command count to 225.
2026-05-07 07:39:05 -07:00
|
|
|
|
"--center-mesh", "--flip-mesh-normals", "--mirror-mesh",
|
2026-05-07 08:33:20 -07:00
|
|
|
|
"--smooth-mesh-normals",
|
2026-05-07 07:52:17 -07:00
|
|
|
|
"--merge-meshes",
|
feat(editor): add --gen-texture-dots polka-dot pattern
Solid background with circular dots arranged on a regular grid.
Useful for fabric/clothing textures, game-board patterns, mushroom
caps, and decorative tiling.
Args: <out.png> <bgHex> <dotHex> [radius] [spacing] [W H]
Defaults: radius 8, spacing 32, 256x256.
Adds the 8th procedural texture pattern (joining solid, checker,
grid, vertical/horizontal/radial gradients, noise, stripes).
Verified: 128x128 white-bg / red-dot at radius 12 + spacing 48
writes successfully.
2026-05-07 16:03:51 -07:00
|
|
|
|
"--gen-texture-radial", "--gen-texture-stripes", "--gen-texture-dots",
|
2026-05-07 22:39:07 -07:00
|
|
|
|
"--gen-texture-rings", "--gen-texture-checker", "--gen-texture-brick",
|
2026-05-08 04:47:51 -07:00
|
|
|
|
"--gen-texture-wood", "--gen-texture-grass", "--gen-texture-fabric",
|
2026-05-08 12:31:54 -07:00
|
|
|
|
"--gen-texture-cobble", "--gen-texture-marble", "--gen-texture-metal",
|
2026-05-08 19:07:41 -07:00
|
|
|
|
"--gen-texture-leather", "--gen-texture-sand", "--gen-texture-snow",
|
2026-05-08 23:15:03 -07:00
|
|
|
|
"--gen-texture-lava", "--gen-texture-tile", "--gen-texture-bark",
|
2026-05-09 02:37:06 -07:00
|
|
|
|
"--gen-texture-clouds", "--gen-texture-stars", "--gen-texture-vines",
|
2026-05-09 04:42:31 -07:00
|
|
|
|
"--gen-texture-mosaic", "--gen-texture-rust", "--gen-texture-circuit",
|
2026-05-09 06:02:13 -07:00
|
|
|
|
"--gen-texture-coral", "--gen-texture-flame", "--gen-texture-tartan",
|
2026-05-09 06:49:29 -07:00
|
|
|
|
"--gen-texture-argyle", "--gen-texture-herringbone",
|
2026-05-09 07:19:02 -07:00
|
|
|
|
"--gen-texture-scales", "--gen-texture-stained-glass",
|
2026-05-09 07:41:17 -07:00
|
|
|
|
"--gen-texture-shingles", "--gen-texture-frost",
|
2026-05-09 08:03:54 -07:00
|
|
|
|
"--gen-texture-parquet", "--gen-texture-bubbles",
|
2026-05-09 08:24:12 -07:00
|
|
|
|
"--gen-texture-spider-web", "--gen-texture-gingham",
|
2026-05-09 08:49:51 -07:00
|
|
|
|
"--gen-texture-lattice", "--gen-texture-honeycomb",
|
2026-05-09 09:06:56 -07:00
|
|
|
|
"--gen-texture-cracked",
|
2026-05-06 18:41:55 -07:00
|
|
|
|
"--validate-glb", "--info-glb", "--info-glb-tree", "--info-glb-bytes",
|
2026-05-06 14:10:07 -07:00
|
|
|
|
"--validate-jsondbc", "--check-glb-bounds", "--validate-stl",
|
2026-05-06 15:07:46 -07:00
|
|
|
|
"--validate-png", "--validate-blp",
|
feat(editor): add --info-project-tree for multi-zone project overview
Project-level tree view: every zone with quick counts + bake/viewer
artifact status. --info-zone-tree drills into one zone; this gives
the bird's-eye view across the whole project:
wowee_editor --info-project-tree custom_zones
custom_zones/ (2 zones, 2 tiles, 1 creatures, 1 objects, 1 quests)
├─ Empty/ (tiles=1, creat=0, obj=0, quest=0)
│ ├─ name : Empty
│ ├─ mapName : Empty
│ ├─ artifacts : (none)
│ └─ status : empty (only terrain)
└─ Forest/ (tiles=1, creat=1, obj=1, quest=1)
├─ name : Forest
├─ mapName : Forest
├─ artifacts : .glb
└─ status : populated
Per-zone summary line shows counts; sub-tree shows display name,
map slug, which derived artifacts have been baked (.glb / .obj /
.stl / .html / ZONE.md), and a populated/empty status.
Project-header line aggregates totals across all zones for the
'how big is my project?' answer in one glance.
Pairs with --info-tilemap (spatial coverage) and --zone-stats
(quantitative aggregate) — three different lenses on the project:
spatial, quantitative, and structural.
2026-05-06 16:57:26 -07:00
|
|
|
|
"--zone-summary", "--info-zone-tree", "--info-project-tree",
|
2026-05-06 20:44:43 -07:00
|
|
|
|
"--info-zone-bytes", "--info-project-bytes",
|
2026-05-06 21:55:27 -07:00
|
|
|
|
"--info-zone-extents", "--info-project-extents",
|
2026-05-06 22:06:22 -07:00
|
|
|
|
"--info-zone-water", "--info-project-water",
|
2026-05-06 22:17:39 -07:00
|
|
|
|
"--info-zone-density", "--info-project-density",
|
2026-05-06 13:29:23 -07:00
|
|
|
|
"--export-zone-summary-md", "--export-quest-graph",
|
2026-05-06 15:04:43 -07:00
|
|
|
|
"--export-zone-csv", "--export-zone-html", "--export-project-html",
|
2026-05-06 20:15:45 -07:00
|
|
|
|
"--export-project-md", "--export-zone-checksum", "--export-project-checksum",
|
2026-05-06 20:34:45 -07:00
|
|
|
|
"--validate-project-checksum",
|
2026-05-06 16:25:23 -07:00
|
|
|
|
"--scaffold-zone", "--mvp-zone", "--add-tile", "--remove-tile", "--list-tiles",
|
2026-05-06 18:11:54 -07:00
|
|
|
|
"--for-each-zone", "--for-each-tile", "--zone-stats", "--info-tilemap",
|
2026-05-06 22:42:29 -07:00
|
|
|
|
"--list-zone-deps", "--list-project-orphans", "--remove-project-orphans",
|
2026-05-06 22:30:10 -07:00
|
|
|
|
"--check-zone-refs", "--check-zone-content",
|
2026-05-06 19:44:28 -07:00
|
|
|
|
"--check-project-content", "--check-project-refs",
|
2026-05-06 18:59:44 -07:00
|
|
|
|
"--export-zone-deps-md", "--export-zone-spawn-png",
|
feat(editor): add --add-item, introducing zone items.json content type
Introduces a new per-zone content file alongside creatures.json /
objects.json / quests.json. Schema: {"items": [{id, name, quality,
displayId, itemLevel, stackable}, ...]}. Inline JSON manipulation
via nlohmann::json — items are simple records and don't yet need
NpcSpawner-style infrastructure.
ID assignment: pass 0 (or omit) to auto-pick the smallest unused
positive integer so numbering stays contiguous. Explicit IDs are
honored. Duplicate IDs rejected with exit 1 so collisions are
visible.
Quality is 0..6 (poor/common/uncommon/rare/epic/legendary/artifact)
— summary line maps the number to the human name so users see what
they wrote.
Verified: auto-id sequential (1, 2) → explicit id (99) honored →
duplicate id rejected with exit 1; JSON schema stable, quality
labels correct, file auto-created on first call. Brings command
count to 196.
2026-05-07 01:33:58 -07:00
|
|
|
|
"--add-creature", "--add-object", "--add-quest", "--add-item",
|
2026-05-07 11:57:55 -07:00
|
|
|
|
"--random-populate-zone", "--random-populate-items",
|
2026-05-07 13:57:25 -07:00
|
|
|
|
"--info-zone-audio", "--snap-zone-to-ground", "--audit-zone-spawns",
|
2026-05-07 13:42:19 -07:00
|
|
|
|
"--info-project-audio", "--snap-project-to-ground",
|
2026-05-07 15:15:20 -07:00
|
|
|
|
"--audit-project-spawns", "--list-zone-spawns", "--list-project-spawns",
|
2026-05-07 23:18:00 -07:00
|
|
|
|
"--gen-random-zone", "--gen-random-project", "--gen-zone-texture-pack",
|
2026-05-08 07:05:17 -07:00
|
|
|
|
"--gen-zone-mesh-pack", "--gen-zone-starter-pack",
|
|
|
|
|
|
"--gen-project-starter-pack", "--gen-audio-tone",
|
2026-05-08 06:37:50 -07:00
|
|
|
|
"--gen-audio-noise", "--gen-audio-sweep", "--gen-zone-audio-pack",
|
2026-05-08 15:16:00 -07:00
|
|
|
|
"--info-zone-summary", "--info-project-summary",
|
|
|
|
|
|
"--info-zone-deps", "--info-project-deps",
|
2026-05-08 12:00:28 -07:00
|
|
|
|
"--gen-zone-readme", "--gen-project-readme",
|
2026-05-08 09:58:29 -07:00
|
|
|
|
"--validate-zone-pack", "--validate-project-packs", "--info-spawn",
|
2026-05-07 21:04:14 -07:00
|
|
|
|
"--diff-zone-spawns",
|
2026-05-07 04:45:25 -07:00
|
|
|
|
"--list-items", "--info-item", "--set-item", "--export-zone-items-md",
|
2026-05-07 07:26:00 -07:00
|
|
|
|
"--export-project-items-md", "--export-project-items-csv",
|
2026-05-06 12:18:20 -07:00
|
|
|
|
"--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward",
|
2026-05-06 12:57:31 -07:00
|
|
|
|
"--remove-quest-objective", "--clone-quest", "--clone-creature",
|
2026-05-07 04:57:03 -07:00
|
|
|
|
"--clone-item", "--validate-items", "--validate-project-items",
|
|
|
|
|
|
"--info-project-items",
|
2026-05-06 13:10:25 -07:00
|
|
|
|
"--clone-object",
|
2026-05-07 01:52:43 -07:00
|
|
|
|
"--remove-creature", "--remove-object", "--remove-quest", "--remove-item",
|
2026-05-07 07:12:55 -07:00
|
|
|
|
"--copy-zone-items",
|
2026-05-06 19:26:10 -07:00
|
|
|
|
"--copy-zone", "--rename-zone", "--remove-zone",
|
2026-05-06 21:04:32 -07:00
|
|
|
|
"--clear-zone-content", "--strip-zone", "--strip-project",
|
2026-05-06 21:44:47 -07:00
|
|
|
|
"--repair-zone", "--repair-project",
|
|
|
|
|
|
"--gen-makefile", "--gen-project-makefile",
|
2026-05-06 11:35:07 -07:00
|
|
|
|
"--build-woc", "--regen-collision", "--fix-zone",
|
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
|
|
|
|
"--export-png", "--export-obj", "--import-obj",
|
|
|
|
|
|
"--export-wob-obj", "--import-wob-obj",
|
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
|
|
|
|
"--export-woc-obj", "--export-whm-obj",
|
2026-05-06 13:17:37 -07:00
|
|
|
|
"--export-glb", "--export-wob-glb", "--export-whm-glb",
|
2026-05-06 14:07:22 -07:00
|
|
|
|
"--export-stl", "--import-stl",
|
|
|
|
|
|
"--bake-zone-glb", "--bake-zone-stl", "--bake-zone-obj",
|
2026-05-06 16:08:44 -07:00
|
|
|
|
"--bake-project-obj", "--bake-project-stl", "--bake-project-glb",
|
2026-05-06 22:55:39 -07:00
|
|
|
|
"--convert-m2", "--convert-m2-batch",
|
2026-05-06 23:07:34 -07:00
|
|
|
|
"--convert-wmo", "--convert-wmo-batch",
|
2026-05-06 23:31:38 -07:00
|
|
|
|
"--convert-dbc-json", "--convert-dbc-batch", "--convert-json-dbc",
|
2026-05-06 23:19:38 -07:00
|
|
|
|
"--convert-blp-png", "--convert-blp-batch",
|
2026-05-06 20:05:51 -07:00
|
|
|
|
"--migrate-wom", "--migrate-zone", "--migrate-project",
|
2026-05-07 00:05:54 -07:00
|
|
|
|
"--migrate-data-tree", "--info-data-tree", "--strip-data-tree",
|
2026-05-07 00:15:02 -07:00
|
|
|
|
"--audit-data-tree",
|
2026-05-06 20:05:51 -07:00
|
|
|
|
"--migrate-jsondbc",
|
2026-05-06 08:08:05 -07:00
|
|
|
|
};
|
|
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
|
|
|
|
for (const char* opt : kArgRequired) {
|
|
|
|
|
|
if (std::strcmp(argv[i], opt) == 0 && i + 1 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr, "%s requires an argument\n", opt);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (std::strcmp(argv[i], "--adt") == 0 && i + 3 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr, "--adt requires <map> <x> <y>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:10:22 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-zone") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-zone requires <zoneA> <zoneB>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 13:57:25 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-glb") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-glb requires <a.glb> <b.glb>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 14:13:07 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-wom") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-wom requires <a-base> <b-base>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 14:25:40 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-wob") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-wob requires <a-base> <b-base>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
feat(editor): add --diff-whm and --diff-woc completing the open-format diff suite
Last two missing entries in the diff family — terrain heightmap pairs
and collision meshes:
wowee_editor --diff-whm Z1/Z1_30_30 Z2/Z2_31_30
Diff: ...
a b
tile : ( 30, 30) ( 31, 30) DIFF
loadedChunks : 256 256
doodadPlace : 0 0
wmoPlace : 0 0
heightRange : [-1.50,1.50] [-1.50,1.50]
wowee_editor --diff-woc tile_a.woc tile_b.woc
Diff: ...
a b
tile : ( 30, 30) ( 31, 30) DIFF
triangles : 32768 32768
walkable : 32768 32768
steep : 0 0
--diff-whm: tile coord, loaded chunk count, doodad/WMO placement
counts, texture/name table sizes, height range (min/max with float
epsilon). Pointwise height compare intentionally not done — float
perturbation from format round-trips would false-flag.
--diff-woc: tile coord, total triangles, walkable + steep counts.
Catches whether a --regen-collision pass actually changed something.
Diff family is now exhaustively complete for every shippable open
format:
--diff-wcp archive vs archive
--diff-zone unpacked zone dir vs zone dir
--diff-glb glTF binary vs glTF binary
--diff-wom WOM model vs WOM model
--diff-wob WOB building vs WOB building
--diff-whm WHM/WOT terrain pair vs pair
--diff-woc WOC collision vs collision
Verified: tile (30,30) vs (31,30) reports tile DIFF + identical
counts (since both are flat scaffolds); same-vs-self reports
IDENTICAL with exit 0.
2026-05-06 14:35:03 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-whm") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-whm requires <a-base> <b-base>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (std::strcmp(argv[i], "--diff-woc") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-woc requires <a.woc> <b.woc>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 14:41:32 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-jsondbc") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-jsondbc requires <a.json> <b.json>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 15:25:18 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-extract") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-extract requires <dirA> <dirB>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 16:32:55 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-checksum") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--diff-checksum requires <a.sha256> <b.sha256>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 08:29:21 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--diff-wcp") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr, "--diff-wcp requires two paths\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 11:35:07 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--add-creature") == 0 && i + 5 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--add-creature requires <zoneDir> <name> <x> <y> <z>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 11:37:10 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--add-object") == 0 && i + 6 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--add-object requires <zoneDir> <m2|wmo> <gamePath> <x> <y> <z>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 11:41:11 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--add-quest") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--add-quest requires <zoneDir> <title>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:12:06 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--add-quest-objective") == 0 && i + 4 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--add-quest-objective requires <zoneDir> <questIdx> <type> <targetName>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:35:10 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--remove-quest-objective") == 0 && i + 3 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--remove-quest-objective requires <zoneDir> <questIdx> <objIdx>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:53:23 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--clone-quest") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--clone-quest requires <zoneDir> <questIdx>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:57:31 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--clone-creature") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--clone-creature requires <zoneDir> <idx>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 13:10:25 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--clone-object") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--clone-object requires <zoneDir> <idx>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:18:20 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--add-quest-reward-item") == 0 && i + 3 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--add-quest-reward-item requires <zoneDir> <questIdx> <itemPath>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (std::strcmp(argv[i], "--set-quest-reward") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--set-quest-reward requires <zoneDir> <questIdx> [--xp N] [--gold N] [--silver N] [--copper N]\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:33:32 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--add-tile") == 0 && i + 3 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--add-tile requires <zoneDir> <tx> <ty>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:40:19 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--remove-tile") == 0 && i + 3 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--remove-tile requires <zoneDir> <tx> <ty>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
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
|
|
|
|
if (std::strcmp(argv[i], "--copy-zone") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--copy-zone requires <srcDir> <newName>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:24:36 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--rename-zone") == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"--rename-zone requires <srcDir> <newName>\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-05-06 12:01:52 -07:00
|
|
|
|
for (const char* opt : {"--remove-creature", "--remove-object",
|
|
|
|
|
|
"--remove-quest"}) {
|
|
|
|
|
|
if (std::strcmp(argv[i], opt) == 0 && i + 2 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr, "%s requires <zoneDir> <index>\n", opt);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-06 08:08:05 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
for (int i = 1; i < argc; i++) {
|
refactor(editor): extract gen-audio-* handlers into cli_gen_audio.cpp
main.cpp had grown past 28k lines, with each new procedural-
generation command adding 100-200 lines to the inline if/else
dispatch chain. This commit starts breaking that up by moving
the four audio-related handlers (--gen-audio-tone, -noise,
-sweep, --gen-zone-audio-pack) into their own translation unit.
Pattern established here for future family extractions:
- Family lives in cli_<family>.{hpp,cpp}
- Single dispatch entry point: bool handle<Family>(int& i, int argc,
char** argv, int& outRc) — true if matched (writes outRc), false
to fall through.
- main.cpp's argv loop calls each family's dispatcher first and
returns its outRc on match, before the legacy in-line chain.
Side-benefit: consolidated the duplicated 25-line WAV header
writer + 5ms attack/release envelope into shared helpers
(writeWavMono16, applyEdgeEnvelope) at the top of the new file.
main.cpp drops from 28,943 → 28,329 lines (-614). Audio family
is fully self-contained (~440 lines), behavior unchanged
(verified by re-running tone/noise/sweep + zone-audio-pack).
2026-05-08 16:19:30 -07:00
|
|
|
|
// Modular handler families: extracted from the in-line if/else
|
|
|
|
|
|
// chain below to keep main.cpp from sprawling further. Each
|
|
|
|
|
|
// family lives in its own .cpp; if it matches argv[i] it
|
|
|
|
|
|
// sets outRc and we exit. Otherwise fall through to the
|
|
|
|
|
|
// legacy in-line dispatch.
|
|
|
|
|
|
{
|
|
|
|
|
|
int outRc = 0;
|
|
|
|
|
|
if (wowee::editor::cli::handleGenAudio(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-08 16:46:14 -07:00
|
|
|
|
if (wowee::editor::cli::handleZonePacks(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-08 17:12:10 -07:00
|
|
|
|
if (wowee::editor::cli::handleAudits(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-08 17:36:10 -07:00
|
|
|
|
if (wowee::editor::cli::handleReadmes(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-08 18:24:01 -07:00
|
|
|
|
if (wowee::editor::cli::handleZoneInventory(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-08 18:47:06 -07:00
|
|
|
|
if (wowee::editor::cli::handleProjectInventory(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-08 20:59:02 -07:00
|
|
|
|
if (wowee::editor::cli::handleGenTexture(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
refactor(editor): extract 12 composite mesh primitives into cli_gen_mesh.cpp
Moves the recently-added composite-prop mesh handlers (rock,
pillar, bridge, tower, house, fountain, statue, altar, portal,
archway, barrel, chest) into their own translation unit. These
12 handlers were the most contiguous block of similar-shaped
mesh code in main.cpp.
Other mesh handlers (--gen-mesh dispatcher, fence, tree, grid,
stairs, disc, tube, capsule, arch, pyramid, from-heightmap,
textured) still live in main.cpp and may be migrated in
subsequent batches.
main.cpp drops 24,270 → 22,681 lines (-1,589). Behavior
verified by re-running rock/chest/archway/fountain.
2026-05-08 22:19:41 -07:00
|
|
|
|
if (wowee::editor::cli::handleGenMesh(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 00:04:27 -07:00
|
|
|
|
if (wowee::editor::cli::handleMeshIO(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 00:36:51 -07:00
|
|
|
|
if (wowee::editor::cli::handleMeshEdit(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 01:18:09 -07:00
|
|
|
|
if (wowee::editor::cli::handleWomInfo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 01:57:37 -07:00
|
|
|
|
if (wowee::editor::cli::handleFormatValidate(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 02:25:05 -07:00
|
|
|
|
if (wowee::editor::cli::handleConvert(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 02:48:58 -07:00
|
|
|
|
if (wowee::editor::cli::handleFormatInfo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 03:12:09 -07:00
|
|
|
|
if (wowee::editor::cli::handlePack(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 03:33:40 -07:00
|
|
|
|
if (wowee::editor::cli::handleContentInfo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 03:52:44 -07:00
|
|
|
|
if (wowee::editor::cli::handleZoneInfo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 04:14:32 -07:00
|
|
|
|
if (wowee::editor::cli::handleDataTree(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 04:35:08 -07:00
|
|
|
|
if (wowee::editor::cli::handleDiff(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 05:05:22 -07:00
|
|
|
|
if (wowee::editor::cli::handleSpawnAudit(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 05:19:04 -07:00
|
|
|
|
if (wowee::editor::cli::handleItems(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 05:32:27 -07:00
|
|
|
|
if (wowee::editor::cli::handleExtractInfo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 05:45:00 -07:00
|
|
|
|
if (wowee::editor::cli::handleExport(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 05:57:25 -07:00
|
|
|
|
if (wowee::editor::cli::handleBake(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 06:13:41 -07:00
|
|
|
|
if (wowee::editor::cli::handleMigrate(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 06:25:04 -07:00
|
|
|
|
if (wowee::editor::cli::handleConvertSingle(i, argc, argv,
|
|
|
|
|
|
dataPath, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
refactor(editor): extract interop --validate-* into cli_validate_interop.cpp
Moves the four structural validators for INTEROP file formats
(--validate-stl, --validate-png, --validate-blp, --validate-jsondbc)
out of main.cpp into a new cli_validate_interop.{hpp,cpp} module.
These check files coming in/out of wowee from third-party tools,
distinct from cli_format_validate.cpp which validates the native
open formats (WOM, WOB, WOC, WHM).
main.cpp shrinks by 524 lines (10,644 to 10,121). Each validator
preserves its --json output mode for machine-readable reports.
2026-05-09 06:36:02 -07:00
|
|
|
|
if (wowee::editor::cli::handleValidateInterop(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 06:46:02 -07:00
|
|
|
|
if (wowee::editor::cli::handleGlbInspect(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
refactor(editor): extract WOM <-> OBJ/GLB/STL into cli_wom_io.cpp
Moves the four WOM interchange-format handlers (--export-obj,
--export-glb, --export-stl, --import-stl) out of main.cpp into
a new cli_wom_io.{hpp,cpp} module. WOM is our open M2
replacement; these are the bridge that lets it round-trip
through every external 3D tool — Blender, Three.js, slicers,
CAD packages — so the open format is actually useful.
main.cpp shrinks by 467 lines (9,464 to 8,997). The five WOB
and WHM exporters (--export-wob-glb, --export-whm-glb, etc.)
remain inline for a follow-up extraction.
2026-05-09 06:55:00 -07:00
|
|
|
|
if (wowee::editor::cli::handleWomIo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
refactor(editor): extract WOB/WHM/WOC IO into cli_world_io.cpp
Moves all six world-asset interchange handlers (--export-wob-glb,
--export-wob-obj, --import-wob-obj, --export-whm-glb,
--export-whm-obj, --export-woc-obj) out of main.cpp into a new
cli_world_io.{hpp,cpp} module. WOB / WHM / WOC are our open
replacements for proprietary WMO / ADT-heightmap / ADT-collision
data; these are the bridge that lets the open formats round-trip
through Blender, MeshLab, Three.js, and the rest of the standard
3D toolchain.
main.cpp shrinks by 858 lines (8,997 to 8,140). The single-mesh
--import-obj handler stays inline for now -- it shadow-mirrors
cli_wom_io's --import-stl semantics and will move there next.
2026-05-09 07:03:14 -07:00
|
|
|
|
if (wowee::editor::cli::handleWorldIo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
refactor(editor): extract --info-{zone,project}-tree into cli_info_tree.cpp
Moves the two tree-style content browser handlers
(--info-zone-tree, --info-project-tree) out of main.cpp into
a new cli_info_tree.{hpp,cpp} module. Both render
Unix-`tree`-style hierarchical views — one drilling into a
single zone (manifest, tiles, creatures, objects, quests,
files) and one giving a bird's-eye view of every zone in a
project with bake/viewer status.
main.cpp shrinks by 201 lines (8,140 to 7,939). The remaining
info-zone/-project-* pairs (bytes, extents, water, density,
audio) form the next natural extraction batch.
2026-05-09 07:10:12 -07:00
|
|
|
|
if (wowee::editor::cli::handleInfoTree(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 07:16:27 -07:00
|
|
|
|
if (wowee::editor::cli::handleInfoBytes(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 07:22:06 -07:00
|
|
|
|
if (wowee::editor::cli::handleInfoExtents(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 07:28:15 -07:00
|
|
|
|
if (wowee::editor::cli::handleInfoWater(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 07:33:40 -07:00
|
|
|
|
if (wowee::editor::cli::handleInfoDensity(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 07:38:36 -07:00
|
|
|
|
if (wowee::editor::cli::handleInfoAudio(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 07:44:57 -07:00
|
|
|
|
if (wowee::editor::cli::handleWorldInfo(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 07:56:16 -07:00
|
|
|
|
if (wowee::editor::cli::handleQuestObjective(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:01:28 -07:00
|
|
|
|
if (wowee::editor::cli::handleQuestReward(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:06:20 -07:00
|
|
|
|
if (wowee::editor::cli::handleClone(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:11:50 -07:00
|
|
|
|
if (wowee::editor::cli::handleRemove(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:16:52 -07:00
|
|
|
|
if (wowee::editor::cli::handleAdd(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:22:06 -07:00
|
|
|
|
if (wowee::editor::cli::handleRandom(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:26:52 -07:00
|
|
|
|
if (wowee::editor::cli::handleItemsExport(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:33:59 -07:00
|
|
|
|
if (wowee::editor::cli::handleItemsMutate(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:42:49 -07:00
|
|
|
|
if (wowee::editor::cli::handleZoneCreate(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:47:32 -07:00
|
|
|
|
if (wowee::editor::cli::handleTiles(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:52:19 -07:00
|
|
|
|
if (wowee::editor::cli::handleZoneMgmt(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:56:47 -07:00
|
|
|
|
if (wowee::editor::cli::handleStrip(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 08:59:51 -07:00
|
|
|
|
if (wowee::editor::cli::handleRepair(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 09:04:44 -07:00
|
|
|
|
if (wowee::editor::cli::handleMakefile(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
2026-05-09 09:09:06 -07:00
|
|
|
|
if (wowee::editor::cli::handleZoneList(i, argc, argv, outRc)) {
|
|
|
|
|
|
return outRc;
|
|
|
|
|
|
}
|
refactor(editor): extract gen-audio-* handlers into cli_gen_audio.cpp
main.cpp had grown past 28k lines, with each new procedural-
generation command adding 100-200 lines to the inline if/else
dispatch chain. This commit starts breaking that up by moving
the four audio-related handlers (--gen-audio-tone, -noise,
-sweep, --gen-zone-audio-pack) into their own translation unit.
Pattern established here for future family extractions:
- Family lives in cli_<family>.{hpp,cpp}
- Single dispatch entry point: bool handle<Family>(int& i, int argc,
char** argv, int& outRc) — true if matched (writes outRc), false
to fall through.
- main.cpp's argv loop calls each family's dispatcher first and
returns its outRc on match, before the legacy in-line chain.
Side-benefit: consolidated the duplicated 25-line WAV header
writer + 5ms attack/release envelope into shared helpers
(writeWavMono16, applyEdgeEnvelope) at the top of the new file.
main.cpp drops from 28,943 → 28,329 lines (-614). Audio family
is fully self-contained (~440 lines), behavior unchanged
(verified by re-running tone/noise/sweep + zone-audio-pack).
2026-05-08 16:19:30 -07:00
|
|
|
|
}
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
|
|
|
|
|
|
dataPath = argv[++i];
|
|
|
|
|
|
} else if (std::strcmp(argv[i], "--adt") == 0 && i + 3 < argc) {
|
|
|
|
|
|
adtMap = argv[++i];
|
|
|
|
|
|
adtX = std::atoi(argv[++i]);
|
|
|
|
|
|
adtY = std::atoi(argv[++i]);
|
2026-05-06 19:53:31 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-zone-models-total") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Aggregate WOM/WOB stats across every model in a zone.
|
|
|
|
|
|
// Useful for capacity planning ('how many bones across all
|
|
|
|
|
|
// my creatures?') and perf budgeting ('total triangles
|
|
|
|
|
|
// per frame if all loaded?').
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"info-zone-models-total: %s does not exist\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
int womCount = 0, wobCount = 0;
|
|
|
|
|
|
uint64_t womVerts = 0, womIndices = 0;
|
|
|
|
|
|
uint64_t womBones = 0, womAnims = 0, womBatches = 0;
|
|
|
|
|
|
uint64_t wobGroups = 0, wobVerts = 0, wobIndices = 0;
|
|
|
|
|
|
uint64_t wobDoodads = 0, wobPortals = 0;
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
std::string ext = e.path().extension().string();
|
|
|
|
|
|
std::string base = e.path().string();
|
|
|
|
|
|
if (base.size() > ext.size())
|
|
|
|
|
|
base = base.substr(0, base.size() - ext.size());
|
|
|
|
|
|
if (ext == ".wom") {
|
|
|
|
|
|
womCount++;
|
|
|
|
|
|
auto wom = wowee::pipeline::WoweeModelLoader::load(base);
|
|
|
|
|
|
womVerts += wom.vertices.size();
|
|
|
|
|
|
womIndices += wom.indices.size();
|
|
|
|
|
|
womBones += wom.bones.size();
|
|
|
|
|
|
womAnims += wom.animations.size();
|
|
|
|
|
|
womBatches += wom.batches.size();
|
|
|
|
|
|
} else if (ext == ".wob") {
|
|
|
|
|
|
wobCount++;
|
|
|
|
|
|
auto wob = wowee::pipeline::WoweeBuildingLoader::load(base);
|
|
|
|
|
|
wobGroups += wob.groups.size();
|
|
|
|
|
|
for (const auto& g : wob.groups) {
|
|
|
|
|
|
wobVerts += g.vertices.size();
|
|
|
|
|
|
wobIndices += g.indices.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
wobDoodads += wob.doodads.size();
|
|
|
|
|
|
wobPortals += wob.portals.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["zone"] = zoneDir;
|
|
|
|
|
|
j["wom"] = {{"count", womCount},
|
|
|
|
|
|
{"vertices", womVerts},
|
|
|
|
|
|
{"indices", womIndices},
|
|
|
|
|
|
{"triangles", womIndices / 3},
|
|
|
|
|
|
{"bones", womBones},
|
|
|
|
|
|
{"animations", womAnims},
|
|
|
|
|
|
{"batches", womBatches}};
|
|
|
|
|
|
j["wob"] = {{"count", wobCount},
|
|
|
|
|
|
{"groups", wobGroups},
|
|
|
|
|
|
{"vertices", wobVerts},
|
|
|
|
|
|
{"indices", wobIndices},
|
|
|
|
|
|
{"triangles", wobIndices / 3},
|
|
|
|
|
|
{"doodads", wobDoodads},
|
|
|
|
|
|
{"portals", wobPortals}};
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Zone models total: %s\n", zoneDir.c_str());
|
|
|
|
|
|
std::printf("\n WOM (open M2):\n");
|
|
|
|
|
|
std::printf(" files : %d\n", womCount);
|
|
|
|
|
|
std::printf(" vertices : %llu\n", static_cast<unsigned long long>(womVerts));
|
|
|
|
|
|
std::printf(" triangles : %llu\n", static_cast<unsigned long long>(womIndices / 3));
|
|
|
|
|
|
std::printf(" bones : %llu\n", static_cast<unsigned long long>(womBones));
|
|
|
|
|
|
std::printf(" anims : %llu\n", static_cast<unsigned long long>(womAnims));
|
|
|
|
|
|
std::printf(" batches : %llu\n", static_cast<unsigned long long>(womBatches));
|
|
|
|
|
|
std::printf("\n WOB (open WMO):\n");
|
|
|
|
|
|
std::printf(" files : %d\n", wobCount);
|
|
|
|
|
|
std::printf(" groups : %llu\n", static_cast<unsigned long long>(wobGroups));
|
|
|
|
|
|
std::printf(" vertices : %llu\n", static_cast<unsigned long long>(wobVerts));
|
|
|
|
|
|
std::printf(" triangles : %llu\n", static_cast<unsigned long long>(wobIndices / 3));
|
|
|
|
|
|
std::printf(" doodads : %llu\n", static_cast<unsigned long long>(wobDoodads));
|
|
|
|
|
|
std::printf(" portals : %llu\n", static_cast<unsigned long long>(wobPortals));
|
|
|
|
|
|
std::printf("\n Combined :\n");
|
|
|
|
|
|
std::printf(" vertices : %llu\n", static_cast<unsigned long long>(womVerts + wobVerts));
|
|
|
|
|
|
std::printf(" triangles : %llu\n", static_cast<unsigned long long>((womIndices + wobIndices) / 3));
|
|
|
|
|
|
return 0;
|
2026-05-08 19:28:18 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--list-zone-meshes-detail") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Per-mesh breakdown of every .wom file in <zoneDir>,
|
|
|
|
|
|
// sorted by triangle count descending so the heaviest
|
|
|
|
|
|
// meshes float to the top. Complements
|
|
|
|
|
|
// --list-zone-meshes (per-zone summary) by surfacing
|
|
|
|
|
|
// individual mesh metrics — useful for spotting
|
|
|
|
|
|
// outliers ("which mesh is using 80% of my triangle
|
|
|
|
|
|
// budget?") and for content audits.
|
feat(editor): add --list-zone-meshes per-mesh breakdown
Per-mesh listing of every .wom in a zone. Complements
--info-zone-models-total (aggregate) by surfacing individual mesh
metrics: version, vertex count, triangle count, bone count, batch
count, texture-slot count, and on-disk bytes.
Sorted by triangle count descending so the heaviest meshes float to
the top of the table — useful for spotting outliers ("which mesh is
using 80% of my triangle budget?") and for content audits.
Both human (table) and --json output modes. Verified on synthetic
3-mesh zone (sphere 384 tris, cube 12, plane 2): table sorted
descending, totals row matches sum of individual rows, sub-dir mesh
shown with its relative path. Brings command count to 205.
2026-05-07 03:15:05 -07:00
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
2026-05-08 19:28:18 -07:00
|
|
|
|
"list-zone-meshes-detail: %s does not exist\n", zoneDir.c_str());
|
feat(editor): add --list-zone-meshes per-mesh breakdown
Per-mesh listing of every .wom in a zone. Complements
--info-zone-models-total (aggregate) by surfacing individual mesh
metrics: version, vertex count, triangle count, bone count, batch
count, texture-slot count, and on-disk bytes.
Sorted by triangle count descending so the heaviest meshes float to
the top of the table — useful for spotting outliers ("which mesh is
using 80% of my triangle budget?") and for content audits.
Both human (table) and --json output modes. Verified on synthetic
3-mesh zone (sphere 384 tris, cube 12, plane 2): table sorted
descending, totals row matches sum of individual rows, sub-dir mesh
shown with its relative path. Brings command count to 205.
2026-05-07 03:15:05 -07:00
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
struct Row {
|
|
|
|
|
|
std::string path;
|
|
|
|
|
|
size_t verts;
|
|
|
|
|
|
size_t tris;
|
|
|
|
|
|
size_t bones;
|
|
|
|
|
|
size_t batches;
|
|
|
|
|
|
size_t textures;
|
|
|
|
|
|
uint64_t bytes;
|
|
|
|
|
|
uint32_t version;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<Row> rows;
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
if (e.path().extension() != ".wom") continue;
|
|
|
|
|
|
std::string base = e.path().string();
|
|
|
|
|
|
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
auto wom = wowee::pipeline::WoweeModelLoader::load(base);
|
|
|
|
|
|
Row r;
|
|
|
|
|
|
r.path = fs::relative(e.path(), zoneDir, ec).string();
|
|
|
|
|
|
if (ec) r.path = e.path().filename().string();
|
|
|
|
|
|
r.verts = wom.vertices.size();
|
|
|
|
|
|
r.tris = wom.indices.size() / 3;
|
|
|
|
|
|
r.bones = wom.bones.size();
|
|
|
|
|
|
r.batches = wom.batches.size();
|
|
|
|
|
|
r.textures = wom.texturePaths.size();
|
|
|
|
|
|
r.bytes = e.file_size(ec);
|
|
|
|
|
|
if (ec) r.bytes = 0;
|
|
|
|
|
|
r.version = wom.version;
|
|
|
|
|
|
rows.push_back(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(rows.begin(), rows.end(),
|
|
|
|
|
|
[](const Row& a, const Row& b) { return a.tris > b.tris; });
|
|
|
|
|
|
uint64_t totVerts = 0, totTris = 0, totBones = 0, totBytes = 0;
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
totVerts += r.verts; totTris += r.tris;
|
|
|
|
|
|
totBones += r.bones; totBytes += r.bytes;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["zone"] = zoneDir;
|
|
|
|
|
|
j["meshCount"] = rows.size();
|
|
|
|
|
|
j["totals"] = {{"vertices", totVerts},
|
|
|
|
|
|
{"triangles", totTris},
|
|
|
|
|
|
{"bones", totBones},
|
|
|
|
|
|
{"bytes", totBytes}};
|
|
|
|
|
|
nlohmann::json arr = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
arr.push_back({{"path", r.path},
|
|
|
|
|
|
{"version", r.version},
|
|
|
|
|
|
{"vertices", r.verts},
|
|
|
|
|
|
{"triangles", r.tris},
|
|
|
|
|
|
{"bones", r.bones},
|
|
|
|
|
|
{"batches", r.batches},
|
|
|
|
|
|
{"textures", r.textures},
|
|
|
|
|
|
{"bytes", r.bytes}});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["meshes"] = arr;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Zone meshes: %s\n", zoneDir.c_str());
|
|
|
|
|
|
std::printf(" meshes : %zu\n", rows.size());
|
|
|
|
|
|
std::printf(" totals : %llu verts, %llu tris, %llu bones, %.1f KB\n",
|
|
|
|
|
|
static_cast<unsigned long long>(totVerts),
|
|
|
|
|
|
static_cast<unsigned long long>(totTris),
|
|
|
|
|
|
static_cast<unsigned long long>(totBones),
|
|
|
|
|
|
totBytes / 1024.0);
|
|
|
|
|
|
if (rows.empty()) {
|
|
|
|
|
|
std::printf("\n *no .wom files in this zone*\n");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n v verts tris bones batch tex bytes path\n");
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
std::printf(" v%u %6zu %6zu %5zu %5zu %3zu %7llu %s\n",
|
|
|
|
|
|
r.version, r.verts, r.tris, r.bones,
|
|
|
|
|
|
r.batches, r.textures,
|
|
|
|
|
|
static_cast<unsigned long long>(r.bytes),
|
|
|
|
|
|
r.path.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
2026-05-07 05:44:37 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-mesh") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Single-mesh detail view aggregating bounds, version,
|
|
|
|
|
|
// batches, bones, animations, and texture slots into one
|
|
|
|
|
|
// report. Composite of what --info-batches / --info-bones
|
|
|
|
|
|
// / --info-batches show separately. Useful authoring
|
|
|
|
|
|
// command: pass a WOM and see everything about it without
|
|
|
|
|
|
// running three sub-commands.
|
|
|
|
|
|
std::string base = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") {
|
|
|
|
|
|
base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wowee::pipeline::WoweeModelLoader::exists(base)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"info-mesh: %s.wom does not exist\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto wom = wowee::pipeline::WoweeModelLoader::load(base);
|
|
|
|
|
|
if (!wom.isValid()) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"info-mesh: failed to load %s.wom\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Per-batch material summary.
|
|
|
|
|
|
static const char* blendNames[] = {
|
|
|
|
|
|
"opaque", "alpha-test", "alpha", "additive", "?", "?", "?", "?"
|
|
|
|
|
|
};
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["base"] = base;
|
|
|
|
|
|
j["name"] = wom.name;
|
|
|
|
|
|
j["version"] = wom.version;
|
|
|
|
|
|
j["bounds"] = {{"min", {wom.boundMin.x, wom.boundMin.y, wom.boundMin.z}},
|
|
|
|
|
|
{"max", {wom.boundMax.x, wom.boundMax.y, wom.boundMax.z}},
|
|
|
|
|
|
{"radius", wom.boundRadius}};
|
|
|
|
|
|
j["counts"] = {{"vertices", wom.vertices.size()},
|
|
|
|
|
|
{"indices", wom.indices.size()},
|
|
|
|
|
|
{"triangles", wom.indices.size() / 3},
|
|
|
|
|
|
{"bones", wom.bones.size()},
|
|
|
|
|
|
{"animations", wom.animations.size()},
|
|
|
|
|
|
{"batches", wom.batches.size()},
|
|
|
|
|
|
{"textures", wom.texturePaths.size()}};
|
|
|
|
|
|
nlohmann::json bs = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& b : wom.batches) {
|
|
|
|
|
|
std::string tex;
|
|
|
|
|
|
if (b.textureIndex < wom.texturePaths.size())
|
|
|
|
|
|
tex = wom.texturePaths[b.textureIndex];
|
|
|
|
|
|
bs.push_back({{"indexStart", b.indexStart},
|
|
|
|
|
|
{"indexCount", b.indexCount},
|
|
|
|
|
|
{"triangles", b.indexCount / 3},
|
|
|
|
|
|
{"textureIndex", b.textureIndex},
|
|
|
|
|
|
{"texture", tex},
|
|
|
|
|
|
{"blendMode", b.blendMode},
|
|
|
|
|
|
{"flags", b.flags}});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["batchDetail"] = bs;
|
|
|
|
|
|
j["texturePaths"] = wom.texturePaths;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Mesh: %s.wom\n", base.c_str());
|
|
|
|
|
|
std::printf(" name : %s\n", wom.name.c_str());
|
|
|
|
|
|
std::printf(" version : v%u\n", wom.version);
|
|
|
|
|
|
std::printf("\n Counts:\n");
|
|
|
|
|
|
std::printf(" vertices : %zu\n", wom.vertices.size());
|
|
|
|
|
|
std::printf(" triangles : %zu\n", wom.indices.size() / 3);
|
|
|
|
|
|
std::printf(" bones : %zu\n", wom.bones.size());
|
|
|
|
|
|
std::printf(" anims : %zu\n", wom.animations.size());
|
|
|
|
|
|
std::printf(" batches : %zu\n", wom.batches.size());
|
|
|
|
|
|
std::printf(" textures : %zu\n", wom.texturePaths.size());
|
|
|
|
|
|
std::printf("\n Bounds:\n");
|
|
|
|
|
|
std::printf(" min : (%.3f, %.3f, %.3f)\n",
|
|
|
|
|
|
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z);
|
|
|
|
|
|
std::printf(" max : (%.3f, %.3f, %.3f)\n",
|
|
|
|
|
|
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
|
|
|
|
|
|
std::printf(" radius : %.3f\n", wom.boundRadius);
|
|
|
|
|
|
if (!wom.batches.empty()) {
|
|
|
|
|
|
std::printf("\n Batches:\n");
|
|
|
|
|
|
std::printf(" idx iStart iCount tris blend texture\n");
|
|
|
|
|
|
for (size_t k = 0; k < wom.batches.size(); ++k) {
|
|
|
|
|
|
const auto& b = wom.batches[k];
|
|
|
|
|
|
std::string tex = "<oob>";
|
|
|
|
|
|
if (b.textureIndex < wom.texturePaths.size())
|
|
|
|
|
|
tex = wom.texturePaths[b.textureIndex];
|
|
|
|
|
|
if (tex.empty()) tex = "(empty)";
|
|
|
|
|
|
int blend = b.blendMode < 8 ? b.blendMode : 0;
|
|
|
|
|
|
std::printf(" %3zu %6u %6u %4u %-10s %s\n",
|
|
|
|
|
|
k, b.indexStart, b.indexCount,
|
|
|
|
|
|
b.indexCount / 3, blendNames[blend],
|
|
|
|
|
|
tex.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wom.texturePaths.empty()) {
|
|
|
|
|
|
std::printf("\n Texture slots:\n");
|
|
|
|
|
|
for (size_t k = 0; k < wom.texturePaths.size(); ++k) {
|
|
|
|
|
|
std::printf(" [%zu] %s\n", k,
|
|
|
|
|
|
wom.texturePaths[k].empty()
|
|
|
|
|
|
? "(empty placeholder)"
|
|
|
|
|
|
: wom.texturePaths[k].c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
2026-05-07 09:00:47 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-mesh-storage-budget") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Estimated bytes-per-category breakdown for a WOM.
|
|
|
|
|
|
// Numbers are based on the in-memory struct sizes, not
|
|
|
|
|
|
// the actual on-disk encoding (which has framing
|
|
|
|
|
|
// overhead) — but the relative shares are accurate and
|
|
|
|
|
|
// help users decide where shrinking efforts pay off.
|
|
|
|
|
|
//
|
|
|
|
|
|
// For example: a heightmap mesh's bytes are dominated by
|
|
|
|
|
|
// vertices, so reducing vertex count is the lever to
|
|
|
|
|
|
// pull. A skeletal mesh's animation keyframes can dwarf
|
|
|
|
|
|
// the geometry itself — surfacing that lets the user
|
|
|
|
|
|
// know to consider --strip-mesh --anims.
|
|
|
|
|
|
std::string base = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") {
|
|
|
|
|
|
base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wowee::pipeline::WoweeModelLoader::exists(base)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"info-mesh-storage-budget: %s.wom does not exist\n",
|
|
|
|
|
|
base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto wom = wowee::pipeline::WoweeModelLoader::load(base);
|
|
|
|
|
|
if (!wom.isValid()) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"info-mesh-storage-budget: failed to load %s.wom\n",
|
|
|
|
|
|
base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Per-category byte estimates. Vertex is 12+12+8+4+4=40
|
|
|
|
|
|
// bytes (pos/normal/uv/4 weights/4 indices). Index is
|
|
|
|
|
|
// 4 bytes. Bone is 4+2+12+4=22 bytes. Batch is 4+4+4+2+
|
|
|
|
|
|
// 2=16. Animation keyframe is 4+12+16+12=44 bytes.
|
|
|
|
|
|
// Texture path is summed length plus a small per-string
|
|
|
|
|
|
// overhead.
|
|
|
|
|
|
uint64_t vertBytes = wom.vertices.size() * 40;
|
|
|
|
|
|
uint64_t idxBytes = wom.indices.size() * 4;
|
|
|
|
|
|
uint64_t boneBytes = wom.bones.size() * 22;
|
|
|
|
|
|
uint64_t batchBytes = wom.batches.size() * 16;
|
|
|
|
|
|
uint64_t animBytes = 0;
|
|
|
|
|
|
size_t totalKeyframes = 0;
|
|
|
|
|
|
for (const auto& a : wom.animations) {
|
|
|
|
|
|
animBytes += 12; // id + duration + movingSpeed
|
|
|
|
|
|
for (const auto& bone : a.boneKeyframes) {
|
|
|
|
|
|
animBytes += bone.size() * 44;
|
|
|
|
|
|
totalKeyframes += bone.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
uint64_t texBytes = 0;
|
|
|
|
|
|
for (const auto& t : wom.texturePaths) texBytes += t.size() + 8;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
uint64_t actualBytes = fs::file_size(base + ".wom");
|
|
|
|
|
|
uint64_t estBytes = vertBytes + idxBytes + boneBytes +
|
|
|
|
|
|
batchBytes + animBytes + texBytes;
|
|
|
|
|
|
struct Row { const char* name; uint64_t bytes; };
|
|
|
|
|
|
std::vector<Row> rows = {
|
|
|
|
|
|
{"vertices ", vertBytes},
|
|
|
|
|
|
{"indices ", idxBytes},
|
|
|
|
|
|
{"bones ", boneBytes},
|
|
|
|
|
|
{"animations", animBytes},
|
|
|
|
|
|
{"batches ", batchBytes},
|
|
|
|
|
|
{"textures ", texBytes},
|
|
|
|
|
|
};
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["base"] = base;
|
|
|
|
|
|
j["fileBytes"] = actualBytes;
|
|
|
|
|
|
j["estimatedBytes"] = estBytes;
|
|
|
|
|
|
j["categories"] = nlohmann::json::object();
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
double share = estBytes > 0
|
|
|
|
|
|
? 100.0 * r.bytes / estBytes : 0.0;
|
|
|
|
|
|
j["categories"][r.name] = {{"bytes", r.bytes},
|
|
|
|
|
|
{"share", share}};
|
|
|
|
|
|
}
|
|
|
|
|
|
j["counts"] = {{"vertices", wom.vertices.size()},
|
|
|
|
|
|
{"indices", wom.indices.size()},
|
|
|
|
|
|
{"bones", wom.bones.size()},
|
|
|
|
|
|
{"animations", wom.animations.size()},
|
|
|
|
|
|
{"keyframes", totalKeyframes},
|
|
|
|
|
|
{"batches", wom.batches.size()},
|
|
|
|
|
|
{"textures", wom.texturePaths.size()}};
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Mesh storage budget: %s.wom\n", base.c_str());
|
|
|
|
|
|
std::printf(" on-disk : %llu bytes (%.1f KB)\n",
|
|
|
|
|
|
static_cast<unsigned long long>(actualBytes),
|
|
|
|
|
|
actualBytes / 1024.0);
|
|
|
|
|
|
std::printf(" estimated : %llu bytes (sum of in-memory parts)\n",
|
|
|
|
|
|
static_cast<unsigned long long>(estBytes));
|
|
|
|
|
|
std::printf("\n Per-category (estimated):\n");
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
if (r.bytes == 0) continue;
|
|
|
|
|
|
double share = estBytes > 0
|
|
|
|
|
|
? 100.0 * r.bytes / estBytes : 0.0;
|
|
|
|
|
|
std::printf(" %s : %10llu bytes (%5.1f%%)\n",
|
|
|
|
|
|
r.name,
|
|
|
|
|
|
static_cast<unsigned long long>(r.bytes),
|
|
|
|
|
|
share);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n Tips:\n");
|
|
|
|
|
|
if (animBytes > vertBytes && wom.animations.size() > 0) {
|
|
|
|
|
|
std::printf(" - animations dominate; --strip-mesh "
|
|
|
|
|
|
"--anims would save %.1f KB\n",
|
|
|
|
|
|
animBytes / 1024.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (boneBytes > vertBytes / 2 && wom.bones.size() > 0) {
|
|
|
|
|
|
std::printf(" - bones non-trivial; consider "
|
|
|
|
|
|
"--strip-mesh --bones for static placement\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (vertBytes > estBytes / 2) {
|
|
|
|
|
|
std::printf(" - vertices dominate; check if a "
|
|
|
|
|
|
"lower-poly variant works for placement\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
2026-05-06 20:25:00 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-project-models-total") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Multi-zone aggregate. Walks every zone in <projectDir>,
|
|
|
|
|
|
// sums the same WOM/WOB metrics --info-zone-models-total
|
|
|
|
|
|
// emits, and prints a per-zone breakdown table followed
|
|
|
|
|
|
// by project-wide totals. Useful for capacity planning
|
|
|
|
|
|
// across an entire content project.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"info-project-models-total: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zones.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zones.begin(), zones.end());
|
|
|
|
|
|
struct ZRow {
|
|
|
|
|
|
std::string name;
|
|
|
|
|
|
int womCount = 0, wobCount = 0;
|
|
|
|
|
|
uint64_t womVerts = 0, womIndices = 0, womBones = 0;
|
|
|
|
|
|
uint64_t womAnims = 0, womBatches = 0;
|
|
|
|
|
|
uint64_t wobGroups = 0, wobVerts = 0, wobIndices = 0;
|
|
|
|
|
|
uint64_t wobDoodads = 0, wobPortals = 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<ZRow> rows;
|
|
|
|
|
|
ZRow tot;
|
|
|
|
|
|
tot.name = "TOTAL";
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
ZRow r;
|
|
|
|
|
|
r.name = fs::path(zoneDir).filename().string();
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
std::string ext = e.path().extension().string();
|
|
|
|
|
|
std::string base = e.path().string();
|
|
|
|
|
|
if (base.size() > ext.size())
|
|
|
|
|
|
base = base.substr(0, base.size() - ext.size());
|
|
|
|
|
|
if (ext == ".wom") {
|
|
|
|
|
|
r.womCount++;
|
|
|
|
|
|
auto wom = wowee::pipeline::WoweeModelLoader::load(base);
|
|
|
|
|
|
r.womVerts += wom.vertices.size();
|
|
|
|
|
|
r.womIndices += wom.indices.size();
|
|
|
|
|
|
r.womBones += wom.bones.size();
|
|
|
|
|
|
r.womAnims += wom.animations.size();
|
|
|
|
|
|
r.womBatches += wom.batches.size();
|
|
|
|
|
|
} else if (ext == ".wob") {
|
|
|
|
|
|
r.wobCount++;
|
|
|
|
|
|
auto wob = wowee::pipeline::WoweeBuildingLoader::load(base);
|
|
|
|
|
|
r.wobGroups += wob.groups.size();
|
|
|
|
|
|
for (const auto& g : wob.groups) {
|
|
|
|
|
|
r.wobVerts += g.vertices.size();
|
|
|
|
|
|
r.wobIndices += g.indices.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
r.wobDoodads += wob.doodads.size();
|
|
|
|
|
|
r.wobPortals += wob.portals.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tot.womCount += r.womCount;
|
|
|
|
|
|
tot.wobCount += r.wobCount;
|
|
|
|
|
|
tot.womVerts += r.womVerts;
|
|
|
|
|
|
tot.womIndices += r.womIndices;
|
|
|
|
|
|
tot.womBones += r.womBones;
|
|
|
|
|
|
tot.womAnims += r.womAnims;
|
|
|
|
|
|
tot.womBatches += r.womBatches;
|
|
|
|
|
|
tot.wobGroups += r.wobGroups;
|
|
|
|
|
|
tot.wobVerts += r.wobVerts;
|
|
|
|
|
|
tot.wobIndices += r.wobIndices;
|
|
|
|
|
|
tot.wobDoodads += r.wobDoodads;
|
|
|
|
|
|
tot.wobPortals += r.wobPortals;
|
|
|
|
|
|
rows.push_back(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["project"] = projectDir;
|
|
|
|
|
|
j["zones"] = nlohmann::json::array();
|
|
|
|
|
|
auto rowJson = [](const ZRow& r) {
|
|
|
|
|
|
nlohmann::json z;
|
|
|
|
|
|
z["name"] = r.name;
|
|
|
|
|
|
z["wom"] = {{"count", r.womCount},
|
|
|
|
|
|
{"vertices", r.womVerts},
|
|
|
|
|
|
{"indices", r.womIndices},
|
|
|
|
|
|
{"triangles", r.womIndices / 3},
|
|
|
|
|
|
{"bones", r.womBones},
|
|
|
|
|
|
{"animations", r.womAnims},
|
|
|
|
|
|
{"batches", r.womBatches}};
|
|
|
|
|
|
z["wob"] = {{"count", r.wobCount},
|
|
|
|
|
|
{"groups", r.wobGroups},
|
|
|
|
|
|
{"vertices", r.wobVerts},
|
|
|
|
|
|
{"indices", r.wobIndices},
|
|
|
|
|
|
{"triangles", r.wobIndices / 3},
|
|
|
|
|
|
{"doodads", r.wobDoodads},
|
|
|
|
|
|
{"portals", r.wobPortals}};
|
|
|
|
|
|
return z;
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const auto& r : rows) j["zones"].push_back(rowJson(r));
|
|
|
|
|
|
j["total"] = rowJson(tot);
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Project models total: %s\n", projectDir.c_str());
|
|
|
|
|
|
std::printf(" zones : %zu\n\n", zones.size());
|
|
|
|
|
|
std::printf(" zone WOMs WOMtri bones WOBs WOBtri doodads\n");
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
std::printf(" %-20s %5d %7llu %6llu %5d %7llu %8llu\n",
|
|
|
|
|
|
r.name.substr(0, 20).c_str(),
|
|
|
|
|
|
r.womCount,
|
|
|
|
|
|
static_cast<unsigned long long>(r.womIndices / 3),
|
|
|
|
|
|
static_cast<unsigned long long>(r.womBones),
|
|
|
|
|
|
r.wobCount,
|
|
|
|
|
|
static_cast<unsigned long long>(r.wobIndices / 3),
|
|
|
|
|
|
static_cast<unsigned long long>(r.wobDoodads));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf(" %-20s %5d %7llu %6llu %5d %7llu %8llu\n",
|
|
|
|
|
|
tot.name.c_str(),
|
|
|
|
|
|
tot.womCount,
|
|
|
|
|
|
static_cast<unsigned long long>(tot.womIndices / 3),
|
|
|
|
|
|
static_cast<unsigned long long>(tot.womBones),
|
|
|
|
|
|
tot.wobCount,
|
|
|
|
|
|
static_cast<unsigned long long>(tot.wobIndices / 3),
|
|
|
|
|
|
static_cast<unsigned long long>(tot.wobDoodads));
|
|
|
|
|
|
std::printf("\n Combined verts/tris (WOM+WOB): %llu / %llu\n",
|
|
|
|
|
|
static_cast<unsigned long long>(tot.womVerts + tot.wobVerts),
|
|
|
|
|
|
static_cast<unsigned long long>((tot.womIndices + tot.wobIndices) / 3));
|
|
|
|
|
|
return 0;
|
2026-05-07 19:31:09 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--copy-project") == 0 && i + 2 < argc) {
|
|
|
|
|
|
// Recursively copy an entire project tree. Refuses to
|
|
|
|
|
|
// overwrite an existing destination so a typo doesn't
|
|
|
|
|
|
// silently merge into the wrong project.
|
|
|
|
|
|
std::string fromDir = argv[++i];
|
|
|
|
|
|
std::string toDir = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(fromDir) || !fs::is_directory(fromDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"copy-project: %s is not a directory\n", fromDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fs::exists(toDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"copy-project: destination %s already exists "
|
|
|
|
|
|
"(delete it first if intentional)\n", toDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
fs::copy(fromDir, toDir,
|
|
|
|
|
|
fs::copy_options::recursive | fs::copy_options::copy_symlinks,
|
|
|
|
|
|
ec);
|
|
|
|
|
|
if (ec) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"copy-project: copy failed (%s)\n", ec.message().c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Count what was copied for the report.
|
|
|
|
|
|
int zoneCount = 0, fileCount = 0;
|
|
|
|
|
|
uint64_t totalBytes = 0;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(toDir, ec)) {
|
|
|
|
|
|
if (entry.is_directory() &&
|
|
|
|
|
|
fs::exists(entry.path() / "zone.json")) zoneCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(toDir, ec)) {
|
|
|
|
|
|
if (e.is_regular_file()) {
|
|
|
|
|
|
fileCount++;
|
|
|
|
|
|
totalBytes += e.file_size(ec);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Copied %s -> %s\n", fromDir.c_str(), toDir.c_str());
|
|
|
|
|
|
std::printf(" zones : %d\n", zoneCount);
|
|
|
|
|
|
std::printf(" files : %d\n", fileCount);
|
|
|
|
|
|
std::printf(" total bytes : %llu (%.1f MB)\n",
|
|
|
|
|
|
static_cast<unsigned long long>(totalBytes),
|
|
|
|
|
|
totalBytes / (1024.0 * 1024.0));
|
|
|
|
|
|
return 0;
|
2026-05-06 08:39:38 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--zone-summary") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// One-shot zone overview: validate + creature/object/quest counts.
|
|
|
|
|
|
// Collapses the most common multi-step inspection into a single
|
|
|
|
|
|
// command; useful for CI reports and quick sanity checks.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
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
|
|
|
|
// Optional --json after the dir for machine-readable output.
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
2026-05-06 08:39:38 -07:00
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir)) {
|
|
|
|
|
|
std::fprintf(stderr, "zone-summary: %s does not exist\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto v = wowee::editor::ContentPacker::validateZone(zoneDir);
|
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
|
|
|
|
|
|
|
|
|
|
// Read creature/object/quest data once so both human and JSON
|
|
|
|
|
|
// outputs share the same numbers.
|
|
|
|
|
|
int creatureTotal = 0, hostile = 0, qg = 0, vendor = 0;
|
|
|
|
|
|
int objectTotal = 0, m2Count = 0, wmoCount = 0;
|
|
|
|
|
|
int questTotal = 0, chainWarnings = 0;
|
2026-05-06 08:39:38 -07:00
|
|
|
|
std::string creaturesPath = zoneDir + "/creatures.json";
|
|
|
|
|
|
if (fs::exists(creaturesPath)) {
|
|
|
|
|
|
wowee::editor::NpcSpawner sp;
|
|
|
|
|
|
if (sp.loadFromFile(creaturesPath)) {
|
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
|
|
|
|
creatureTotal = static_cast<int>(sp.getSpawns().size());
|
2026-05-06 08:39:38 -07:00
|
|
|
|
for (const auto& s : sp.getSpawns()) {
|
|
|
|
|
|
if (s.hostile) hostile++;
|
|
|
|
|
|
if (s.questgiver) qg++;
|
|
|
|
|
|
if (s.vendor) vendor++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string objectsPath = zoneDir + "/objects.json";
|
|
|
|
|
|
if (fs::exists(objectsPath)) {
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(objectsPath)) {
|
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
|
|
|
|
objectTotal = static_cast<int>(op.getObjects().size());
|
2026-05-06 08:39:38 -07:00
|
|
|
|
for (const auto& o : op.getObjects()) {
|
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
|
|
|
|
if (o.type == wowee::editor::PlaceableType::M2) m2Count++;
|
|
|
|
|
|
else wmoCount++;
|
2026-05-06 08:39:38 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string questsPath = zoneDir + "/quests.json";
|
|
|
|
|
|
if (fs::exists(questsPath)) {
|
|
|
|
|
|
wowee::editor::QuestEditor qe;
|
|
|
|
|
|
if (qe.loadFromFile(questsPath)) {
|
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
|
|
|
|
questTotal = static_cast<int>(qe.getQuests().size());
|
2026-05-06 08:39:38 -07:00
|
|
|
|
std::vector<std::string> errors;
|
|
|
|
|
|
qe.validateChains(errors);
|
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
|
|
|
|
chainWarnings = static_cast<int>(errors.size());
|
2026-05-06 08:39:38 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["zone"] = zoneDir;
|
|
|
|
|
|
j["score"] = v.openFormatScore();
|
|
|
|
|
|
j["maxScore"] = 7;
|
|
|
|
|
|
j["formats"] = v.summary();
|
|
|
|
|
|
j["counts"] = {
|
|
|
|
|
|
{"wot", v.wotCount}, {"whm", v.whmCount},
|
|
|
|
|
|
{"wom", v.womCount}, {"wob", v.wobCount},
|
|
|
|
|
|
{"woc", v.wocCount}, {"png", v.pngCount},
|
|
|
|
|
|
};
|
|
|
|
|
|
j["creatures"] = {
|
|
|
|
|
|
{"total", creatureTotal},
|
|
|
|
|
|
{"hostile", hostile},
|
|
|
|
|
|
{"questgiver", qg},
|
|
|
|
|
|
{"vendor", vendor},
|
|
|
|
|
|
};
|
|
|
|
|
|
j["objects"] = {
|
|
|
|
|
|
{"total", objectTotal},
|
|
|
|
|
|
{"m2", m2Count},
|
|
|
|
|
|
{"wmo", wmoCount},
|
|
|
|
|
|
};
|
|
|
|
|
|
j["quests"] = {
|
|
|
|
|
|
{"total", questTotal},
|
|
|
|
|
|
{"chainWarnings", chainWarnings},
|
|
|
|
|
|
};
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return v.openFormatScore() == 7 ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Zone: %s\n", zoneDir.c_str());
|
|
|
|
|
|
std::printf(" open formats : %d/7 (%s)\n",
|
|
|
|
|
|
v.openFormatScore(), v.summary().c_str());
|
|
|
|
|
|
std::printf(" WOT/WHM : %d/%d WOM: %d WOB: %d WOC: %d PNG: %d\n",
|
|
|
|
|
|
v.wotCount, v.whmCount, v.womCount, v.wobCount,
|
|
|
|
|
|
v.wocCount, v.pngCount);
|
|
|
|
|
|
if (creatureTotal > 0) {
|
|
|
|
|
|
std::printf(" creatures : %d (%d hostile, %d quest, %d vendor)\n",
|
|
|
|
|
|
creatureTotal, hostile, qg, vendor);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (objectTotal > 0) {
|
|
|
|
|
|
std::printf(" objects : %d (%d M2, %d WMO)\n",
|
|
|
|
|
|
objectTotal, m2Count, wmoCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (questTotal > 0) {
|
|
|
|
|
|
std::printf(" quests : %d (%d chain warnings)\n",
|
|
|
|
|
|
questTotal, chainWarnings);
|
|
|
|
|
|
}
|
2026-05-06 08:39:38 -07:00
|
|
|
|
return v.openFormatScore() == 7 ? 0 : 1;
|
2026-05-06 18:50:20 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--bench-bake-project") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Time WHM/WOT load (the dominant cost in --bake-zone-glb/obj/
|
|
|
|
|
|
// stl) per zone. The actual write side adds ~constant cost
|
|
|
|
|
|
// proportional to vertex count, so load time is a strong
|
|
|
|
|
|
// proxy. Useful for tracking 'has my latest geometry change
|
|
|
|
|
|
// made baking 3× slower?' across releases.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bench-bake-project: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zones.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zones.begin(), zones.end());
|
|
|
|
|
|
struct Timing {
|
|
|
|
|
|
std::string name;
|
|
|
|
|
|
int tiles;
|
|
|
|
|
|
double loadMs;
|
|
|
|
|
|
int chunks;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<Timing> timings;
|
|
|
|
|
|
double totalMs = 0;
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(zoneDir + "/zone.json")) continue;
|
|
|
|
|
|
Timing t{fs::path(zoneDir).filename().string(), 0, 0.0, 0};
|
|
|
|
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
std::string base = zoneDir + "/" + zm.mapName + "_" +
|
|
|
|
|
|
std::to_string(tx) + "_" + std::to_string(ty);
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(base)) continue;
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
wowee::pipeline::WoweeTerrainLoader::load(base, terrain);
|
|
|
|
|
|
t.tiles++;
|
|
|
|
|
|
for (const auto& chunk : terrain.chunks) {
|
|
|
|
|
|
if (chunk.heightMap.isLoaded()) t.chunks++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
auto t1 = std::chrono::steady_clock::now();
|
|
|
|
|
|
t.loadMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
|
|
|
|
|
|
totalMs += t.loadMs;
|
|
|
|
|
|
timings.push_back(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
double avgMs = !timings.empty() ? totalMs / timings.size() : 0.0;
|
|
|
|
|
|
double minMs = 1e30, maxMs = 0;
|
|
|
|
|
|
std::string slowest;
|
|
|
|
|
|
for (const auto& t : timings) {
|
|
|
|
|
|
if (t.loadMs < minMs) minMs = t.loadMs;
|
|
|
|
|
|
if (t.loadMs > maxMs) { maxMs = t.loadMs; slowest = t.name; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (timings.empty()) { minMs = 0; maxMs = 0; }
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["projectDir"] = projectDir;
|
|
|
|
|
|
j["totalMs"] = totalMs;
|
|
|
|
|
|
j["zoneCount"] = timings.size();
|
|
|
|
|
|
j["avgMs"] = avgMs;
|
|
|
|
|
|
j["minMs"] = minMs;
|
|
|
|
|
|
j["maxMs"] = maxMs;
|
|
|
|
|
|
j["slowestZone"] = slowest;
|
|
|
|
|
|
nlohmann::json arr = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& t : timings) {
|
|
|
|
|
|
arr.push_back({{"zone", t.name},
|
|
|
|
|
|
{"loadMs", t.loadMs},
|
|
|
|
|
|
{"tiles", t.tiles},
|
|
|
|
|
|
{"chunks", t.chunks}});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["perZone"] = arr;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Bench bake (load-only): %s\n", projectDir.c_str());
|
|
|
|
|
|
std::printf(" zones : %zu\n", timings.size());
|
|
|
|
|
|
std::printf(" total : %.2f ms (terrain load)\n", totalMs);
|
|
|
|
|
|
std::printf(" per zone : avg=%.2f min=%.2f max=%.2f ms\n",
|
|
|
|
|
|
avgMs, minMs, maxMs);
|
|
|
|
|
|
if (!slowest.empty()) {
|
|
|
|
|
|
std::printf(" slowest : %s (%.2f ms)\n", slowest.c_str(), maxMs);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n Per-zone:\n");
|
|
|
|
|
|
std::printf(" zone ms tiles chunks ms/tile\n");
|
|
|
|
|
|
for (const auto& t : timings) {
|
|
|
|
|
|
double mspt = t.tiles > 0 ? t.loadMs / t.tiles : 0.0;
|
|
|
|
|
|
std::printf(" %-26s %7.2f %5d %5d %6.2f\n",
|
|
|
|
|
|
t.name.substr(0, 26).c_str(),
|
|
|
|
|
|
t.loadMs, t.tiles, t.chunks, mspt);
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
2026-05-06 08:22:26 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--export-png") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Render heightmap, normal-map, and zone-map PNG previews for a
|
|
|
|
|
|
// terrain. Useful for portfolio screenshots, ground-truth map
|
|
|
|
|
|
// comparison, and quick visual validation without launching GUI.
|
|
|
|
|
|
std::string base = argv[++i];
|
|
|
|
|
|
for (const char* ext : {".wot", ".whm"}) {
|
|
|
|
|
|
if (base.size() >= 4 && base.substr(base.size() - 4) == ext) {
|
|
|
|
|
|
base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(base)) {
|
|
|
|
|
|
std::fprintf(stderr, "WOT/WHM not found at base: %s\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::load(base, terrain)) {
|
|
|
|
|
|
std::fprintf(stderr, "Failed to load terrain: %s\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::WoweeTerrain::exportHeightmapPreview(terrain, base + "_heightmap.png");
|
|
|
|
|
|
wowee::editor::WoweeTerrain::exportNormalMap(terrain, base + "_normals.png");
|
|
|
|
|
|
wowee::editor::WoweeTerrain::exportZoneMap(terrain, base + "_zone.png", 512);
|
|
|
|
|
|
std::printf("Exported PNGs: %s_{heightmap,normals,zone}.png\n", base.c_str());
|
2026-05-06 10:08:49 -07:00
|
|
|
|
return 0;
|
|
|
|
|
|
} else if (std::strcmp(argv[i], "--fix-zone") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Re-parse + re-save every JSON/binary file in a zone to apply
|
|
|
|
|
|
// the editor's load-time scrubs and save-time caps. Useful when
|
|
|
|
|
|
// an old zone was created before recent hardening — running
|
|
|
|
|
|
// this once cleans up NaN/oversize fields without touching
|
|
|
|
|
|
// the editor GUI.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir)) {
|
|
|
|
|
|
std::fprintf(stderr, "fix-zone: %s does not exist\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
int touched = 0;
|
|
|
|
|
|
// zone.json
|
|
|
|
|
|
{
|
|
|
|
|
|
wowee::editor::ZoneManifest m;
|
|
|
|
|
|
std::string p = zoneDir + "/zone.json";
|
|
|
|
|
|
if (fs::exists(p) && m.load(p) && m.save(p)) touched++;
|
|
|
|
|
|
}
|
|
|
|
|
|
// creatures.json
|
|
|
|
|
|
{
|
|
|
|
|
|
wowee::editor::NpcSpawner sp;
|
|
|
|
|
|
std::string p = zoneDir + "/creatures.json";
|
|
|
|
|
|
if (fs::exists(p) && sp.loadFromFile(p) && sp.saveToFile(p)) touched++;
|
|
|
|
|
|
}
|
|
|
|
|
|
// objects.json
|
|
|
|
|
|
{
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
std::string p = zoneDir + "/objects.json";
|
|
|
|
|
|
if (fs::exists(p) && op.loadFromFile(p) && op.saveToFile(p)) touched++;
|
|
|
|
|
|
}
|
|
|
|
|
|
// quests.json
|
|
|
|
|
|
{
|
|
|
|
|
|
wowee::editor::QuestEditor qe;
|
|
|
|
|
|
std::string p = zoneDir + "/quests.json";
|
|
|
|
|
|
if (fs::exists(p) && qe.loadFromFile(p) && qe.saveToFile(p)) touched++;
|
|
|
|
|
|
}
|
|
|
|
|
|
// WHM/WOT pairs and WoB files would need full pipeline access;
|
|
|
|
|
|
// skip them — the editor opens them on next zone load anyway,
|
|
|
|
|
|
// and the load-time scrubs run then.
|
|
|
|
|
|
std::printf("fix-zone: cleaned %d files in %s\n", touched, zoneDir.c_str());
|
2026-05-06 08:22:26 -07:00
|
|
|
|
return 0;
|
2026-05-06 08:53:12 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--regen-collision") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Find all WHM/WOT pairs under a zone dir and rebuild WOC for each.
|
|
|
|
|
|
// Useful after sculpting changes when you want to re-derive
|
|
|
|
|
|
// collision in batch instead of one tile at a time.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir)) {
|
|
|
|
|
|
std::fprintf(stderr, "regen-collision: %s does not exist\n",
|
|
|
|
|
|
zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
int rebuilt = 0, failed = 0;
|
|
|
|
|
|
for (auto& entry : fs::recursive_directory_iterator(zoneDir)) {
|
|
|
|
|
|
if (!entry.is_regular_file()) continue;
|
|
|
|
|
|
if (entry.path().extension() != ".whm") continue;
|
|
|
|
|
|
std::string base = entry.path().string();
|
|
|
|
|
|
base = base.substr(0, base.size() - 4); // strip .whm
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::load(base, terrain)) {
|
|
|
|
|
|
std::fprintf(stderr, " FAILED to load: %s\n", base.c_str());
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto col = wowee::pipeline::WoweeCollisionBuilder::fromTerrain(terrain);
|
|
|
|
|
|
std::string outPath = base + ".woc";
|
|
|
|
|
|
if (wowee::pipeline::WoweeCollisionBuilder::save(col, outPath)) {
|
|
|
|
|
|
std::printf(" WOC rebuilt: %s (%zu triangles)\n",
|
|
|
|
|
|
outPath.c_str(), col.triangles.size());
|
|
|
|
|
|
rebuilt++;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
std::fprintf(stderr, " FAILED to save: %s\n", outPath.c_str());
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("regen-collision: %d rebuilt, %d failed\n", rebuilt, failed);
|
|
|
|
|
|
return failed > 0 ? 1 : 0;
|
2026-05-06 08:21:14 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--build-woc") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Generate a WOC collision mesh from a WHM/WOT terrain pair.
|
|
|
|
|
|
// Uses terrain triangles only (no WMO overlays); useful as a
|
|
|
|
|
|
// first-pass collision build before the editor adds buildings.
|
|
|
|
|
|
std::string base = argv[++i];
|
|
|
|
|
|
for (const char* ext : {".wot", ".whm", ".woc"}) {
|
|
|
|
|
|
if (base.size() >= 4 && base.substr(base.size() - 4) == ext) {
|
|
|
|
|
|
base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(base)) {
|
|
|
|
|
|
std::fprintf(stderr, "WOT/WHM not found at base: %s\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::load(base, terrain)) {
|
|
|
|
|
|
std::fprintf(stderr, "Failed to load terrain: %s\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto col = wowee::pipeline::WoweeCollisionBuilder::fromTerrain(terrain);
|
|
|
|
|
|
std::string outPath = base + ".woc";
|
|
|
|
|
|
if (!wowee::pipeline::WoweeCollisionBuilder::save(col, outPath)) {
|
|
|
|
|
|
std::fprintf(stderr, "WOC save failed: %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("WOC built: %s (%zu triangles, %zu walkable, %zu steep)\n",
|
|
|
|
|
|
outPath.c_str(),
|
|
|
|
|
|
col.triangles.size(), col.walkableCount(), col.steepCount());
|
|
|
|
|
|
return 0;
|
2026-05-07 00:52:29 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--gen-texture") == 0 && i + 2 < argc) {
|
|
|
|
|
|
// Synthesize a placeholder PNG texture. Lets users add a
|
|
|
|
|
|
// working texture to their project without an external
|
|
|
|
|
|
// image editor — useful for prototyping new meshes,
|
|
|
|
|
|
// filling out a zone before art is final, or generating
|
|
|
|
|
|
// test fixtures.
|
|
|
|
|
|
//
|
|
|
|
|
|
// <colorHex|pattern>:
|
|
|
|
|
|
// "RRGGBB" or "RGB" hex (case-insensitive) → solid color
|
|
|
|
|
|
// "checker" → 32x32 black/white checkerboard
|
|
|
|
|
|
// "grid" → black background with white 1-px grid every 16
|
|
|
|
|
|
std::string outPath = argv[++i];
|
|
|
|
|
|
std::string spec = argv[++i];
|
|
|
|
|
|
int W = 256, H = 256;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
|
|
|
|
|
try { W = std::stoi(argv[++i]); } catch (...) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
|
|
|
|
|
try { H = std::stoi(argv[++i]); } catch (...) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (W < 1 || H < 1 || W > 8192 || H > 8192) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"gen-texture: invalid size %dx%d (must be 1..8192)\n", W, H);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<uint8_t> pixels(static_cast<size_t>(W) * H * 3, 0);
|
|
|
|
|
|
std::string lower = spec;
|
|
|
|
|
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
|
|
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
|
if (lower == "checker") {
|
|
|
|
|
|
for (int y = 0; y < H; ++y) {
|
|
|
|
|
|
for (int x = 0; x < W; ++x) {
|
|
|
|
|
|
bool dark = ((x / 32) + (y / 32)) & 1;
|
|
|
|
|
|
uint8_t v = dark ? 16 : 240;
|
|
|
|
|
|
size_t i2 = (static_cast<size_t>(y) * W + x) * 3;
|
|
|
|
|
|
pixels[i2 + 0] = v;
|
|
|
|
|
|
pixels[i2 + 1] = v;
|
|
|
|
|
|
pixels[i2 + 2] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (lower == "grid") {
|
|
|
|
|
|
for (int y = 0; y < H; ++y) {
|
|
|
|
|
|
for (int x = 0; x < W; ++x) {
|
|
|
|
|
|
bool line = (x % 16 == 0) || (y % 16 == 0);
|
|
|
|
|
|
uint8_t v = line ? 240 : 32;
|
|
|
|
|
|
size_t i2 = (static_cast<size_t>(y) * W + x) * 3;
|
|
|
|
|
|
pixels[i2 + 0] = v;
|
|
|
|
|
|
pixels[i2 + 1] = v;
|
|
|
|
|
|
pixels[i2 + 2] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Hex color. Accept "RGB" (3 chars) or "RRGGBB" (6 chars),
|
|
|
|
|
|
// optional leading '#'.
|
|
|
|
|
|
std::string hex = lower;
|
|
|
|
|
|
if (!hex.empty() && hex[0] == '#') hex.erase(0, 1);
|
|
|
|
|
|
auto fromHex = [](char c) -> int {
|
|
|
|
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
|
|
|
|
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
};
|
|
|
|
|
|
uint8_t r = 0, g = 0, b = 0;
|
|
|
|
|
|
if (hex.size() == 6) {
|
|
|
|
|
|
int hi, lo;
|
|
|
|
|
|
if ((hi = fromHex(hex[0])) < 0) goto bad_color;
|
|
|
|
|
|
if ((lo = fromHex(hex[1])) < 0) goto bad_color;
|
|
|
|
|
|
r = static_cast<uint8_t>((hi << 4) | lo);
|
|
|
|
|
|
if ((hi = fromHex(hex[2])) < 0) goto bad_color;
|
|
|
|
|
|
if ((lo = fromHex(hex[3])) < 0) goto bad_color;
|
|
|
|
|
|
g = static_cast<uint8_t>((hi << 4) | lo);
|
|
|
|
|
|
if ((hi = fromHex(hex[4])) < 0) goto bad_color;
|
|
|
|
|
|
if ((lo = fromHex(hex[5])) < 0) goto bad_color;
|
|
|
|
|
|
b = static_cast<uint8_t>((hi << 4) | lo);
|
|
|
|
|
|
} else if (hex.size() == 3) {
|
|
|
|
|
|
int v0, v1, v2;
|
|
|
|
|
|
if ((v0 = fromHex(hex[0])) < 0) goto bad_color;
|
|
|
|
|
|
if ((v1 = fromHex(hex[1])) < 0) goto bad_color;
|
|
|
|
|
|
if ((v2 = fromHex(hex[2])) < 0) goto bad_color;
|
|
|
|
|
|
r = static_cast<uint8_t>((v0 << 4) | v0);
|
|
|
|
|
|
g = static_cast<uint8_t>((v1 << 4) | v1);
|
|
|
|
|
|
b = static_cast<uint8_t>((v2 << 4) | v2);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
goto bad_color;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int y = 0; y < H; ++y) {
|
|
|
|
|
|
for (int x = 0; x < W; ++x) {
|
|
|
|
|
|
size_t i2 = (static_cast<size_t>(y) * W + x) * 3;
|
|
|
|
|
|
pixels[i2 + 0] = r;
|
|
|
|
|
|
pixels[i2 + 1] = g;
|
|
|
|
|
|
pixels[i2 + 2] = b;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
goto color_ok;
|
|
|
|
|
|
bad_color:
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"gen-texture: '%s' is not a valid hex color or 'checker'/'grid'\n",
|
|
|
|
|
|
spec.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
color_ok: ;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!stbi_write_png(outPath.c_str(), W, H, 3,
|
|
|
|
|
|
pixels.data(), W * 3)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"gen-texture: stbi_write_png failed for %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Wrote %s\n", outPath.c_str());
|
|
|
|
|
|
std::printf(" size : %dx%d\n", W, H);
|
|
|
|
|
|
std::printf(" spec : %s\n", spec.c_str());
|
|
|
|
|
|
return 0;
|
2026-05-07 04:00:21 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--add-texture-to-zone") == 0 && i + 2 < argc) {
|
|
|
|
|
|
// Import an existing PNG into a zone directory. Useful
|
|
|
|
|
|
// for the "I have an artist-painted texture, get it into
|
|
|
|
|
|
// my project" workflow — complements --gen-texture
|
|
|
|
|
|
// (procedural placeholder) and --convert-blp-png (legacy
|
|
|
|
|
|
// BLP migration).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Optional <renameTo> argument lets the user store the
|
|
|
|
|
|
// PNG under a project-specific name (e.g., a generic
|
|
|
|
|
|
// "stone.png" downloaded from a tileset becomes
|
|
|
|
|
|
// "courtyard_floor.png" in the zone).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Refuses to overwrite an existing destination unless the
|
|
|
|
|
|
// source and destination are byte-identical (idempotent
|
|
|
|
|
|
// re-runs are safe).
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
std::string srcPng = argv[++i];
|
|
|
|
|
|
std::string renameTo;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') renameTo = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir) || !fs::is_directory(zoneDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"add-texture-to-zone: %s is not a directory\n",
|
|
|
|
|
|
zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!fs::exists(srcPng) || !fs::is_regular_file(srcPng)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"add-texture-to-zone: %s is not a file\n",
|
|
|
|
|
|
srcPng.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Sanity-check: must end in .png (any case) so users
|
|
|
|
|
|
// don't accidentally drop a .blp/.tga and get surprised
|
|
|
|
|
|
// when nothing renders.
|
|
|
|
|
|
std::string srcExt = fs::path(srcPng).extension().string();
|
|
|
|
|
|
std::transform(srcExt.begin(), srcExt.end(), srcExt.begin(),
|
|
|
|
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
|
if (srcExt != ".png") {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"add-texture-to-zone: %s is not a .png "
|
|
|
|
|
|
"(use --convert-blp-png for .blp first)\n",
|
|
|
|
|
|
srcPng.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string destLeaf = renameTo.empty()
|
|
|
|
|
|
? fs::path(srcPng).filename().string()
|
|
|
|
|
|
: renameTo;
|
|
|
|
|
|
// If the rename arg lacks an extension, append .png so
|
|
|
|
|
|
// common typos ("stone" -> "stone.png") just work.
|
|
|
|
|
|
if (fs::path(destLeaf).extension().string().empty()) {
|
|
|
|
|
|
destLeaf += ".png";
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string destPath = zoneDir + "/" + destLeaf;
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
if (fs::exists(destPath)) {
|
|
|
|
|
|
// Allow re-running if the bytes already match — makes
|
|
|
|
|
|
// makefile-driven workflows idempotent.
|
|
|
|
|
|
if (fs::file_size(srcPng, ec) == fs::file_size(destPath, ec)) {
|
|
|
|
|
|
std::ifstream a(srcPng, std::ios::binary);
|
|
|
|
|
|
std::ifstream b(destPath, std::ios::binary);
|
|
|
|
|
|
std::stringstream sa, sb;
|
|
|
|
|
|
sa << a.rdbuf(); sb << b.rdbuf();
|
|
|
|
|
|
if (sa.str() == sb.str()) {
|
|
|
|
|
|
std::printf("Already present: %s (no-op)\n",
|
|
|
|
|
|
destPath.c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"add-texture-to-zone: %s already exists with different "
|
|
|
|
|
|
"content (delete it first if intentional)\n",
|
|
|
|
|
|
destPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
fs::copy_file(srcPng, destPath, ec);
|
|
|
|
|
|
if (ec) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"add-texture-to-zone: copy failed (%s)\n",
|
|
|
|
|
|
ec.message().c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint64_t bytes = fs::file_size(destPath, ec);
|
|
|
|
|
|
std::printf("Imported %s -> %s\n",
|
|
|
|
|
|
srcPng.c_str(), destPath.c_str());
|
|
|
|
|
|
std::printf(" bytes : %llu\n",
|
|
|
|
|
|
static_cast<unsigned long long>(bytes));
|
|
|
|
|
|
std::printf(" next : --add-texture-to-mesh <wom-base> %s\n",
|
|
|
|
|
|
destPath.c_str());
|
|
|
|
|
|
return 0;
|
2026-05-06 15:01:32 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-tilemap") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Visualize the WoW 64x64 ADT grid showing which tiles are
|
|
|
|
|
|
// claimed by which zones across a project. Useful for
|
|
|
|
|
|
// spotting tile-coord collisions before two zones try to
|
|
|
|
|
|
// ship overlapping content, and for getting a 'where am I
|
|
|
|
|
|
// working?' overview of a multi-zone project.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"info-tilemap: %s is not a directory\n", projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Map (tx, ty) -> vector<zone names> so collision overlaps
|
|
|
|
|
|
// are visible. Walk every zone in the project.
|
|
|
|
|
|
std::map<std::pair<int,int>, std::vector<std::string>> claims;
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load((entry.path() / "zone.json").string())) continue;
|
|
|
|
|
|
std::string zname = zm.mapName.empty()
|
|
|
|
|
|
? entry.path().filename().string() : zm.mapName;
|
|
|
|
|
|
zones.push_back(zname);
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
if (tx >= 0 && tx < 64 && ty >= 0 && ty < 64) {
|
|
|
|
|
|
claims[{tx, ty}].push_back(zname);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Per-zone label glyph: first letter of the zone name,
|
|
|
|
|
|
// uppercased so different zones get distinct chars in the
|
|
|
|
|
|
// grid. Multi-letter overlap collapses to '*'.
|
|
|
|
|
|
std::map<std::string, char> zoneGlyph;
|
|
|
|
|
|
char nextGlyph = 'A';
|
|
|
|
|
|
for (const auto& z : zones) {
|
|
|
|
|
|
if (zoneGlyph.count(z)) continue;
|
|
|
|
|
|
if (!z.empty() && std::isalpha(static_cast<unsigned char>(z[0]))) {
|
|
|
|
|
|
zoneGlyph[z] = static_cast<char>(std::toupper(static_cast<unsigned char>(z[0])));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
zoneGlyph[z] = nextGlyph++;
|
|
|
|
|
|
if (nextGlyph > 'Z') nextGlyph = 'a';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
int collisions = 0;
|
|
|
|
|
|
for (const auto& [coord, owners] : claims) {
|
|
|
|
|
|
if (owners.size() > 1) collisions++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["projectDir"] = projectDir;
|
|
|
|
|
|
j["zoneCount"] = zones.size();
|
|
|
|
|
|
j["claimedTiles"] = claims.size();
|
|
|
|
|
|
j["collisions"] = collisions;
|
|
|
|
|
|
nlohmann::json claimsJson = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& [coord, owners] : claims) {
|
|
|
|
|
|
claimsJson.push_back({{"x", coord.first},
|
|
|
|
|
|
{"y", coord.second},
|
|
|
|
|
|
{"zones", owners}});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["claims"] = claimsJson;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return collisions == 0 ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Tilemap: %s\n", projectDir.c_str());
|
|
|
|
|
|
std::printf(" zones : %zu\n", zones.size());
|
|
|
|
|
|
std::printf(" tiles used : %zu\n", claims.size());
|
|
|
|
|
|
std::printf(" collisions : %d (multiple zones claiming same tile)\n",
|
|
|
|
|
|
collisions);
|
|
|
|
|
|
std::printf(" legend :");
|
|
|
|
|
|
for (const auto& [name, glyph] : zoneGlyph) {
|
|
|
|
|
|
std::printf(" %c=%s", glyph, name.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n\n");
|
|
|
|
|
|
// Render 64x64 grid. Print column header in groups of 10
|
|
|
|
|
|
// for readability.
|
|
|
|
|
|
std::printf(" ");
|
|
|
|
|
|
for (int x = 0; x < 64; ++x) {
|
|
|
|
|
|
std::printf("%c", (x % 10 == 0) ? '0' + (x / 10) : ' ');
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n");
|
|
|
|
|
|
std::printf(" ");
|
|
|
|
|
|
for (int x = 0; x < 64; ++x) std::printf("%d", x % 10);
|
|
|
|
|
|
std::printf("\n");
|
|
|
|
|
|
for (int y = 0; y < 64; ++y) {
|
|
|
|
|
|
// Skip rows that have no tiles claimed — keeps the
|
|
|
|
|
|
// output bounded for projects in one corner of the map.
|
|
|
|
|
|
bool rowHasContent = false;
|
|
|
|
|
|
for (int x = 0; x < 64 && !rowHasContent; ++x) {
|
|
|
|
|
|
if (claims.count({x, y})) rowHasContent = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!rowHasContent) continue;
|
|
|
|
|
|
std::printf(" y=%2d ", y);
|
|
|
|
|
|
for (int x = 0; x < 64; ++x) {
|
|
|
|
|
|
auto it = claims.find({x, y});
|
|
|
|
|
|
if (it == claims.end()) {
|
|
|
|
|
|
std::printf(".");
|
|
|
|
|
|
} else if (it->second.size() > 1) {
|
|
|
|
|
|
std::printf("*"); // collision
|
|
|
|
|
|
} else {
|
|
|
|
|
|
std::printf("%c", zoneGlyph[it->second[0]]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (collisions > 0) {
|
|
|
|
|
|
std::printf("\n COLLISIONS:\n");
|
|
|
|
|
|
for (const auto& [coord, owners] : claims) {
|
|
|
|
|
|
if (owners.size() < 2) continue;
|
|
|
|
|
|
std::printf(" (%d, %d) claimed by:", coord.first, coord.second);
|
|
|
|
|
|
for (const auto& o : owners) std::printf(" %s", o.c_str());
|
|
|
|
|
|
std::printf("\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return collisions == 0 ? 0 : 1;
|
2026-05-06 13:24:27 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--list-zone-deps") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Enumerate every external model path a zone references —
|
|
|
|
|
|
// both directly placed (objects.json) and indirectly via
|
|
|
|
|
|
// doodad placements inside any WOB sitting next to the
|
|
|
|
|
|
// zone manifest. Useful when packaging a content pack to
|
|
|
|
|
|
// confirm every needed asset will ship.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir + "/zone.json")) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"list-zone-deps: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Collect with usage counts so duplicates report '×N' instead
|
|
|
|
|
|
// of cluttering the table.
|
|
|
|
|
|
std::map<std::string, int> directM2; // m2 placements
|
|
|
|
|
|
std::map<std::string, int> directWMO; // wmo placements
|
|
|
|
|
|
std::map<std::string, int> doodadM2; // m2s referenced inside WOBs
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (const auto& o : op.getObjects()) {
|
|
|
|
|
|
if (o.type == wowee::editor::PlaceableType::M2) directM2[o.path]++;
|
|
|
|
|
|
else if (o.type == wowee::editor::PlaceableType::WMO) directWMO[o.path]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Walk WOBs in the zone directory recursively and pull in
|
|
|
|
|
|
// their doodad model paths. Sub-dirs caught too in case the
|
|
|
|
|
|
// user organizes buildings under a buildings/ subfolder.
|
|
|
|
|
|
int wobCount = 0;
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
std::string ext = e.path().extension().string();
|
|
|
|
|
|
if (ext != ".wob") continue;
|
|
|
|
|
|
wobCount++;
|
|
|
|
|
|
std::string base = e.path().string();
|
|
|
|
|
|
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
|
|
|
|
|
for (const auto& d : bld.doodads) {
|
|
|
|
|
|
if (d.modelPath.empty()) continue;
|
|
|
|
|
|
doodadM2[d.modelPath]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// For each direct WMO placement, also recurse into the WOB
|
|
|
|
|
|
// sitting at that path (relative to the zone) so transitive
|
|
|
|
|
|
// doodad deps surface — this matches the runtime's actual
|
|
|
|
|
|
// load chain.
|
|
|
|
|
|
for (const auto& [path, count] : directWMO) {
|
|
|
|
|
|
// Strip extension since loader takes a base path.
|
|
|
|
|
|
std::string base = path;
|
|
|
|
|
|
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wmo")
|
|
|
|
|
|
base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
// Try relative-to-zone first, then absolute.
|
|
|
|
|
|
std::string trial = zoneDir + "/" + base;
|
|
|
|
|
|
if (!wowee::pipeline::WoweeBuildingLoader::exists(trial)) trial = base;
|
|
|
|
|
|
if (!wowee::pipeline::WoweeBuildingLoader::exists(trial)) continue;
|
|
|
|
|
|
auto bld = wowee::pipeline::WoweeBuildingLoader::load(trial);
|
|
|
|
|
|
for (const auto& d : bld.doodads) {
|
|
|
|
|
|
if (d.modelPath.empty()) continue;
|
|
|
|
|
|
doodadM2[d.modelPath]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
size_t totalUnique = directM2.size() + directWMO.size() + doodadM2.size();
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["zone"] = zoneDir;
|
|
|
|
|
|
j["wobCount"] = wobCount;
|
|
|
|
|
|
j["totalUnique"] = totalUnique;
|
|
|
|
|
|
auto toArr = [](const std::map<std::string, int>& m) {
|
|
|
|
|
|
nlohmann::json a = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& [path, count] : m) {
|
|
|
|
|
|
a.push_back({{"path", path}, {"count", count}});
|
|
|
|
|
|
}
|
|
|
|
|
|
return a;
|
|
|
|
|
|
};
|
|
|
|
|
|
j["directM2"] = toArr(directM2);
|
|
|
|
|
|
j["directWMO"] = toArr(directWMO);
|
|
|
|
|
|
j["doodadM2"] = toArr(doodadM2);
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Zone deps: %s\n", zoneDir.c_str());
|
|
|
|
|
|
std::printf(" WOBs scanned : %d\n", wobCount);
|
|
|
|
|
|
std::printf(" unique paths total : %zu\n", totalUnique);
|
|
|
|
|
|
auto emit = [](const char* tag, const std::map<std::string, int>& m) {
|
|
|
|
|
|
std::printf("\n %s (%zu unique):\n", tag, m.size());
|
|
|
|
|
|
if (m.empty()) {
|
|
|
|
|
|
std::printf(" *none*\n");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const auto& [path, count] : m) {
|
|
|
|
|
|
if (count > 1) std::printf(" %s ×%d\n", path.c_str(), count);
|
|
|
|
|
|
else std::printf(" %s\n", path.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
emit("Direct M2 placements", directM2);
|
|
|
|
|
|
emit("Direct WMO placements", directWMO);
|
|
|
|
|
|
emit("WOB doodad M2 refs", doodadM2);
|
|
|
|
|
|
return 0;
|
2026-05-06 22:30:10 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--list-project-orphans") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Inverse of --list-zone-deps. Walks every zone in
|
|
|
|
|
|
// <projectDir>, collects the set of .wom/.wob files
|
|
|
|
|
|
// sitting on disk and the set of paths actually
|
|
|
|
|
|
// referenced by objects.json placements + WOB doodad
|
|
|
|
|
|
// lists. Files in the first set but not the second are
|
|
|
|
|
|
// orphans — candidates for removal before --pack-wcp so
|
|
|
|
|
|
// the archive doesn't carry dead weight.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Comparison is by basename (extension stripped) since
|
|
|
|
|
|
// the reference paths sometimes include the extension and
|
|
|
|
|
|
// sometimes don't.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"list-project-orphans: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zones.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zones.begin(), zones.end());
|
|
|
|
|
|
// Project-wide reference set. Normalize by stripping
|
|
|
|
|
|
// extension and any leading "./".
|
|
|
|
|
|
auto normalize = [](std::string p) {
|
|
|
|
|
|
while (p.size() >= 2 && p[0] == '.' && p[1] == '/') p.erase(0, 2);
|
|
|
|
|
|
std::string ext = fs::path(p).extension().string();
|
|
|
|
|
|
if (ext == ".wom" || ext == ".wob" || ext == ".m2" || ext == ".wmo") {
|
|
|
|
|
|
p = p.substr(0, p.size() - ext.size());
|
|
|
|
|
|
}
|
|
|
|
|
|
return p;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::set<std::string> referencedBases; // normalized basenames
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (const auto& o : op.getObjects()) {
|
|
|
|
|
|
if (o.path.empty()) continue;
|
|
|
|
|
|
// Reference can be relative to zone or just a
|
|
|
|
|
|
// bare model name; record both forms for the
|
|
|
|
|
|
// membership test.
|
|
|
|
|
|
std::string norm = normalize(o.path);
|
|
|
|
|
|
referencedBases.insert(norm);
|
|
|
|
|
|
// Also try the leaf basename so unqualified
|
|
|
|
|
|
// refs match.
|
|
|
|
|
|
referencedBases.insert(fs::path(norm).filename().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
if (e.path().extension() != ".wob") continue;
|
|
|
|
|
|
std::string base = e.path().string();
|
|
|
|
|
|
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
|
|
|
|
|
for (const auto& d : bld.doodads) {
|
|
|
|
|
|
if (d.modelPath.empty()) continue;
|
|
|
|
|
|
std::string norm = normalize(d.modelPath);
|
|
|
|
|
|
referencedBases.insert(norm);
|
|
|
|
|
|
referencedBases.insert(fs::path(norm).filename().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Now walk every zone again and flag orphan .wom/.wob files.
|
|
|
|
|
|
struct Orphan { std::string zone, path; uint64_t bytes; };
|
|
|
|
|
|
std::vector<Orphan> orphans;
|
|
|
|
|
|
uint64_t totalOrphanBytes = 0;
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
std::string zoneName = fs::path(zoneDir).filename().string();
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
std::string ext = e.path().extension().string();
|
|
|
|
|
|
if (ext != ".wom" && ext != ".wob") continue;
|
|
|
|
|
|
std::string rel = fs::relative(e.path(), zoneDir, ec).string();
|
|
|
|
|
|
if (ec) rel = e.path().filename().string();
|
|
|
|
|
|
std::string normRel = rel.substr(0, rel.size() - ext.size());
|
|
|
|
|
|
std::string leaf = e.path().stem().string();
|
|
|
|
|
|
if (referencedBases.count(normRel) ||
|
|
|
|
|
|
referencedBases.count(leaf)) {
|
|
|
|
|
|
continue; // referenced, not orphan
|
|
|
|
|
|
}
|
|
|
|
|
|
uint64_t sz = e.file_size(ec);
|
|
|
|
|
|
if (ec) sz = 0;
|
|
|
|
|
|
orphans.push_back({zoneName, rel, sz});
|
|
|
|
|
|
totalOrphanBytes += sz;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(orphans.begin(), orphans.end(),
|
|
|
|
|
|
[](const Orphan& a, const Orphan& b) {
|
|
|
|
|
|
if (a.zone != b.zone) return a.zone < b.zone;
|
|
|
|
|
|
return a.path < b.path;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["project"] = projectDir;
|
|
|
|
|
|
j["referencedCount"] = referencedBases.size();
|
|
|
|
|
|
j["orphanCount"] = orphans.size();
|
|
|
|
|
|
j["orphanBytes"] = totalOrphanBytes;
|
|
|
|
|
|
nlohmann::json arr = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& o : orphans) {
|
|
|
|
|
|
arr.push_back({{"zone", o.zone},
|
|
|
|
|
|
{"path", o.path},
|
|
|
|
|
|
{"bytes", o.bytes}});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["orphans"] = arr;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Project orphans: %s\n", projectDir.c_str());
|
|
|
|
|
|
std::printf(" zones scanned : %zu\n", zones.size());
|
|
|
|
|
|
std::printf(" refs collected : %zu (normalized basenames)\n",
|
|
|
|
|
|
referencedBases.size());
|
|
|
|
|
|
std::printf(" orphan .wom/.wob : %zu file(s), %.1f KB\n",
|
|
|
|
|
|
orphans.size(), totalOrphanBytes / 1024.0);
|
|
|
|
|
|
if (orphans.empty()) {
|
|
|
|
|
|
std::printf("\n (no orphans — every model file is referenced)\n");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n zone bytes path\n");
|
|
|
|
|
|
for (const auto& o : orphans) {
|
|
|
|
|
|
std::printf(" %-20s %8llu %s\n",
|
|
|
|
|
|
o.zone.substr(0, 20).c_str(),
|
|
|
|
|
|
static_cast<unsigned long long>(o.bytes),
|
|
|
|
|
|
o.path.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
2026-05-06 22:42:29 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--remove-project-orphans") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Destructive companion to --list-project-orphans. Reuses
|
|
|
|
|
|
// the same reference-collection + orphan-detection logic
|
|
|
|
|
|
// and then deletes the resulting files. --dry-run shows
|
|
|
|
|
|
// what would be removed without touching anything.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
bool dryRun = false;
|
|
|
|
|
|
if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) {
|
|
|
|
|
|
dryRun = true; i++;
|
|
|
|
|
|
}
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"remove-project-orphans: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zones.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zones.begin(), zones.end());
|
|
|
|
|
|
// Same normalize + reference collection as --list-project-orphans.
|
|
|
|
|
|
// Keep both functions in sync if the matching rules evolve.
|
|
|
|
|
|
auto normalize = [](std::string p) {
|
|
|
|
|
|
while (p.size() >= 2 && p[0] == '.' && p[1] == '/') p.erase(0, 2);
|
|
|
|
|
|
std::string ext = fs::path(p).extension().string();
|
|
|
|
|
|
if (ext == ".wom" || ext == ".wob" || ext == ".m2" || ext == ".wmo") {
|
|
|
|
|
|
p = p.substr(0, p.size() - ext.size());
|
|
|
|
|
|
}
|
|
|
|
|
|
return p;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::set<std::string> referencedBases;
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (const auto& o : op.getObjects()) {
|
|
|
|
|
|
if (o.path.empty()) continue;
|
|
|
|
|
|
std::string norm = normalize(o.path);
|
|
|
|
|
|
referencedBases.insert(norm);
|
|
|
|
|
|
referencedBases.insert(fs::path(norm).filename().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
if (e.path().extension() != ".wob") continue;
|
|
|
|
|
|
std::string base = e.path().string();
|
|
|
|
|
|
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
|
|
|
|
|
for (const auto& d : bld.doodads) {
|
|
|
|
|
|
if (d.modelPath.empty()) continue;
|
|
|
|
|
|
std::string norm = normalize(d.modelPath);
|
|
|
|
|
|
referencedBases.insert(norm);
|
|
|
|
|
|
referencedBases.insert(fs::path(norm).filename().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
int removed = 0, failed = 0;
|
|
|
|
|
|
uint64_t freedBytes = 0;
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
std::string zoneName = fs::path(zoneDir).filename().string();
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
std::vector<fs::path> toRemove;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file()) continue;
|
|
|
|
|
|
std::string ext = e.path().extension().string();
|
|
|
|
|
|
if (ext != ".wom" && ext != ".wob") continue;
|
|
|
|
|
|
std::string rel = fs::relative(e.path(), zoneDir, ec).string();
|
|
|
|
|
|
if (ec) rel = e.path().filename().string();
|
|
|
|
|
|
std::string normRel = rel.substr(0, rel.size() - ext.size());
|
|
|
|
|
|
std::string leaf = e.path().stem().string();
|
|
|
|
|
|
if (referencedBases.count(normRel) ||
|
|
|
|
|
|
referencedBases.count(leaf)) continue;
|
|
|
|
|
|
toRemove.push_back(e.path());
|
|
|
|
|
|
}
|
|
|
|
|
|
// Materialize the deletion list before removing so we
|
|
|
|
|
|
// don't mutate the directory while iterating.
|
|
|
|
|
|
for (const auto& p : toRemove) {
|
|
|
|
|
|
uint64_t sz = fs::file_size(p, ec);
|
|
|
|
|
|
if (ec) sz = 0;
|
|
|
|
|
|
std::string rel = fs::relative(p, zoneDir, ec).string();
|
|
|
|
|
|
if (ec) rel = p.filename().string();
|
|
|
|
|
|
if (dryRun) {
|
|
|
|
|
|
std::printf(" would remove: %s/%s (%llu bytes)\n",
|
|
|
|
|
|
zoneName.c_str(), rel.c_str(),
|
|
|
|
|
|
static_cast<unsigned long long>(sz));
|
|
|
|
|
|
removed++;
|
|
|
|
|
|
freedBytes += sz;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (fs::remove(p, ec)) {
|
|
|
|
|
|
std::printf(" removed: %s/%s (%llu bytes)\n",
|
|
|
|
|
|
zoneName.c_str(), rel.c_str(),
|
|
|
|
|
|
static_cast<unsigned long long>(sz));
|
|
|
|
|
|
removed++;
|
|
|
|
|
|
freedBytes += sz;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
" WARN: failed to remove %s (%s)\n",
|
|
|
|
|
|
p.c_str(), ec.message().c_str());
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\nremove-project-orphans: %s%s\n",
|
|
|
|
|
|
projectDir.c_str(), dryRun ? " (dry-run)" : "");
|
|
|
|
|
|
std::printf(" zones : %zu\n", zones.size());
|
|
|
|
|
|
std::printf(" refs : %zu (normalized basenames)\n",
|
|
|
|
|
|
referencedBases.size());
|
|
|
|
|
|
std::printf(" %s : %d file(s)\n",
|
|
|
|
|
|
dryRun ? "would remove" : "removed ", removed);
|
|
|
|
|
|
std::printf(" freed : %.1f KB\n", freedBytes / 1024.0);
|
|
|
|
|
|
if (failed > 0) {
|
|
|
|
|
|
std::printf(" FAILED : %d (see stderr)\n", failed);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (dryRun && removed > 0) {
|
|
|
|
|
|
std::printf(" re-run without --dry-run to apply\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
return failed == 0 ? 0 : 1;
|
2026-05-06 14:44:53 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--export-zone-deps-md") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Markdown counterpart to --list-zone-deps. Writes a sortable
|
|
|
|
|
|
// GitHub-rendered table of every external model the zone
|
|
|
|
|
|
// references plus on-disk presence (so PR reviewers see at a
|
|
|
|
|
|
// glance whether dependencies are accounted for in the
|
|
|
|
|
|
// accompanying asset bundle).
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir + "/zone.json")) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"export-zone-deps-md: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
zm.load(zoneDir + "/zone.json");
|
|
|
|
|
|
if (outPath.empty()) outPath = zoneDir + "/DEPS.md";
|
|
|
|
|
|
// Same dep-collection pass as --list-zone-deps.
|
|
|
|
|
|
std::map<std::string, int> directM2;
|
|
|
|
|
|
std::map<std::string, int> directWMO;
|
|
|
|
|
|
std::map<std::string, int> doodadM2;
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (const auto& o : op.getObjects()) {
|
|
|
|
|
|
if (o.type == wowee::editor::PlaceableType::M2) directM2[o.path]++;
|
|
|
|
|
|
else if (o.type == wowee::editor::PlaceableType::WMO) directWMO[o.path]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
int wobCount = 0;
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
|
|
|
|
|
if (!e.is_regular_file() ||
|
|
|
|
|
|
e.path().extension() != ".wob") continue;
|
|
|
|
|
|
wobCount++;
|
|
|
|
|
|
std::string base = e.path().string();
|
|
|
|
|
|
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
|
|
|
|
|
for (const auto& d : bld.doodads) {
|
|
|
|
|
|
if (!d.modelPath.empty()) doodadM2[d.modelPath]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Resolve dep on disk. Same heuristic as --check-zone-refs:
|
|
|
|
|
|
// try both open + proprietary in conventional roots.
|
|
|
|
|
|
auto stripExt = [](const std::string& p, const char* ext) {
|
|
|
|
|
|
size_t n = std::strlen(ext);
|
|
|
|
|
|
if (p.size() >= n) {
|
|
|
|
|
|
std::string tail = p.substr(p.size() - n);
|
|
|
|
|
|
std::string lower = tail;
|
|
|
|
|
|
for (auto& c : lower) c = std::tolower(static_cast<unsigned char>(c));
|
|
|
|
|
|
if (lower == ext) return p.substr(0, p.size() - n);
|
|
|
|
|
|
}
|
|
|
|
|
|
return p;
|
|
|
|
|
|
};
|
|
|
|
|
|
auto resolveStatus = [&](const std::string& path, bool isWMO) {
|
|
|
|
|
|
std::string base, openExt, propExt;
|
|
|
|
|
|
if (isWMO) {
|
|
|
|
|
|
base = stripExt(path, ".wmo");
|
|
|
|
|
|
openExt = ".wob"; propExt = ".wmo";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
base = stripExt(path, ".m2");
|
|
|
|
|
|
openExt = ".wom"; propExt = ".m2";
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> roots = {
|
|
|
|
|
|
"", zoneDir + "/", "output/", "custom_zones/", "Data/"
|
|
|
|
|
|
};
|
|
|
|
|
|
bool hasOpen = false, hasProp = false;
|
|
|
|
|
|
for (const auto& root : roots) {
|
|
|
|
|
|
if (fs::exists(root + base + openExt)) hasOpen = true;
|
|
|
|
|
|
if (fs::exists(root + base + propExt)) hasProp = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hasOpen && hasProp) return "open + proprietary";
|
|
|
|
|
|
if (hasOpen) return "open only";
|
|
|
|
|
|
if (hasProp) return "proprietary only";
|
|
|
|
|
|
return "MISSING";
|
|
|
|
|
|
};
|
|
|
|
|
|
std::ofstream out(outPath);
|
|
|
|
|
|
if (!out) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"export-zone-deps-md: cannot write %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
out << "# Dependencies — " <<
|
|
|
|
|
|
(zm.displayName.empty() ? zm.mapName : zm.displayName) << "\n\n";
|
|
|
|
|
|
out << "*Auto-generated by `wowee_editor --export-zone-deps-md`. "
|
|
|
|
|
|
"Status is best-effort — checks zone-local, output/, "
|
|
|
|
|
|
"custom_zones/, Data/ roots in that order.*\n\n";
|
|
|
|
|
|
auto emitTable = [&](const char* heading,
|
|
|
|
|
|
const std::map<std::string,int>& m,
|
|
|
|
|
|
bool isWMO) {
|
|
|
|
|
|
out << "## " << heading << " (" << m.size() << ")\n\n";
|
|
|
|
|
|
if (m.empty()) {
|
|
|
|
|
|
out << "*None.*\n\n";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
out << "| Refs | Path | Status |\n";
|
|
|
|
|
|
out << "|---:|---|---|\n";
|
|
|
|
|
|
for (const auto& [path, count] : m) {
|
|
|
|
|
|
out << "| " << count << " | `" << path << "` | "
|
|
|
|
|
|
<< resolveStatus(path, isWMO) << " |\n";
|
|
|
|
|
|
}
|
|
|
|
|
|
out << "\n";
|
|
|
|
|
|
};
|
|
|
|
|
|
emitTable("Direct M2 placements", directM2, false);
|
|
|
|
|
|
emitTable("Direct WMO placements", directWMO, true);
|
|
|
|
|
|
emitTable("WOB doodad M2 refs", doodadM2, false);
|
|
|
|
|
|
out << "## Summary\n\n";
|
|
|
|
|
|
out << "- Zone: `" << zm.mapName << "`\n";
|
|
|
|
|
|
out << "- WOBs scanned: " << wobCount << "\n";
|
|
|
|
|
|
out << "- Unique dependencies: " <<
|
|
|
|
|
|
directM2.size() + directWMO.size() + doodadM2.size() << "\n";
|
|
|
|
|
|
out.close();
|
|
|
|
|
|
std::printf("Wrote %s\n", outPath.c_str());
|
|
|
|
|
|
std::printf(" %zu M2 placements, %zu WMO placements, %zu WOB doodad refs\n",
|
|
|
|
|
|
directM2.size(), directWMO.size(), doodadM2.size());
|
|
|
|
|
|
return 0;
|
2026-05-06 18:59:44 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--export-zone-spawn-png") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Top-down PNG of spawn positions colored by type. Bound by
|
|
|
|
|
|
// the zone's tile range so the image is properly framed.
|
|
|
|
|
|
// Useful for design review (does the spawn distribution
|
|
|
|
|
|
// match the intended encounter design?) and for showing
|
|
|
|
|
|
// collaborators 'where are the mobs'.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
std::string manifestPath = zoneDir + "/zone.json";
|
|
|
|
|
|
if (!fs::exists(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"export-zone-spawn-png: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"export-zone-spawn-png: parse failed\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (zm.tiles.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "export-zone-spawn-png: zone has no tiles\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) outPath = zoneDir + "/" + zm.mapName + "_spawns.png";
|
|
|
|
|
|
// Compute world-space bounds from manifest tiles. Same math
|
|
|
|
|
|
// as --info-zone-extents.
|
|
|
|
|
|
constexpr float kTileSize = 533.33333f;
|
|
|
|
|
|
int tileMinX = 64, tileMaxX = -1;
|
|
|
|
|
|
int tileMinY = 64, tileMaxY = -1;
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
tileMinX = std::min(tileMinX, tx);
|
|
|
|
|
|
tileMaxX = std::max(tileMaxX, tx);
|
|
|
|
|
|
tileMinY = std::min(tileMinY, ty);
|
|
|
|
|
|
tileMaxY = std::max(tileMaxY, ty);
|
|
|
|
|
|
}
|
|
|
|
|
|
float worldMinX = (32.0f - tileMaxY - 1) * kTileSize;
|
|
|
|
|
|
float worldMaxX = (32.0f - tileMinY) * kTileSize;
|
|
|
|
|
|
float worldMinY = (32.0f - tileMaxX - 1) * kTileSize;
|
|
|
|
|
|
float worldMaxY = (32.0f - tileMinX) * kTileSize;
|
|
|
|
|
|
// Image dimensions: 256px per tile so detail is visible
|
|
|
|
|
|
// without inflating per-pixel cost.
|
|
|
|
|
|
int tilesX = tileMaxY - tileMinY + 1; // tile.y maps to world.x
|
|
|
|
|
|
int tilesY = tileMaxX - tileMinX + 1;
|
|
|
|
|
|
const int kPxPerTile = 256;
|
|
|
|
|
|
int imgW = tilesX * kPxPerTile;
|
|
|
|
|
|
int imgH = tilesY * kPxPerTile;
|
|
|
|
|
|
// Cap output size — 16-tile-wide projects shouldn't exceed
|
|
|
|
|
|
// 4096 wide. Scale down if needed.
|
|
|
|
|
|
int maxDim = std::max(imgW, imgH);
|
|
|
|
|
|
if (maxDim > 4096) {
|
|
|
|
|
|
int divisor = (maxDim + 4095) / 4096;
|
|
|
|
|
|
imgW = std::max(64, imgW / divisor);
|
|
|
|
|
|
imgH = std::max(64, imgH / divisor);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<uint8_t> img(imgW * imgH * 3, 32); // dark grey background
|
|
|
|
|
|
// Tile-grid lines so the boundary is visible.
|
|
|
|
|
|
for (int t = 1; t < tilesX; ++t) {
|
|
|
|
|
|
int x = (t * imgW) / tilesX;
|
|
|
|
|
|
if (x >= 0 && x < imgW) {
|
|
|
|
|
|
for (int y = 0; y < imgH; ++y) {
|
|
|
|
|
|
size_t off = (y * imgW + x) * 3;
|
|
|
|
|
|
img[off] = img[off+1] = img[off+2] = 64;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int t = 1; t < tilesY; ++t) {
|
|
|
|
|
|
int y = (t * imgH) / tilesY;
|
|
|
|
|
|
if (y >= 0 && y < imgH) {
|
|
|
|
|
|
for (int x = 0; x < imgW; ++x) {
|
|
|
|
|
|
size_t off = (y * imgW + x) * 3;
|
|
|
|
|
|
img[off] = img[off+1] = img[off+2] = 64;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Plot spawn points. Map world (X, Y) to image (px, py):
|
|
|
|
|
|
// px = (worldMaxX - X) / (worldMaxX - worldMinX) * imgW
|
|
|
|
|
|
// py = (worldMaxY - Y) / (worldMaxY - worldMinY) * imgH
|
|
|
|
|
|
// since +X world is north (up) and +Y world is west (left)
|
|
|
|
|
|
// in WoW coords.
|
|
|
|
|
|
float wRangeX = worldMaxX - worldMinX;
|
|
|
|
|
|
float wRangeY = worldMaxY - worldMinY;
|
|
|
|
|
|
auto plotPoint = [&](float wx, float wy, uint8_t r, uint8_t g, uint8_t b) {
|
|
|
|
|
|
if (wRangeX <= 0 || wRangeY <= 0) return;
|
|
|
|
|
|
int px = static_cast<int>((worldMaxX - wx) / wRangeX * imgW);
|
|
|
|
|
|
int py = static_cast<int>((worldMaxY - wy) / wRangeY * imgH);
|
|
|
|
|
|
// 3×3 dot.
|
|
|
|
|
|
for (int dy = -1; dy <= 1; ++dy) {
|
|
|
|
|
|
for (int dx = -1; dx <= 1; ++dx) {
|
|
|
|
|
|
int x = px + dx, y = py + dy;
|
|
|
|
|
|
if (x < 0 || x >= imgW || y < 0 || y >= imgH) continue;
|
|
|
|
|
|
size_t off = (y * imgW + x) * 3;
|
|
|
|
|
|
img[off] = r; img[off+1] = g; img[off+2] = b;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
// Creatures = red.
|
|
|
|
|
|
wowee::editor::NpcSpawner sp;
|
|
|
|
|
|
int creaturesPlotted = 0;
|
|
|
|
|
|
if (sp.loadFromFile(zoneDir + "/creatures.json")) {
|
|
|
|
|
|
for (const auto& s : sp.getSpawns()) {
|
|
|
|
|
|
plotPoint(s.position.x, s.position.y, 220, 60, 60);
|
|
|
|
|
|
creaturesPlotted++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Objects = green (M2) / blue (WMO).
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
int objectsPlotted = 0;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (const auto& o : op.getObjects()) {
|
|
|
|
|
|
if (o.type == wowee::editor::PlaceableType::M2) {
|
|
|
|
|
|
plotPoint(o.position.x, o.position.y, 60, 200, 60);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
plotPoint(o.position.x, o.position.y, 60, 120, 220);
|
|
|
|
|
|
}
|
|
|
|
|
|
objectsPlotted++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!stbi_write_png(outPath.c_str(), imgW, imgH, 3,
|
|
|
|
|
|
img.data(), imgW * 3)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"export-zone-spawn-png: stbi_write_png failed\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Wrote %s\n", outPath.c_str());
|
|
|
|
|
|
std::printf(" %dx%d px, tile grid %dx%d, %d creatures (red), %d objects (green/blue)\n",
|
|
|
|
|
|
imgW, imgH, tilesX, tilesY, creaturesPlotted, objectsPlotted);
|
|
|
|
|
|
return 0;
|
2026-05-06 13:27:18 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--check-zone-refs") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Cross-reference checker: every model path in objects.json
|
|
|
|
|
|
// must resolve as either an open WOM/WOB sidecar or a
|
|
|
|
|
|
// proprietary M2/WMO; every quest's giver/turnIn NPC ID must
|
|
|
|
|
|
// appear in creatures.json (when the zone has creatures).
|
|
|
|
|
|
// Catches dangling references that --validate doesn't, since
|
|
|
|
|
|
// --validate only checks open-format file presence.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir + "/zone.json")) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"check-zone-refs: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Try to find a model on disk in any of the conventional
|
|
|
|
|
|
// locations (zone-local, output/, custom_zones/, Data/).
|
|
|
|
|
|
// Strips extension and tries each open + proprietary variant.
|
|
|
|
|
|
auto stripExt = [](const std::string& p, const char* ext) {
|
|
|
|
|
|
size_t n = std::strlen(ext);
|
|
|
|
|
|
if (p.size() >= n) {
|
|
|
|
|
|
std::string tail = p.substr(p.size() - n);
|
|
|
|
|
|
std::string lower = tail;
|
|
|
|
|
|
for (auto& c : lower) c = std::tolower(static_cast<unsigned char>(c));
|
|
|
|
|
|
if (lower == ext) return p.substr(0, p.size() - n);
|
|
|
|
|
|
}
|
|
|
|
|
|
return p;
|
|
|
|
|
|
};
|
|
|
|
|
|
auto modelExists = [&](const std::string& path, bool isWMO) {
|
|
|
|
|
|
std::string base;
|
|
|
|
|
|
std::vector<std::string> exts;
|
|
|
|
|
|
if (isWMO) {
|
|
|
|
|
|
base = stripExt(path, ".wmo");
|
|
|
|
|
|
exts = {".wob", ".wmo"};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
base = stripExt(path, ".m2");
|
|
|
|
|
|
exts = {".wom", ".m2"};
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> roots = {
|
|
|
|
|
|
"", zoneDir + "/", "output/", "custom_zones/", "Data/"
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const auto& root : roots) {
|
|
|
|
|
|
for (const auto& ext : exts) {
|
|
|
|
|
|
if (fs::exists(root + base + ext)) return true;
|
|
|
|
|
|
// Case-fold fallback for case-sensitive filesystems
|
|
|
|
|
|
// (designers usually type Mixed Case but Linux
|
|
|
|
|
|
// stores asset paths lowercase after extraction).
|
|
|
|
|
|
std::string lower = base + ext;
|
|
|
|
|
|
for (auto& c : lower) c = std::tolower(static_cast<unsigned char>(c));
|
|
|
|
|
|
if (fs::exists(root + lower)) return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<std::string> errors;
|
|
|
|
|
|
// Object placements -> models on disk
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
int objectsChecked = 0, objectsMissing = 0;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (size_t k = 0; k < op.getObjects().size(); ++k) {
|
|
|
|
|
|
const auto& o = op.getObjects()[k];
|
|
|
|
|
|
objectsChecked++;
|
|
|
|
|
|
bool isWMO = (o.type == wowee::editor::PlaceableType::WMO);
|
|
|
|
|
|
if (!modelExists(o.path, isWMO)) {
|
|
|
|
|
|
objectsMissing++;
|
|
|
|
|
|
if (errors.size() < 30) {
|
|
|
|
|
|
errors.push_back("object[" + std::to_string(k) +
|
|
|
|
|
|
"] missing: " + o.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Quest NPCs -> creatures.json IDs (only when creatures exist;
|
|
|
|
|
|
// otherwise NPC IDs may legitimately reference upstream content
|
|
|
|
|
|
// outside the zone).
|
|
|
|
|
|
wowee::editor::NpcSpawner sp;
|
|
|
|
|
|
wowee::editor::QuestEditor qe;
|
|
|
|
|
|
int questsChecked = 0, questsMissing = 0;
|
|
|
|
|
|
bool hasCreatures = sp.loadFromFile(zoneDir + "/creatures.json");
|
|
|
|
|
|
std::unordered_set<uint32_t> creatureIds;
|
|
|
|
|
|
if (hasCreatures) {
|
|
|
|
|
|
for (const auto& s : sp.getSpawns()) creatureIds.insert(s.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (qe.loadFromFile(zoneDir + "/quests.json") && hasCreatures) {
|
|
|
|
|
|
for (size_t k = 0; k < qe.getQuests().size(); ++k) {
|
|
|
|
|
|
const auto& q = qe.getQuests()[k];
|
|
|
|
|
|
questsChecked++;
|
|
|
|
|
|
bool localGiver = (q.questGiverNpcId != 0 &&
|
|
|
|
|
|
creatureIds.count(q.questGiverNpcId) == 0);
|
|
|
|
|
|
bool localTurn = (q.turnInNpcId != 0 &&
|
|
|
|
|
|
q.turnInNpcId != q.questGiverNpcId &&
|
|
|
|
|
|
creatureIds.count(q.turnInNpcId) == 0);
|
|
|
|
|
|
// Only flag IDs that look 'small' (likely zone-local).
|
|
|
|
|
|
// Production uses 6-digit IDs that reference upstream
|
|
|
|
|
|
// content; designers wire those in deliberately.
|
|
|
|
|
|
if (localGiver && q.questGiverNpcId < 100000) {
|
|
|
|
|
|
questsMissing++;
|
|
|
|
|
|
if (errors.size() < 30) {
|
|
|
|
|
|
errors.push_back("quest[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
q.title + "' giver " +
|
|
|
|
|
|
std::to_string(q.questGiverNpcId) +
|
|
|
|
|
|
" not in creatures.json");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (localTurn && q.turnInNpcId < 100000) {
|
|
|
|
|
|
questsMissing++;
|
|
|
|
|
|
if (errors.size() < 30) {
|
|
|
|
|
|
errors.push_back("quest[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
q.title + "' turn-in " +
|
|
|
|
|
|
std::to_string(q.turnInNpcId) +
|
|
|
|
|
|
" not in creatures.json");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
int totalErrors = objectsMissing + questsMissing;
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["zone"] = zoneDir;
|
|
|
|
|
|
j["objectsChecked"] = objectsChecked;
|
|
|
|
|
|
j["objectsMissing"] = objectsMissing;
|
|
|
|
|
|
j["questsChecked"] = questsChecked;
|
|
|
|
|
|
j["questsMissing"] = questsMissing;
|
|
|
|
|
|
j["errors"] = errors;
|
|
|
|
|
|
j["passed"] = (totalErrors == 0);
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return totalErrors == 0 ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Zone refs: %s\n", zoneDir.c_str());
|
|
|
|
|
|
std::printf(" objects checked : %d (%d missing)\n",
|
|
|
|
|
|
objectsChecked, objectsMissing);
|
|
|
|
|
|
std::printf(" quests checked : %d (%d bad NPC refs)\n",
|
|
|
|
|
|
questsChecked, questsMissing);
|
|
|
|
|
|
if (totalErrors == 0) {
|
|
|
|
|
|
std::printf(" PASSED\n");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf(" FAILED — %d issue(s):\n", totalErrors);
|
|
|
|
|
|
for (const auto& e : errors) std::printf(" - %s\n", e.c_str());
|
|
|
|
|
|
return 1;
|
2026-05-06 15:31:12 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--check-zone-content") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Sanity-check creature/object/quest fields for plausible
|
|
|
|
|
|
// values. --check-zone-refs catches dangling references;
|
|
|
|
|
|
// this catches data-quality issues like creatures with 0 HP,
|
|
|
|
|
|
// objects with negative scale, quests with no objectives.
|
|
|
|
|
|
// Both are needed — a quest can have valid NPC IDs (refs OK)
|
|
|
|
|
|
// AND no objectives (content broken).
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(zoneDir + "/zone.json")) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"check-zone-content: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> warnings;
|
|
|
|
|
|
int creatureWarn = 0, objectWarn = 0, questWarn = 0;
|
|
|
|
|
|
// Creatures
|
|
|
|
|
|
wowee::editor::NpcSpawner sp;
|
|
|
|
|
|
if (sp.loadFromFile(zoneDir + "/creatures.json")) {
|
|
|
|
|
|
for (size_t k = 0; k < sp.spawnCount(); ++k) {
|
|
|
|
|
|
const auto& s = sp.getSpawns()[k];
|
|
|
|
|
|
if (s.name.empty()) {
|
|
|
|
|
|
warnings.push_back("creature[" + std::to_string(k) + "] has empty name");
|
|
|
|
|
|
creatureWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s.health == 0) {
|
|
|
|
|
|
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
s.name + "' has 0 health");
|
|
|
|
|
|
creatureWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s.level == 0) {
|
|
|
|
|
|
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
s.name + "' has level 0");
|
|
|
|
|
|
creatureWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s.minDamage > s.maxDamage) {
|
|
|
|
|
|
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
s.name + "' has minDamage > maxDamage");
|
|
|
|
|
|
creatureWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s.scale <= 0.0f || !std::isfinite(s.scale)) {
|
|
|
|
|
|
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
s.name + "' has non-positive or non-finite scale");
|
|
|
|
|
|
creatureWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s.displayId == 0) {
|
|
|
|
|
|
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
s.name + "' has displayId=0 (will render invisibly)");
|
|
|
|
|
|
creatureWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Objects
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (size_t k = 0; k < op.getObjects().size(); ++k) {
|
|
|
|
|
|
const auto& o = op.getObjects()[k];
|
|
|
|
|
|
if (o.path.empty()) {
|
|
|
|
|
|
warnings.push_back("object[" + std::to_string(k) + "] has empty path");
|
|
|
|
|
|
objectWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (o.scale <= 0.0f || !std::isfinite(o.scale)) {
|
|
|
|
|
|
warnings.push_back("object[" + std::to_string(k) +
|
|
|
|
|
|
"] has non-positive or non-finite scale");
|
|
|
|
|
|
objectWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!std::isfinite(o.position.x) ||
|
|
|
|
|
|
!std::isfinite(o.position.y) ||
|
|
|
|
|
|
!std::isfinite(o.position.z)) {
|
|
|
|
|
|
warnings.push_back("object[" + std::to_string(k) +
|
|
|
|
|
|
"] has non-finite position");
|
|
|
|
|
|
objectWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Quests
|
|
|
|
|
|
wowee::editor::QuestEditor qe;
|
|
|
|
|
|
if (qe.loadFromFile(zoneDir + "/quests.json")) {
|
|
|
|
|
|
for (size_t k = 0; k < qe.questCount(); ++k) {
|
|
|
|
|
|
const auto& q = qe.getQuests()[k];
|
|
|
|
|
|
if (q.title.empty()) {
|
|
|
|
|
|
warnings.push_back("quest[" + std::to_string(k) + "] has empty title");
|
|
|
|
|
|
questWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (q.objectives.empty()) {
|
|
|
|
|
|
warnings.push_back("quest[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
q.title + "' has no objectives (uncompletable)");
|
|
|
|
|
|
questWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (q.reward.xp == 0 && q.reward.itemRewards.empty() &&
|
|
|
|
|
|
q.reward.gold == 0 && q.reward.silver == 0 && q.reward.copper == 0) {
|
|
|
|
|
|
warnings.push_back("quest[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
q.title + "' has no reward at all");
|
|
|
|
|
|
questWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (q.requiredLevel == 0) {
|
|
|
|
|
|
warnings.push_back("quest[" + std::to_string(k) + "] '" +
|
|
|
|
|
|
q.title + "' has requiredLevel=0");
|
|
|
|
|
|
questWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
int total = creatureWarn + objectWarn + questWarn;
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["zone"] = zoneDir;
|
|
|
|
|
|
j["creatureWarnings"] = creatureWarn;
|
|
|
|
|
|
j["objectWarnings"] = objectWarn;
|
|
|
|
|
|
j["questWarnings"] = questWarn;
|
|
|
|
|
|
j["totalWarnings"] = total;
|
|
|
|
|
|
j["warnings"] = warnings;
|
|
|
|
|
|
j["passed"] = (total == 0);
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return total == 0 ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Zone content: %s\n", zoneDir.c_str());
|
|
|
|
|
|
std::printf(" creature warnings: %d\n", creatureWarn);
|
|
|
|
|
|
std::printf(" object warnings : %d\n", objectWarn);
|
|
|
|
|
|
std::printf(" quest warnings : %d\n", questWarn);
|
|
|
|
|
|
if (total == 0) {
|
|
|
|
|
|
std::printf(" PASSED\n");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf(" FAILED — %d total warning(s):\n", total);
|
|
|
|
|
|
for (const auto& w : warnings) std::printf(" - %s\n", w.c_str());
|
|
|
|
|
|
return 1;
|
2026-05-06 19:08:31 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--check-project-content") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Project-level content sanity check. Walks every zone and
|
|
|
|
|
|
// runs the same per-zone checks that --check-zone-content
|
|
|
|
|
|
// does, aggregating warnings per zone. Exit 1 if any zone
|
|
|
|
|
|
// has any warning. Designed for CI gates before --pack-wcp.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"check-project-content: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zones.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zones.begin(), zones.end());
|
|
|
|
|
|
// Same per-zone walks as --check-zone-content. Reuse the
|
|
|
|
|
|
// logic by counting issues directly here (cheaper than
|
|
|
|
|
|
// shelling out to a sub-invocation per zone).
|
|
|
|
|
|
struct ZoneRow { std::string name; int creatureWarn, objectWarn, questWarn; };
|
|
|
|
|
|
std::vector<ZoneRow> rows;
|
|
|
|
|
|
int projectFailedZones = 0;
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
ZoneRow row{fs::path(zoneDir).filename().string(), 0, 0, 0};
|
|
|
|
|
|
wowee::editor::NpcSpawner sp;
|
|
|
|
|
|
if (sp.loadFromFile(zoneDir + "/creatures.json")) {
|
|
|
|
|
|
for (const auto& s : sp.getSpawns()) {
|
|
|
|
|
|
if (s.name.empty()) row.creatureWarn++;
|
|
|
|
|
|
if (s.health == 0) row.creatureWarn++;
|
|
|
|
|
|
if (s.level == 0) row.creatureWarn++;
|
|
|
|
|
|
if (s.minDamage > s.maxDamage) row.creatureWarn++;
|
|
|
|
|
|
if (s.scale <= 0.0f || !std::isfinite(s.scale)) row.creatureWarn++;
|
|
|
|
|
|
if (s.displayId == 0) row.creatureWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (const auto& o : op.getObjects()) {
|
|
|
|
|
|
if (o.path.empty()) row.objectWarn++;
|
|
|
|
|
|
if (o.scale <= 0.0f || !std::isfinite(o.scale)) row.objectWarn++;
|
|
|
|
|
|
if (!std::isfinite(o.position.x) ||
|
|
|
|
|
|
!std::isfinite(o.position.y) ||
|
|
|
|
|
|
!std::isfinite(o.position.z)) row.objectWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::QuestEditor qe;
|
|
|
|
|
|
if (qe.loadFromFile(zoneDir + "/quests.json")) {
|
|
|
|
|
|
for (const auto& q : qe.getQuests()) {
|
|
|
|
|
|
if (q.title.empty()) row.questWarn++;
|
|
|
|
|
|
if (q.objectives.empty()) row.questWarn++;
|
|
|
|
|
|
if (q.reward.xp == 0 && q.reward.itemRewards.empty() &&
|
|
|
|
|
|
q.reward.gold == 0 && q.reward.silver == 0 &&
|
|
|
|
|
|
q.reward.copper == 0) row.questWarn++;
|
|
|
|
|
|
if (q.requiredLevel == 0) row.questWarn++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
int rowTotal = row.creatureWarn + row.objectWarn + row.questWarn;
|
|
|
|
|
|
if (rowTotal > 0) projectFailedZones++;
|
|
|
|
|
|
rows.push_back(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
int allPassed = (projectFailedZones == 0);
|
|
|
|
|
|
int totalWarn = 0;
|
|
|
|
|
|
for (const auto& r : rows) totalWarn += r.creatureWarn + r.objectWarn + r.questWarn;
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["projectDir"] = projectDir;
|
|
|
|
|
|
j["totalZones"] = zones.size();
|
|
|
|
|
|
j["failedZones"] = projectFailedZones;
|
|
|
|
|
|
j["totalWarnings"] = totalWarn;
|
|
|
|
|
|
j["passed"] = bool(allPassed);
|
|
|
|
|
|
nlohmann::json arr = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
arr.push_back({{"zone", r.name},
|
|
|
|
|
|
{"creatureWarn", r.creatureWarn},
|
|
|
|
|
|
{"objectWarn", r.objectWarn},
|
|
|
|
|
|
{"questWarn", r.questWarn}});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["zones"] = arr;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return allPassed ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("check-project-content: %s\n", projectDir.c_str());
|
|
|
|
|
|
std::printf(" zones : %zu (%d failed)\n",
|
|
|
|
|
|
zones.size(), projectFailedZones);
|
|
|
|
|
|
std::printf(" total warns : %d\n", totalWarn);
|
|
|
|
|
|
std::printf("\n zone creat object quest status\n");
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
int rowTotal = r.creatureWarn + r.objectWarn + r.questWarn;
|
|
|
|
|
|
std::printf(" %-26s %5d %5d %5d %s\n",
|
|
|
|
|
|
r.name.substr(0, 26).c_str(),
|
|
|
|
|
|
r.creatureWarn, r.objectWarn, r.questWarn,
|
|
|
|
|
|
rowTotal == 0 ? "PASS" : "FAIL");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (allPassed) {
|
|
|
|
|
|
std::printf("\n ALL ZONES PASSED\n");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n %d zone(s) have content warnings\n",
|
|
|
|
|
|
projectFailedZones);
|
|
|
|
|
|
return 1;
|
2026-05-06 19:44:28 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--check-project-refs") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Project-level cross-reference checker. Walks every zone
|
|
|
|
|
|
// and runs the same model-path / NPC-id checks as
|
|
|
|
|
|
// --check-zone-refs. Aggregates per zone with file-level
|
|
|
|
|
|
// breakdown. Exit 1 if any zone has dangling refs.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"check-project-refs: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zones.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zones.begin(), zones.end());
|
|
|
|
|
|
// Same model-resolve logic as --check-zone-refs, applied
|
|
|
|
|
|
// per zone with the appropriate root list.
|
|
|
|
|
|
auto stripExt = [](const std::string& p, const char* ext) {
|
|
|
|
|
|
size_t n = std::strlen(ext);
|
|
|
|
|
|
if (p.size() >= n) {
|
|
|
|
|
|
std::string tail = p.substr(p.size() - n);
|
|
|
|
|
|
std::string lower = tail;
|
|
|
|
|
|
for (auto& c : lower) c = std::tolower(static_cast<unsigned char>(c));
|
|
|
|
|
|
if (lower == ext) return p.substr(0, p.size() - n);
|
|
|
|
|
|
}
|
|
|
|
|
|
return p;
|
|
|
|
|
|
};
|
|
|
|
|
|
struct ZoneRow { std::string name; int objCheck, objMiss, qCheck, qMiss; };
|
|
|
|
|
|
std::vector<ZoneRow> rows;
|
|
|
|
|
|
int projectFailedZones = 0;
|
|
|
|
|
|
for (const auto& zoneDir : zones) {
|
|
|
|
|
|
ZoneRow row{fs::path(zoneDir).filename().string(), 0, 0, 0, 0};
|
|
|
|
|
|
auto modelExists = [&](const std::string& path, bool isWMO) {
|
|
|
|
|
|
std::string base;
|
|
|
|
|
|
std::vector<std::string> exts;
|
|
|
|
|
|
if (isWMO) {
|
|
|
|
|
|
base = stripExt(path, ".wmo");
|
|
|
|
|
|
exts = {".wob", ".wmo"};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
base = stripExt(path, ".m2");
|
|
|
|
|
|
exts = {".wom", ".m2"};
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> roots = {
|
|
|
|
|
|
"", zoneDir + "/", "output/", "custom_zones/", "Data/"
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const auto& root : roots) {
|
|
|
|
|
|
for (const auto& ext : exts) {
|
|
|
|
|
|
if (fs::exists(root + base + ext)) return true;
|
|
|
|
|
|
std::string lower = base + ext;
|
|
|
|
|
|
for (auto& c : lower) c = std::tolower(static_cast<unsigned char>(c));
|
|
|
|
|
|
if (fs::exists(root + lower)) return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
wowee::editor::ObjectPlacer op;
|
|
|
|
|
|
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
|
|
|
|
|
for (const auto& o : op.getObjects()) {
|
|
|
|
|
|
row.objCheck++;
|
|
|
|
|
|
bool isWMO = (o.type == wowee::editor::PlaceableType::WMO);
|
|
|
|
|
|
if (!modelExists(o.path, isWMO)) row.objMiss++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::NpcSpawner sp;
|
|
|
|
|
|
wowee::editor::QuestEditor qe;
|
|
|
|
|
|
bool hasCreatures = sp.loadFromFile(zoneDir + "/creatures.json");
|
|
|
|
|
|
std::unordered_set<uint32_t> creatureIds;
|
|
|
|
|
|
if (hasCreatures) {
|
|
|
|
|
|
for (const auto& s : sp.getSpawns()) creatureIds.insert(s.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (qe.loadFromFile(zoneDir + "/quests.json") && hasCreatures) {
|
|
|
|
|
|
for (const auto& q : qe.getQuests()) {
|
|
|
|
|
|
row.qCheck++;
|
|
|
|
|
|
bool localGiver = (q.questGiverNpcId != 0 &&
|
|
|
|
|
|
q.questGiverNpcId < 100000 &&
|
|
|
|
|
|
creatureIds.count(q.questGiverNpcId) == 0);
|
|
|
|
|
|
bool localTurn = (q.turnInNpcId != 0 &&
|
|
|
|
|
|
q.turnInNpcId < 100000 &&
|
|
|
|
|
|
q.turnInNpcId != q.questGiverNpcId &&
|
|
|
|
|
|
creatureIds.count(q.turnInNpcId) == 0);
|
|
|
|
|
|
if (localGiver) row.qMiss++;
|
|
|
|
|
|
if (localTurn) row.qMiss++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (row.objMiss + row.qMiss > 0) projectFailedZones++;
|
|
|
|
|
|
rows.push_back(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
int allPassed = (projectFailedZones == 0);
|
|
|
|
|
|
int totalMiss = 0;
|
|
|
|
|
|
for (const auto& r : rows) totalMiss += r.objMiss + r.qMiss;
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["projectDir"] = projectDir;
|
|
|
|
|
|
j["totalZones"] = zones.size();
|
|
|
|
|
|
j["failedZones"] = projectFailedZones;
|
|
|
|
|
|
j["totalMissing"] = totalMiss;
|
|
|
|
|
|
j["passed"] = bool(allPassed);
|
|
|
|
|
|
nlohmann::json arr = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
arr.push_back({{"zone", r.name},
|
|
|
|
|
|
{"objectsChecked", r.objCheck},
|
|
|
|
|
|
{"objectsMissing", r.objMiss},
|
|
|
|
|
|
{"questsChecked", r.qCheck},
|
|
|
|
|
|
{"questsMissing", r.qMiss}});
|
|
|
|
|
|
}
|
|
|
|
|
|
j["zones"] = arr;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return allPassed ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("check-project-refs: %s\n", projectDir.c_str());
|
|
|
|
|
|
std::printf(" zones : %zu (%d failed)\n",
|
|
|
|
|
|
zones.size(), projectFailedZones);
|
|
|
|
|
|
std::printf(" total missing: %d\n", totalMiss);
|
|
|
|
|
|
std::printf("\n zone obj_chk obj_miss q_chk q_miss status\n");
|
|
|
|
|
|
for (const auto& r : rows) {
|
|
|
|
|
|
int rowMiss = r.objMiss + r.qMiss;
|
|
|
|
|
|
std::printf(" %-26s %5d %5d %5d %5d %s\n",
|
|
|
|
|
|
r.name.substr(0, 26).c_str(),
|
|
|
|
|
|
r.objCheck, r.objMiss, r.qCheck, r.qMiss,
|
|
|
|
|
|
rowMiss == 0 ? "PASS" : "FAIL");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (allPassed) {
|
|
|
|
|
|
std::printf("\n ALL ZONES PASSED\n");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n %d zone(s) have dangling refs\n", projectFailedZones);
|
|
|
|
|
|
return 1;
|
2026-05-06 13:00:15 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--for-each-zone") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Batch runner: enumerates zones in <projectDir> and runs the
|
|
|
|
|
|
// command after '--' for each one. '{}' in the command is
|
|
|
|
|
|
// substituted with the zone path (find -exec convention).
|
|
|
|
|
|
//
|
|
|
|
|
|
// wowee_editor --for-each-zone custom_zones -- \\
|
|
|
|
|
|
// wowee_editor --validate-all {}
|
|
|
|
|
|
//
|
|
|
|
|
|
// Returns the count of failed runs as the exit code (capped
|
|
|
|
|
|
// at 255 so the shell can still see it).
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
// The literal '--' separates the projectDir from the command.
|
|
|
|
|
|
// Skip it; everything after is the command template.
|
|
|
|
|
|
if (i + 1 < argc && std::strcmp(argv[i + 1], "--") == 0) ++i;
|
|
|
|
|
|
if (i + 1 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"for-each-zone: need command after '--'\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Collect command tokens until end of argv. Don't try to be
|
|
|
|
|
|
// clever about quoting — just escape each token for shell
|
|
|
|
|
|
// safety using single quotes (' inside is escaped as '\\'').
|
|
|
|
|
|
std::vector<std::string> cmdTokens;
|
|
|
|
|
|
for (int k = i + 1; k < argc; ++k) cmdTokens.push_back(argv[k]);
|
|
|
|
|
|
i = argc - 1; // consume rest of argv
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr, "for-each-zone: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Find every child dir that contains a zone.json — that's the
|
|
|
|
|
|
// canonical 'is this a zone?' test the rest of the editor uses.
|
|
|
|
|
|
std::vector<std::string> zones;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (fs::exists(entry.path() / "zone.json")) {
|
|
|
|
|
|
zones.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zones.begin(), zones.end());
|
|
|
|
|
|
if (zones.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "for-each-zone: no zones found in %s\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto shellEscape = [](const std::string& s) {
|
|
|
|
|
|
std::string out = "'";
|
|
|
|
|
|
for (char c : s) {
|
|
|
|
|
|
if (c == '\'') out += "'\\''";
|
|
|
|
|
|
else out += c;
|
|
|
|
|
|
}
|
|
|
|
|
|
out += "'";
|
|
|
|
|
|
return out;
|
|
|
|
|
|
};
|
|
|
|
|
|
int failed = 0;
|
|
|
|
|
|
for (const auto& zone : zones) {
|
|
|
|
|
|
std::string cmd;
|
|
|
|
|
|
for (size_t k = 0; k < cmdTokens.size(); ++k) {
|
|
|
|
|
|
if (k > 0) cmd += " ";
|
|
|
|
|
|
std::string token = cmdTokens[k];
|
|
|
|
|
|
// Replace {} with zone path (every occurrence).
|
|
|
|
|
|
size_t pos;
|
|
|
|
|
|
while ((pos = token.find("{}")) != std::string::npos) {
|
|
|
|
|
|
token.replace(pos, 2, zone);
|
|
|
|
|
|
}
|
|
|
|
|
|
cmd += shellEscape(token);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("[%s]\n", zone.c_str());
|
|
|
|
|
|
// Flush before std::system so the header lands above the
|
|
|
|
|
|
// child's output rather than after (parent stdout is line-
|
|
|
|
|
|
// buffered, child writes go straight to the terminal).
|
|
|
|
|
|
std::fflush(stdout);
|
|
|
|
|
|
int rc = std::system(cmd.c_str());
|
|
|
|
|
|
if (rc != 0) {
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"for-each-zone: command exited %d for %s\n",
|
|
|
|
|
|
rc, zone.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\nfor-each-zone: %zu zones, %d failed\n",
|
|
|
|
|
|
zones.size(), failed);
|
|
|
|
|
|
return failed > 255 ? 255 : failed;
|
2026-05-06 18:11:54 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--for-each-tile") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Per-tile batch runner. --for-each-zone iterates zones in
|
|
|
|
|
|
// a project; this iterates tiles within a zone. The '{}' in
|
|
|
|
|
|
// the command template is replaced with the tile-base path
|
|
|
|
|
|
// (zoneDir/mapName_TX_TY) — the form most tile-level
|
|
|
|
|
|
// editor commands take.
|
|
|
|
|
|
//
|
|
|
|
|
|
// wowee_editor --for-each-tile MyZone -- \\
|
|
|
|
|
|
// wowee_editor --build-woc {}
|
|
|
|
|
|
// wowee_editor --for-each-tile MyZone -- \\
|
|
|
|
|
|
// wowee_editor --validate-whm {}
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
if (i + 1 < argc && std::strcmp(argv[i + 1], "--") == 0) ++i;
|
|
|
|
|
|
if (i + 1 >= argc) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"for-each-tile: need command after '--'\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> cmdTokens;
|
|
|
|
|
|
for (int k = i + 1; k < argc; ++k) cmdTokens.push_back(argv[k]);
|
|
|
|
|
|
i = argc - 1;
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
std::string manifestPath = zoneDir + "/zone.json";
|
|
|
|
|
|
if (!fs::exists(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"for-each-tile: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr, "for-each-tile: parse failed\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (zm.tiles.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "for-each-tile: zone has no tiles\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Same shell-escape + cmd-substitution as --for-each-zone.
|
|
|
|
|
|
auto shellEscape = [](const std::string& s) {
|
|
|
|
|
|
std::string out = "'";
|
|
|
|
|
|
for (char c : s) {
|
|
|
|
|
|
if (c == '\'') out += "'\\''";
|
|
|
|
|
|
else out += c;
|
|
|
|
|
|
}
|
|
|
|
|
|
out += "'";
|
|
|
|
|
|
return out;
|
|
|
|
|
|
};
|
|
|
|
|
|
int failed = 0;
|
|
|
|
|
|
// Sort tiles so order is deterministic across runs.
|
|
|
|
|
|
auto tiles = zm.tiles;
|
|
|
|
|
|
std::sort(tiles.begin(), tiles.end());
|
|
|
|
|
|
for (const auto& [tx, ty] : tiles) {
|
|
|
|
|
|
std::string tileBase = zoneDir + "/" + zm.mapName + "_" +
|
|
|
|
|
|
std::to_string(tx) + "_" + std::to_string(ty);
|
|
|
|
|
|
std::string cmd;
|
|
|
|
|
|
for (size_t k = 0; k < cmdTokens.size(); ++k) {
|
|
|
|
|
|
if (k > 0) cmd += " ";
|
|
|
|
|
|
std::string token = cmdTokens[k];
|
|
|
|
|
|
size_t pos;
|
|
|
|
|
|
while ((pos = token.find("{}")) != std::string::npos) {
|
|
|
|
|
|
token.replace(pos, 2, tileBase);
|
|
|
|
|
|
}
|
|
|
|
|
|
cmd += shellEscape(token);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("[%s (%d, %d)]\n", tileBase.c_str(), tx, ty);
|
|
|
|
|
|
std::fflush(stdout);
|
|
|
|
|
|
int rc = std::system(cmd.c_str());
|
|
|
|
|
|
if (rc != 0) {
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"for-each-tile: command exited %d for (%d, %d)\n",
|
|
|
|
|
|
rc, tx, ty);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\nfor-each-tile: %zu tiles, %d failed\n",
|
|
|
|
|
|
tiles.size(), failed);
|
|
|
|
|
|
return failed > 255 ? 255 : failed;
|
2026-05-05 11:53:12 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--version") == 0 || std::strcmp(argv[i], "-v") == 0) {
|
2026-05-05 16:54:03 -07:00
|
|
|
|
std::printf("Wowee World Editor v1.0.0\n");
|
|
|
|
|
|
std::printf("Open formats: WOT/WHM/WOM/WOB/WOC/WCP + PNG/JSON (all novel)\n");
|
|
|
|
|
|
std::printf("By Kelsi Davis\n");
|
2026-05-05 11:53:12 -07:00
|
|
|
|
return 0;
|
2026-05-06 13:59:54 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--list-commands") == 0) {
|
|
|
|
|
|
// Capture printUsage's stdout and grep for '--flag' tokens at
|
|
|
|
|
|
// the start of each line. This auto-tracks the help text as
|
|
|
|
|
|
// commands are added — no parallel list to maintain. Result
|
|
|
|
|
|
// is a sorted, deduped, one-per-line list of recognized flags.
|
|
|
|
|
|
FILE* old = stdout;
|
|
|
|
|
|
// Temp file lets us read printUsage's output back. fmemopen
|
|
|
|
|
|
// would be cleaner but isn't available on Windows; tmpfile is
|
|
|
|
|
|
// portable.
|
|
|
|
|
|
FILE* tmp = std::tmpfile();
|
|
|
|
|
|
if (!tmp) { std::fprintf(stderr, "list-commands: tmpfile failed\n"); return 1; }
|
|
|
|
|
|
stdout = tmp;
|
refactor(editor): extract printUsage into cli_help.cpp
Pulls the 597-line block of printf calls that emits the --help
text out of main.cpp into its own translation unit. printUsage
is the longest single function in main.cpp by far and was pure
boilerplate (no logic, just a flat list of help lines).
Function moved verbatim to wowee::editor::cli::printUsage; all
6 in-tree callers (--list-commands, --info-cli-stats,
--info-cli-categories, --info-cli-help, --validate-cli-help,
and the bare --help/-h handler) updated to the namespaced name.
The CLI-meta commands continue to capture printUsage's stdout
the same way, so behavior is identical (verified by re-running
--validate-cli-help, --info-cli-stats, --list-commands).
main.cpp drops 26,650 → 26,286 lines (-364 net; -597 from the
removal, +233 from the include and namespace-prefixing the
six call sites... wait, no, +6). Actually main.cpp net delta
matches the body extraction.
2026-05-08 20:12:15 -07:00
|
|
|
|
wowee::editor::cli::printUsage(argv[0]);
|
2026-05-06 13:59:54 -07:00
|
|
|
|
stdout = old;
|
|
|
|
|
|
std::fseek(tmp, 0, SEEK_SET);
|
|
|
|
|
|
std::set<std::string> commands;
|
|
|
|
|
|
char line[512];
|
|
|
|
|
|
while (std::fgets(line, sizeof(line), tmp)) {
|
|
|
|
|
|
// Match leading whitespace then '--' then [a-z-]+
|
|
|
|
|
|
const char* p = line;
|
|
|
|
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
|
|
|
|
if (p[0] != '-' || p[1] != '-') continue;
|
|
|
|
|
|
std::string flag;
|
|
|
|
|
|
while (*p && (std::isalnum(static_cast<unsigned char>(*p)) ||
|
|
|
|
|
|
*p == '-' || *p == '_')) {
|
|
|
|
|
|
flag += *p++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (flag.size() > 2) commands.insert(flag);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fclose(tmp);
|
|
|
|
|
|
// Always include the meta-flags that printUsage describes
|
|
|
|
|
|
// alongside others (-h/-v aliases) since the regex above only
|
|
|
|
|
|
// captures double-dash forms.
|
|
|
|
|
|
commands.insert("--help");
|
|
|
|
|
|
commands.insert("--version");
|
|
|
|
|
|
for (const auto& c : commands) std::printf("%s\n", c.c_str());
|
|
|
|
|
|
return 0;
|
2026-05-06 15:21:24 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-cli-stats") == 0) {
|
|
|
|
|
|
// Meta-stats on the CLI surface: total command count + per-
|
|
|
|
|
|
// category breakdown by prefix verb (--info-*, --validate-*,
|
|
|
|
|
|
// --diff-*, etc.). Useful for tracking growth over time and
|
|
|
|
|
|
// spotting category imbalances.
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
// Re-use --list-commands' parser. Capture printUsage stdout.
|
|
|
|
|
|
FILE* old = stdout;
|
|
|
|
|
|
FILE* tmp = std::tmpfile();
|
|
|
|
|
|
if (!tmp) { std::fprintf(stderr, "info-cli-stats: tmpfile failed\n"); return 1; }
|
|
|
|
|
|
stdout = tmp;
|
refactor(editor): extract printUsage into cli_help.cpp
Pulls the 597-line block of printf calls that emits the --help
text out of main.cpp into its own translation unit. printUsage
is the longest single function in main.cpp by far and was pure
boilerplate (no logic, just a flat list of help lines).
Function moved verbatim to wowee::editor::cli::printUsage; all
6 in-tree callers (--list-commands, --info-cli-stats,
--info-cli-categories, --info-cli-help, --validate-cli-help,
and the bare --help/-h handler) updated to the namespaced name.
The CLI-meta commands continue to capture printUsage's stdout
the same way, so behavior is identical (verified by re-running
--validate-cli-help, --info-cli-stats, --list-commands).
main.cpp drops 26,650 → 26,286 lines (-364 net; -597 from the
removal, +233 from the include and namespace-prefixing the
six call sites... wait, no, +6). Actually main.cpp net delta
matches the body extraction.
2026-05-08 20:12:15 -07:00
|
|
|
|
wowee::editor::cli::printUsage(argv[0]);
|
2026-05-06 15:21:24 -07:00
|
|
|
|
stdout = old;
|
|
|
|
|
|
std::fseek(tmp, 0, SEEK_SET);
|
|
|
|
|
|
std::set<std::string> commands;
|
|
|
|
|
|
char line[512];
|
|
|
|
|
|
while (std::fgets(line, sizeof(line), tmp)) {
|
|
|
|
|
|
const char* p = line;
|
|
|
|
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
|
|
|
|
if (p[0] != '-' || p[1] != '-') continue;
|
|
|
|
|
|
std::string flag;
|
|
|
|
|
|
while (*p && (std::isalnum(static_cast<unsigned char>(*p)) ||
|
|
|
|
|
|
*p == '-' || *p == '_')) { flag += *p++; }
|
|
|
|
|
|
if (flag.size() > 2) commands.insert(flag);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fclose(tmp);
|
|
|
|
|
|
commands.insert("--help");
|
|
|
|
|
|
commands.insert("--version");
|
|
|
|
|
|
// Bucket by category — verb is the second token after '--',
|
|
|
|
|
|
// up to the next dash. So '--info-zone-tree' -> 'info'.
|
|
|
|
|
|
std::map<std::string, int> byCategory;
|
|
|
|
|
|
int maxLen = 0;
|
|
|
|
|
|
for (const auto& c : commands) {
|
|
|
|
|
|
if (static_cast<int>(c.size()) > maxLen) maxLen = static_cast<int>(c.size());
|
|
|
|
|
|
size_t verbStart = 2; // skip '--'
|
|
|
|
|
|
size_t verbEnd = c.find('-', verbStart);
|
|
|
|
|
|
std::string verb = (verbEnd == std::string::npos)
|
|
|
|
|
|
? c.substr(verbStart)
|
|
|
|
|
|
: c.substr(verbStart, verbEnd - verbStart);
|
|
|
|
|
|
byCategory[verb]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["totalCommands"] = commands.size();
|
|
|
|
|
|
j["maxFlagLength"] = maxLen;
|
|
|
|
|
|
nlohmann::json cats = nlohmann::json::object();
|
|
|
|
|
|
for (const auto& [v, c] : byCategory) cats[v] = c;
|
|
|
|
|
|
j["byCategory"] = cats;
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("CLI surface stats\n");
|
|
|
|
|
|
std::printf(" total commands : %zu\n", commands.size());
|
|
|
|
|
|
std::printf(" longest flag : %d chars\n", maxLen);
|
|
|
|
|
|
std::printf("\n Categories (by verb prefix, sorted by count):\n");
|
|
|
|
|
|
// Sort by count descending for the table.
|
|
|
|
|
|
std::vector<std::pair<std::string, int>> sorted(
|
|
|
|
|
|
byCategory.begin(), byCategory.end());
|
|
|
|
|
|
std::sort(sorted.begin(), sorted.end(),
|
|
|
|
|
|
[](const auto& a, const auto& b) {
|
|
|
|
|
|
return a.second > b.second;
|
|
|
|
|
|
});
|
|
|
|
|
|
for (const auto& [verb, count] : sorted) {
|
|
|
|
|
|
std::printf(" --%-12s %4d\n", verb.c_str(), count);
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
2026-05-07 17:43:58 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-cli-categories") == 0) {
|
|
|
|
|
|
// Discovery view of every CLI flag grouped by verb prefix.
|
|
|
|
|
|
// Where --info-cli-stats just counts per category, this
|
|
|
|
|
|
// lists every command in each category — handy for "I
|
|
|
|
|
|
// know I want to gen something but what shapes/textures
|
|
|
|
|
|
// are available?"
|
|
|
|
|
|
FILE* old = stdout;
|
|
|
|
|
|
FILE* tmp = std::tmpfile();
|
|
|
|
|
|
if (!tmp) {
|
|
|
|
|
|
std::fprintf(stderr, "info-cli-categories: tmpfile failed\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
stdout = tmp;
|
refactor(editor): extract printUsage into cli_help.cpp
Pulls the 597-line block of printf calls that emits the --help
text out of main.cpp into its own translation unit. printUsage
is the longest single function in main.cpp by far and was pure
boilerplate (no logic, just a flat list of help lines).
Function moved verbatim to wowee::editor::cli::printUsage; all
6 in-tree callers (--list-commands, --info-cli-stats,
--info-cli-categories, --info-cli-help, --validate-cli-help,
and the bare --help/-h handler) updated to the namespaced name.
The CLI-meta commands continue to capture printUsage's stdout
the same way, so behavior is identical (verified by re-running
--validate-cli-help, --info-cli-stats, --list-commands).
main.cpp drops 26,650 → 26,286 lines (-364 net; -597 from the
removal, +233 from the include and namespace-prefixing the
six call sites... wait, no, +6). Actually main.cpp net delta
matches the body extraction.
2026-05-08 20:12:15 -07:00
|
|
|
|
wowee::editor::cli::printUsage(argv[0]);
|
2026-05-07 17:43:58 -07:00
|
|
|
|
stdout = old;
|
|
|
|
|
|
std::fseek(tmp, 0, SEEK_SET);
|
|
|
|
|
|
std::set<std::string> commands;
|
|
|
|
|
|
char line[512];
|
|
|
|
|
|
while (std::fgets(line, sizeof(line), tmp)) {
|
|
|
|
|
|
const char* p = line;
|
|
|
|
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
|
|
|
|
if (p[0] != '-' || p[1] != '-') continue;
|
|
|
|
|
|
std::string flag;
|
|
|
|
|
|
while (*p && (std::isalnum(static_cast<unsigned char>(*p)) ||
|
|
|
|
|
|
*p == '-' || *p == '_')) { flag += *p++; }
|
|
|
|
|
|
if (flag.size() > 2) commands.insert(flag);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fclose(tmp);
|
|
|
|
|
|
commands.insert("--help");
|
|
|
|
|
|
commands.insert("--version");
|
|
|
|
|
|
std::map<std::string, std::vector<std::string>> byCategory;
|
|
|
|
|
|
for (const auto& c : commands) {
|
|
|
|
|
|
size_t verbStart = 2;
|
|
|
|
|
|
size_t verbEnd = c.find('-', verbStart);
|
|
|
|
|
|
std::string verb = (verbEnd == std::string::npos)
|
|
|
|
|
|
? c.substr(verbStart)
|
|
|
|
|
|
: c.substr(verbStart, verbEnd - verbStart);
|
|
|
|
|
|
byCategory[verb].push_back(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("CLI commands by category (%zu total):\n\n",
|
|
|
|
|
|
commands.size());
|
|
|
|
|
|
// Sort categories by count descending, commands within
|
|
|
|
|
|
// each alphabetically.
|
|
|
|
|
|
std::vector<std::pair<std::string, std::vector<std::string>>> sorted(
|
|
|
|
|
|
byCategory.begin(), byCategory.end());
|
|
|
|
|
|
std::sort(sorted.begin(), sorted.end(),
|
|
|
|
|
|
[](const auto& a, const auto& b) {
|
|
|
|
|
|
if (a.second.size() != b.second.size())
|
|
|
|
|
|
return a.second.size() > b.second.size();
|
|
|
|
|
|
return a.first < b.first;
|
|
|
|
|
|
});
|
|
|
|
|
|
for (const auto& [verb, cmds] : sorted) {
|
|
|
|
|
|
std::printf("--%s (%zu):\n", verb.c_str(), cmds.size());
|
|
|
|
|
|
for (const auto& c : cmds) {
|
|
|
|
|
|
std::printf(" %s\n", c.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
2026-05-06 15:43:02 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--info-cli-help") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Substring search through the help text. With 130+ commands,
|
|
|
|
|
|
// 'is there a thing for X?' is a common ask — this answers it
|
|
|
|
|
|
// without making the user scroll the full --help output:
|
|
|
|
|
|
//
|
|
|
|
|
|
// wowee_editor --info-cli-help quest
|
|
|
|
|
|
// wowee_editor --info-cli-help validate
|
|
|
|
|
|
// wowee_editor --info-cli-help glb
|
|
|
|
|
|
std::string pattern = argv[++i];
|
|
|
|
|
|
// Lowercase the pattern for case-insensitive match.
|
|
|
|
|
|
std::string patLower = pattern;
|
|
|
|
|
|
for (auto& c : patLower) c = std::tolower(static_cast<unsigned char>(c));
|
|
|
|
|
|
// Capture printUsage stdout, walk line-by-line, print every
|
|
|
|
|
|
// line containing the pattern (case-insensitive). Continuation
|
|
|
|
|
|
// lines (the indented description on the line after a flag)
|
|
|
|
|
|
// are emitted along with the flag line for context.
|
|
|
|
|
|
FILE* old = stdout;
|
|
|
|
|
|
FILE* tmp = std::tmpfile();
|
|
|
|
|
|
if (!tmp) {
|
|
|
|
|
|
std::fprintf(stderr, "info-cli-help: tmpfile failed\n"); return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
stdout = tmp;
|
refactor(editor): extract printUsage into cli_help.cpp
Pulls the 597-line block of printf calls that emits the --help
text out of main.cpp into its own translation unit. printUsage
is the longest single function in main.cpp by far and was pure
boilerplate (no logic, just a flat list of help lines).
Function moved verbatim to wowee::editor::cli::printUsage; all
6 in-tree callers (--list-commands, --info-cli-stats,
--info-cli-categories, --info-cli-help, --validate-cli-help,
and the bare --help/-h handler) updated to the namespaced name.
The CLI-meta commands continue to capture printUsage's stdout
the same way, so behavior is identical (verified by re-running
--validate-cli-help, --info-cli-stats, --list-commands).
main.cpp drops 26,650 → 26,286 lines (-364 net; -597 from the
removal, +233 from the include and namespace-prefixing the
six call sites... wait, no, +6). Actually main.cpp net delta
matches the body extraction.
2026-05-08 20:12:15 -07:00
|
|
|
|
wowee::editor::cli::printUsage(argv[0]);
|
2026-05-06 15:43:02 -07:00
|
|
|
|
stdout = old;
|
|
|
|
|
|
std::fseek(tmp, 0, SEEK_SET);
|
|
|
|
|
|
std::vector<std::string> lines;
|
|
|
|
|
|
char buf[1024];
|
|
|
|
|
|
while (std::fgets(buf, sizeof(buf), tmp)) {
|
|
|
|
|
|
std::string s = buf;
|
|
|
|
|
|
if (!s.empty() && s.back() == '\n') s.pop_back();
|
|
|
|
|
|
lines.push_back(std::move(s));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fclose(tmp);
|
|
|
|
|
|
int matches = 0;
|
|
|
|
|
|
for (size_t k = 0; k < lines.size(); ++k) {
|
|
|
|
|
|
std::string lower = lines[k];
|
|
|
|
|
|
for (auto& c : lower) c = std::tolower(static_cast<unsigned char>(c));
|
|
|
|
|
|
if (lower.find(patLower) == std::string::npos) continue;
|
|
|
|
|
|
std::printf("%s\n", lines[k].c_str());
|
|
|
|
|
|
// Look ahead for a continuation line (indented and not
|
|
|
|
|
|
// starting with '--'). Print it for context.
|
|
|
|
|
|
if (k + 1 < lines.size()) {
|
|
|
|
|
|
const auto& next = lines[k + 1];
|
|
|
|
|
|
if (!next.empty() && next[0] == ' ' &&
|
|
|
|
|
|
next.find("--") == std::string::npos) {
|
|
|
|
|
|
std::printf("%s\n", next.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
matches++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (matches == 0) {
|
|
|
|
|
|
std::fprintf(stderr, "info-cli-help: no matches for '%s'\n",
|
|
|
|
|
|
pattern.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fprintf(stderr, "\n%d line(s) matched '%s'\n", matches, pattern.c_str());
|
|
|
|
|
|
return 0;
|
2026-05-06 17:04:57 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--validate-cli-help") == 0) {
|
|
|
|
|
|
// Self-check: every flag we declare in kArgRequired (the list
|
|
|
|
|
|
// of commands needing positional args) must appear in the
|
|
|
|
|
|
// help text printUsage emits. Catches drift where someone
|
|
|
|
|
|
// adds a handler + argument check but forgets the help line.
|
|
|
|
|
|
bool jsonOut = (i + 1 < argc &&
|
|
|
|
|
|
std::strcmp(argv[i + 1], "--json") == 0);
|
|
|
|
|
|
if (jsonOut) i++;
|
|
|
|
|
|
// Capture printUsage's stdout.
|
|
|
|
|
|
FILE* old = stdout;
|
|
|
|
|
|
FILE* tmp = std::tmpfile();
|
|
|
|
|
|
if (!tmp) { std::fprintf(stderr, "validate-cli-help: tmpfile failed\n"); return 1; }
|
|
|
|
|
|
stdout = tmp;
|
refactor(editor): extract printUsage into cli_help.cpp
Pulls the 597-line block of printf calls that emits the --help
text out of main.cpp into its own translation unit. printUsage
is the longest single function in main.cpp by far and was pure
boilerplate (no logic, just a flat list of help lines).
Function moved verbatim to wowee::editor::cli::printUsage; all
6 in-tree callers (--list-commands, --info-cli-stats,
--info-cli-categories, --info-cli-help, --validate-cli-help,
and the bare --help/-h handler) updated to the namespaced name.
The CLI-meta commands continue to capture printUsage's stdout
the same way, so behavior is identical (verified by re-running
--validate-cli-help, --info-cli-stats, --list-commands).
main.cpp drops 26,650 → 26,286 lines (-364 net; -597 from the
removal, +233 from the include and namespace-prefixing the
six call sites... wait, no, +6). Actually main.cpp net delta
matches the body extraction.
2026-05-08 20:12:15 -07:00
|
|
|
|
wowee::editor::cli::printUsage(argv[0]);
|
2026-05-06 17:04:57 -07:00
|
|
|
|
stdout = old;
|
|
|
|
|
|
std::fseek(tmp, 0, SEEK_SET);
|
|
|
|
|
|
std::string helpText;
|
|
|
|
|
|
char chunk[1024];
|
|
|
|
|
|
while (std::fgets(chunk, sizeof(chunk), tmp)) helpText += chunk;
|
|
|
|
|
|
std::fclose(tmp);
|
|
|
|
|
|
// Walk kArgRequired and check each appears in the help.
|
|
|
|
|
|
std::vector<std::string> missing;
|
|
|
|
|
|
for (const char* opt : kArgRequired) {
|
|
|
|
|
|
if (helpText.find(opt) == std::string::npos) {
|
|
|
|
|
|
missing.push_back(opt);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (jsonOut) {
|
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
|
j["totalArgRequired"] = sizeof(kArgRequired) / sizeof(kArgRequired[0]);
|
|
|
|
|
|
j["missing"] = missing;
|
|
|
|
|
|
j["passed"] = missing.empty();
|
|
|
|
|
|
std::printf("%s\n", j.dump(2).c_str());
|
|
|
|
|
|
return missing.empty() ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("CLI help self-check\n");
|
|
|
|
|
|
std::printf(" kArgRequired entries : %zu\n",
|
|
|
|
|
|
sizeof(kArgRequired) / sizeof(kArgRequired[0]));
|
|
|
|
|
|
if (missing.empty()) {
|
|
|
|
|
|
std::printf(" PASSED — every kArgRequired flag is documented\n");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf(" FAILED — %zu flag(s) missing from help text:\n", missing.size());
|
|
|
|
|
|
for (const auto& m : missing) std::printf(" - %s\n", m.c_str());
|
|
|
|
|
|
return 1;
|
2026-05-06 13:59:54 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--gen-completion") == 0 && i + 1 < argc) {
|
|
|
|
|
|
// Emit a bash or zsh completion script. Re-execs the editor's
|
|
|
|
|
|
// own --list-commands at completion time so newly-added flags
|
|
|
|
|
|
// light up automatically without regenerating the script.
|
|
|
|
|
|
std::string shell = argv[++i];
|
|
|
|
|
|
if (shell != "bash" && shell != "zsh") {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"gen-completion: shell must be 'bash' or 'zsh', got '%s'\n",
|
|
|
|
|
|
shell.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Use argv[0] as the binary name in the completion so it
|
|
|
|
|
|
// works whether the user installed it as 'wowee_editor' or
|
|
|
|
|
|
// a custom alias. Strip directory components for the
|
|
|
|
|
|
// completion-name registration (bash 'complete -F' expects
|
|
|
|
|
|
// a basename).
|
|
|
|
|
|
std::string self = argv[0];
|
|
|
|
|
|
auto slash = self.find_last_of('/');
|
|
|
|
|
|
std::string baseName = (slash != std::string::npos)
|
|
|
|
|
|
? self.substr(slash + 1)
|
|
|
|
|
|
: self;
|
|
|
|
|
|
if (shell == "bash") {
|
|
|
|
|
|
std::printf(
|
|
|
|
|
|
"# wowee_editor bash completion — source from ~/.bashrc:\n"
|
|
|
|
|
|
"# source <(%s --gen-completion bash)\n"
|
|
|
|
|
|
"_wowee_editor_complete() {\n"
|
|
|
|
|
|
" local cur prev cmds\n"
|
|
|
|
|
|
" COMPREPLY=()\n"
|
|
|
|
|
|
" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n"
|
|
|
|
|
|
" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n"
|
|
|
|
|
|
" # Cache the command list per shell session.\n"
|
|
|
|
|
|
" if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n"
|
|
|
|
|
|
" _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n"
|
|
|
|
|
|
" fi\n"
|
|
|
|
|
|
" if [[ \"$cur\" == --* ]]; then\n"
|
|
|
|
|
|
" COMPREPLY=( $(compgen -W \"$_WOWEE_EDITOR_CMDS\" -- \"$cur\") )\n"
|
|
|
|
|
|
" return 0\n"
|
|
|
|
|
|
" fi\n"
|
|
|
|
|
|
" # Default: complete file paths for arg slots.\n"
|
|
|
|
|
|
" COMPREPLY=( $(compgen -f -- \"$cur\") )\n"
|
|
|
|
|
|
"}\n"
|
|
|
|
|
|
"complete -F _wowee_editor_complete %s\n",
|
|
|
|
|
|
self.c_str(), self.c_str(), baseName.c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// zsh — simpler descriptor-based completion.
|
|
|
|
|
|
std::printf(
|
|
|
|
|
|
"# wowee_editor zsh completion — source from ~/.zshrc:\n"
|
|
|
|
|
|
"# source <(%s --gen-completion zsh)\n"
|
|
|
|
|
|
"_wowee_editor_complete() {\n"
|
|
|
|
|
|
" local -a cmds\n"
|
|
|
|
|
|
" if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n"
|
|
|
|
|
|
" export _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n"
|
|
|
|
|
|
" fi\n"
|
|
|
|
|
|
" cmds=( ${(f)_WOWEE_EDITOR_CMDS} )\n"
|
|
|
|
|
|
" _arguments \"*: :($cmds)\"\n"
|
|
|
|
|
|
"}\n"
|
|
|
|
|
|
"compdef _wowee_editor_complete %s\n",
|
|
|
|
|
|
self.c_str(), self.c_str(), baseName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
} else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) {
|
refactor(editor): extract printUsage into cli_help.cpp
Pulls the 597-line block of printf calls that emits the --help
text out of main.cpp into its own translation unit. printUsage
is the longest single function in main.cpp by far and was pure
boilerplate (no logic, just a flat list of help lines).
Function moved verbatim to wowee::editor::cli::printUsage; all
6 in-tree callers (--list-commands, --info-cli-stats,
--info-cli-categories, --info-cli-help, --validate-cli-help,
and the bare --help/-h handler) updated to the namespaced name.
The CLI-meta commands continue to capture printUsage's stdout
the same way, so behavior is identical (verified by re-running
--validate-cli-help, --info-cli-stats, --list-commands).
main.cpp drops 26,650 → 26,286 lines (-364 net; -597 from the
removal, +233 from the include and namespace-prefixing the
six call sites... wait, no, +6). Actually main.cpp net delta
matches the body extraction.
2026-05-08 20:12:15 -07:00
|
|
|
|
wowee::editor::cli::printUsage(argv[0]);
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 12:41:19 -07:00
|
|
|
|
|
feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
|
|
|
|
if (dataPath.empty()) {
|
|
|
|
|
|
dataPath = "Data";
|
|
|
|
|
|
LOG_INFO("No --data path specified, using default: ", dataPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wowee::editor::EditorApp app;
|
|
|
|
|
|
if (!app.initialize(dataPath)) {
|
|
|
|
|
|
LOG_ERROR("Failed to initialize editor");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!adtMap.empty()) {
|
|
|
|
|
|
app.loadADT(adtMap, adtX, adtY);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
app.run();
|
|
|
|
|
|
app.shutdown();
|
|
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|