Commit graph

471 commits

Author SHA1 Message Date
Kelsi
8f6315f155 feat(editor): add --validate-woc + --validate-whm and roll into validate-all
Round out the per-format validator suite. Open-format zone validation
now covers all four binary formats:

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

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

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

validate-all now reports all four format counts (WOM/WOB/WOC/WHM)
and aggregates errors. Verified end-to-end: a fresh scaffolded zone
with --build-woc yields 256/256 chunks loaded, 32768 walkable
triangles, validate-all PASSED. Synthesized WOC with 0xFF flags
correctly fails with 'unknown flag bits 0xFF' and exit 1.
2026-05-06 11:58:20 -07:00
Kelsi
67b719a2d9 feat(editor): add --validate-all + extract validator helpers
Walks a directory recursively and runs the deep validators on every
.wom and .wob it finds. Single CI gate for an entire zone tree:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Lets shell scripts populate objects/buildings without the GUI:

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

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

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

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

Loads any existing creatures.json first then appends, so multiple
invocations build up the file. The standard NPC spawner caps
(50k creatures) protect against runaway scripts.
2026-05-06 11:35:07 -07:00
Kelsi
65d867c035 feat(editor): --json mode on --info-wot and --info-zone
Last two inspectors to gain --json mode. Schemas show off the
nested structure these commands surface (chunk counts, audio
sub-object, tile array):

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

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

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

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

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

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

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

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

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

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

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

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

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

Schema:

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

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

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

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

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

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

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

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

Lets CI scripts gate on coverage:

  cov=$(wowee_editor --info-extract Data --json | jq .overallCoverage)
  if [ "$cov" != "100" ]; then asset_extract --upgrade-extract Data; fi
2026-05-06 11:07:11 -07:00
Kelsi
5799b5f88f feat(editor): --info-extract reports proprietary vs open-format byte totals
Adds two summary lines so users can see how big a 'purge proprietary
after open conversion' workflow would shrink their tree (or how
much extra a dual-format extraction costs):

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

Counts every BLP/DBC/M2/WMO/ADT into the proprietary bucket and
every PNG/JSON/WOM/WOB/WHM/WOT/WOC into the open bucket. The
ratio surfaces things like 'PNG is bigger than DXT-compressed BLP'
or 'JSON DBC is much smaller than the binary' without the user
having to run du themselves.
2026-05-06 11:03:23 -07:00
Kelsi
5a816f104c feat(editor): add --info-extract CLI for extraction coverage report
Walks an extracted asset tree and reports per-format counts plus
how many proprietary files have a wowee open-format sidecar.
Lets users (or CI) see at a glance whether asset_extract was run
with --emit-open and how complete the open-format coverage is:

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

Skips _NNN group sub-files when counting WMOs (only the root WMO
ships with a WOB sidecar). The headless CLI is now at 22 commands.
2026-05-06 10:50:17 -07:00
Kelsi
9e801f93b6 fix(brush): NaN-guard EditorBrush::setPosition
applyBrush already early-outs on NaN brush position, but the stored
worldPos_ would still capture NaN and surface it to UI panels and
the brush-circle indicator (which renders a NaN ring). Reject NaN
at the setter so the editor state itself stays sane.
2026-05-06 10:15:00 -07:00
Kelsi
4c0f8dd5c0 fix(history): bounds-check chunkIndex in captureChunk/restoreChunk
ADTTerrain.chunks is std::array<MapChunk, 256> — out-of-range
indexing is undefined behaviour. Reject indices outside [0, 255]
and return empty / no-op rather than crashing on a stale undo
record from a future-version terrain layout.
2026-05-06 10:13:56 -07:00
Kelsi
17ca42b70b fix(camera): validate setSpeed input against wheel-clamp range
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
EditorCamera::setSpeed accepted any float, including NaN/inf and
values outside the wheel-zoom clamp range [10, 2000]. NaN speed
would propagate into the per-tick position update (NaN * dt =
NaN) and corrupt the camera view matrix. Match the wheel clamp
so external setters can't bypass the UI's bounds.
2026-05-06 10:12:45 -07:00
Kelsi
e02f2baf9d feat(editor): add --fix-zone CLI to re-apply load-time scrubs/caps
Loads + immediately re-saves zone.json, creatures.json,
objects.json, and quests.json. The load-time scrubs (NaN,
out-of-range, oversize) and save-time caps fire on the round-trip,
producing a cleaned-up zone without ever opening the GUI.

Useful when an old zone was created before recent hardening
batches — running this once normalizes the on-disk state to match
what the current loaders expect.
2026-05-06 10:08:49 -07:00
Kelsi
fc284c7460 fix(project): cap zones at 1024 on project load
Same defense pattern as the other editor JSON loaders. WoW only
supports 65535 maps total and the editor loads one tile at a
time; 1024 zones per project is plenty. Stale autosave or hand-
edit could otherwise grow zones unbounded and slow the project
picker UI.
2026-05-06 10:01:36 -07:00
Kelsi
ffc0862977 fix(wcp): cap readInfo file-list parse at 1M entries
readInfo iterated the info JSON's files array without bounding;
a malicious WCP could declare more entries than the header
fileCount allows and grow info.files unbounded. Cap to 1M
matching the header check so both readInfo callers and
--list-wcp/--info-wcp stay bounded.
2026-05-06 09:57:37 -07:00
Kelsi
bd97470929 fix(npc): cap spawn count at 50k on load
Same defense pattern as QuestEditor (4096) and ObjectPlacer (100k).
A stale autosave or scatter-runaway could carry millions of NPCs;
each emits creature_template + creature + optional addon/waypoint
rows, drowning the SQL output and the M2 marker mesh.

Every editor JSON loader now has a matched-to-cost upper bound
(NPCs 50k, quests 4k, objects 100k, waypoints 256).
2026-05-06 09:56:55 -07:00
Kelsi
49a2907bc5 fix(editor): cap quest count at 4096 and object count at 100k on load
A stale autosave or hand-edited JSON could carry an unbounded list:
- 100k quests would emit 100k quest_template + queststarter/ender
  INSERTs (huge SQL, slow validate, slow chain walks).
- 1M+ objects bloats the M2 instance SSBO and drags editor framerate
  to single digits.

Caps mirror the 256-waypoint cap added in the previous batch — log
a warning and drop the rest so the editor stays responsive.
2026-05-06 09:56:03 -07:00
Kelsi
8039dff51f fix(npc): cap patrol path length at 256 waypoints on load
A stale autosave or hand-edited creature.json could carry an
unbounded patrolPath. The SQL exporter would emit one waypoint_data
INSERT per entry and produce huge SQL files. 256 waypoints covers
any realistic route.
2026-05-06 09:54:25 -07:00
Kelsi
58e0069404 fix(sql): downgrade Wander to stationary when wanderRadius == 0
Same defect as the empty-Patrol case: Wander behavior with 0
radius would spawn a creature that pretends to wander but never
moves. Downgrade to stationary so the export reflects the actual
in-game behavior, matching the Patrol-without-waypoints fix.
2026-05-06 09:53:07 -07:00
Kelsi
0736b27ec7 fix(sql): downgrade Patrol behavior to stationary when path is empty
A creature with behavior=Patrol but an empty patrolPath would emit
movement_type=2 (waypoint) without any waypoint_data rows.
AzerothCore would log 'creature X has no waypoints' on every spawn
and the NPC would behave erratically. Fall back to stationary so
the spawn appears cleanly; user can fix the missing path after.
2026-05-06 09:52:16 -07:00
Kelsi
d07748398f feat(sql): warn about unsupported quest objective types in export
Pre-scans the quest list and emits a single header note when any
quest uses ExploreArea / EscortNPC / UseObject — those have no
direct quest_template column and need AzerothCore script_quest
hooks. Prevents silent dropping of objectives leaving an unfinished
quest in-game; the user sees the warning once at the top of
02_spawns.sql instead of having to grep through editor logs.
2026-05-06 09:51:38 -07:00
Kelsi
6b82196b7d fix(wcp): skip out-of-tree files at pack time
fs::relative can return '../foo' when the pack source is a symlink
that resolves outside the pack root. The unpacker rejects '..' or
absolute paths wholesale, so a single rogue symlink would ruin the
whole archive. Skip the offending file at pack with a warning so
the rest of the zone still ships.
2026-05-06 09:47:49 -07:00
Kelsi
2f56941ad2 feat(sql): export quest chain link to NextQuestInChain column
Quest.nextQuestId was captured by the editor and used by
validateChains for cycle detection, but never made it into the
AzerothCore quest_template SQL. Now resolves the editor-relative
ID to the matching SQL entry (startEntry + nextQuestId) and
writes it to the NextQuestInChain column. Players can now
auto-progress through quest chains in-game.
2026-05-06 09:46:26 -07:00
Kelsi
afd8e69a41 feat(sql): export quest reward items to RewardItem1-4 columns
Quest.reward.itemRewards entries were captured in the editor JSON
but never made it into the AzerothCore SQL export. Parse each
entry as a numeric item ID and emit RewardItem1-4 + count columns;
unparseable entries become 0 (skipped at quest grant time). 4 slot
limit matches AzerothCore's quest_template schema.
2026-05-06 09:45:23 -07:00
Kelsi
9309e62ab9 feat(sql): emit RequiredNpcOrGo for TalkToNPC quest objectives
Previously only KillCreature and CollectItem objectives translated
to SQL. AzerothCore reuses RequiredNpcOrGo for talk objectives
(count=1 indicates an interaction rather than a kill), so wire that
through and add a comment about which objective types need server
scripts (ExploreArea/EscortNPC/UseObject).
2026-05-06 09:44:23 -07:00
Kelsi
a0480dc3a2 feat(editor): add --info-zone CLI for printing zone.json fields
Mirrors the other --info-* family inspectors. Accepts either a
zone directory or the zone.json path directly. Prints every
manifest field: name, mapId, biome, baseHeight, tiles, flags,
audio config. Useful when diffing two zones or auditing the
audio/flag setup before packing.
2026-05-06 09:43:13 -07:00
Kelsi
7552880ca1 fix(npc): range-check waypoint waitTimeMs per-cell on load
Same per-cell range-check pattern as the JSON DBC fix: if a
waypoint's waitTime field is negative or > UINT32_MAX, the
.get<uint32_t> throws json::type_error and the outer try-catch
aborts the entire NPC file load on a single bad waypoint. Read
as int64, clamp to [0, 600000ms = 10-min cap].
2026-05-06 09:41:11 -07:00
Kelsi
e6b0a84f3a fix(wcp): cap pack file count at unpack limit (1M)
Pack previously trusted recursive_directory_iterator to terminate
naturally — fine on most zones but a hostile symlink loop or a
giant accidental subdirectory would produce an archive with > 1M
files, which the unpack header check rejects wholesale. Cap at the
unpack limit and log a warning so the resulting WCP is at least
loadable, even if incomplete.
2026-05-06 09:36:54 -07:00
Kelsi
377cfb32d3 fix(wcp): cap per-file size at pack to match unpack limit (256MB)
Pack previously accepted any file < 4GB and wrote it raw. Unpack
caps at 256MB and rejects the whole archive on overflow — so a
huge file in the source dir would silently produce an unpackable
WCP. Cap at pack and skip the body (size=0 entry) so the rest of
the pack remains usable.
2026-05-06 09:35:49 -07:00
Kelsi
eba6b941e5 docs(formats): document the complete headless CLI surface
The CLI grew from 6 to 19 commands across recent batches —
catalogue them in FORMAT_SPEC so users can discover the headless
workflow without grepping --help. Grouped by purpose: inspection,
validation, authoring, packaging, discovery.
2026-05-06 08:53:45 -07:00
Kelsi
341c07d412 feat(editor): add --regen-collision CLI for batch WOC rebuild
Walks a zone directory recursively, finds every WHM file, and
rebuilds the matching WOC. Useful after batch terrain edits when
you want to refresh collision for many tiles in one shot. Reports
per-tile triangle counts and exits 1 if any rebuild failed.
2026-05-06 08:53:12 -07:00
Kelsi
b88c555830 feat(editor): add --zone-summary CLI for one-shot zone overview
Combines validate + creature/object/quest counts in a single
output. Useful for CI reports and quick sanity checks. Exits 0
if open-format score is 7/7 (full coverage), 1 otherwise.
2026-05-06 08:39:38 -07:00
Kelsi
eb251639cf fix(editor): NaN-safe baseHeight propagation in addAdjacentTile
Source tile's chunks[0].position[2] could be NaN if mid-edit
terrain hadn't run stitchEdges yet. Fall back to 100.0 so the
adjacent tile doesn't start with poisoned base.
2026-05-06 08:37:19 -07:00
Kelsi
4cbffe17d5 feat(editor): add --diff-wcp CLI for archive comparison
Compares two WCP archives file-by-file from their info JSON: lists
added (+), removed (-), and size-changed (~) entries. Useful for
verifying that an authoring tweak changed only what it claimed to
change, and for editor-version regression detection. Exit code 0
if identical, 1 otherwise.
2026-05-06 08:29:21 -07:00
Kelsi
07f4043343 fix(viewport): clear ghost preview on NaN/non-positive inputs
Without this guard, NaN cursor position from a degenerate raycast
would feed directly into the M2 renderer instance transform and
either crash on GPU or silently render at the origin.
2026-05-06 08:24:51 -07:00
Kelsi
303eeb9107 fix(validate): require minimum body bytes when checking format magic
A 4-byte file with just the right magic and no body would pass
the previous magic-only check but fail any actual loader. Require
at least 8 bytes (magic + 1 field) for a file to count as 'valid'
in the score.
2026-05-06 08:24:08 -07:00
Kelsi
8fb7690ea1 feat(editor): add --export-png CLI for terrain preview rendering
Renders heightmap, normal-map, and zone-map PNGs alongside a
WHM/WOT terrain pair. Useful for portfolio screenshots, ground-
truth map comparison, and quick visual validation without
launching the GUI.
2026-05-06 08:22:26 -07:00
Kelsi
21078f8806 feat(editor): add --build-woc CLI for headless collision generation
Loads a WHM/WOT terrain pair and writes a .woc collision mesh
alongside it. Terrain triangles only (no WMO overlays — those need
the asset manager) but enough for first-pass walkability while
authoring.

Verified end-to-end: scaffold-zone → build-woc → info-woc reports
32k triangles for a flat 256-chunk tile.
2026-05-06 08:21:14 -07:00
Kelsi
81832ea676 feat(editor): add --pack-wcp CLI for headless zone packaging
Mirrors --unpack-wcp. Accepts either a zone name (auto-resolved
under custom_zones/ then output/) or a directory path. Default
output is <name>.wcp in the current directory. Combined with
--scaffold-zone and --unpack-wcp, the editor can do the full
zone authoring round-trip from the command line.
2026-05-06 08:19:42 -07:00
Kelsi
b2fa4cd509 feat(editor): add --unpack-wcp CLI for headless extraction
Mirrors --info-wcp / --list-wcp. Default destination is
custom_zones/ (matches the GUI's preferred location). With both
this and --scaffold-zone, the editor binary can fully bootstrap
a zone install without launching the GUI.
2026-05-06 08:18:21 -07:00