Commit graph

3847 commits

Author SHA1 Message Date
Kelsi
1017ab4751 feat(editor): add --add-object CLI for headless M2/WMO placement
Mirrors --add-creature for the object placer:

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

Lets shell scripts populate objects/buildings without the GUI:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Schema:

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

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

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

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

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

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

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

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

Lets CI scripts gate on coverage:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  asset_extract --upgrade-extract Data/expansions/wotlk

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

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

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

Skips _NNN group sub-files when counting WMOs (only the root WMO
ships with a WOB sidecar). The headless CLI is now at 22 commands.
2026-05-06 10:50:17 -07:00
Kelsi
ea745005ce feat(runtime): pick up WHM/WOT/WOC sidecars from asset tree
Closes the loop on the asset_extract --emit-terrain pipeline. The
runtime terrain loader now probes for a .whm/.wot/.woc trio in the
same directory as the resolved ADT (e.g. <data>/world/maps/foo/
foo_30_30.{whm,wot,woc}) before falling back to ADTLoader.

Hits the open-format path when:
  custom_zones/<map>/<map>_X_Y.{whm,wot} exists  (zone author override)
  output/<map>/<map>_X_Y.{whm,wot} exists        (editor export)
  <data>/world/maps/<map>/<map>_X_Y.{whm,wot}    (asset extractor)
  -- otherwise falls through to ADTLoader::load(adtData)

Promotes AssetManager::resolveFile to public so callers (terrain
sidecar probe here, anything else later) can locate an extracted
file's directory without reading the bytes.

Servers/private servers continue to read .adt via manifest paths
unchanged. Runtime sidecar coverage now matches the extractor's
emit set across all five binary open formats.
2026-05-06 10:48:40 -07:00
Kelsi
b5ff9eb2a2 feat(runtime): pick up WOM/WOB sidecars from asset tree at load time
terrain_manager already attempts WOM/WOB via tryLoadByGamePath but
its prefix list only included custom_zones/ and output/. The asset
extractor's --emit-wom/--emit-wob writes sidecars next to the M2/WMO
in the asset tree itself (e.g. <data>/world/maps/foo/foo.wom).

Pass the AssetManager's data path as an extra prefix so the runtime
picks the open-format sidecar up there before falling back to the
proprietary M2/WMO load path. Completes the runtime side of the
dual-format extraction:

  AssetManager::loadTexture → tries .png sidecar first, then BLP.
  AssetManager::loadDBC     → tries .json sidecar first, then DBC.
  TerrainManager M2/WMO     → tries .wom/.wob sidecar first, then m2/wmo.

Servers/private servers see no change — they read from the proprietary
files via manifest paths and don't touch the sidecars.
2026-05-06 10:45:43 -07:00
Kelsi
1995ed9824 feat(runtime): pick up JSON DBC sidecars from --emit-json-dbc
AssetManager::loadDBC now probes for a .json sidecar in the same
directory as the binary DBC the manifest resolves to. asset_extract
--emit-json-dbc writes that sidecar on extraction, so the runtime
client transparently falls back to it when the binary DBC is absent
(e.g. open-format-only extraction for end-to-end format testing).

Order: binary DBC > JSON sidecar > custom_zones JSON > CSV > error.
Servers (AzerothCore/TrinityCore) only read the binary DBC, so this
change is invisible to them — the wowee runtime is the only consumer
that tries the JSON path.

The PNG sidecar pickup (tryLoadPngOverride) was already in place from
prior work — this completes the symmetric runtime-side wiring for
both BLP→PNG and DBC→JSON open-format outputs.
2026-05-06 10:43:13 -07:00
Kelsi
6872ba2bcb test(extract): lock in DBC→JSON emission round-trip
4 tests covering the open_format_emitter:
- emitJsonFromDbc round-trips a hand-built 2-record DBC through
  DBCFile::load (which auto-detects JSON via the '{' prefix) and
  recovers identical record/field/value data.
- Missing input file → graceful failure (no JSON written).
- Bad DBC magic → graceful failure.
- emitOpenFormats walks a subdirectory and writes the side-file
  in the right place (matches the extractor's recursive walk).

Brings ctest target count to 31.
2026-05-06 10:40:33 -07:00
Kelsi
d4c69a2b46 feat(extract): emit WHM+WOT+WOC for ADT terrain tiles
Final piece of the open-format emit pipeline:
  --emit-terrain  foo.adt → foo.whm + foo.wot + foo.woc

With this, --emit-open now produces a fully open-format zone
alongside every Blizzard MPQ extraction:
  BLP  → PNG       (textures)
  DBC  → JSON      (data tables)
  M2   → WOM       (models, with skin merge)
  WMO  → WOB       (buildings, with group merge)
  ADT  → WHM/WOT   (terrain heights + metadata)
       → WOC       (collision mesh derived from heights)

Originals stay on disk and indexed by manifest.json so private
servers continue to load proprietary formats; wowee runtime/editor
read the open formats directly. One extraction now feeds both
audiences with no separate conversion pass.

Implementation:
- Inline WHM+WOT writer in open_format_emitter.cpp (mirrors the
  editor's WoweeTerrain::exportOpen but without the PNG-preview /
  normal-map deps so the extractor stays editor-independent).
- Tile coords (x,y) parsed from <map>_<x>_<y>.adt filename.
- Collision mesh derived via WoweeCollisionBuilder::fromTerrain
  (terrain triangles only — WMO collision overlays would need
  asset manager and aren't worth the extractor complexity).
2026-05-06 10:36:14 -07:00
Kelsi
e6ace7cce5 feat(extract): emit WOM and WOB side-files (M2/WMO → open formats)
Extends asset_extract with two more open-format emitters:
  --emit-wom  foo.m2 (+ foo00.skin) → foo.wom
  --emit-wob  foo.wmo (+ foo_NNN.wmo groups) → foo.wob
  --emit-open now also turns these on

Originals are preserved so private servers still load .m2/.wmo
through the manifest path; the wowee runtime/editor pick up the
.wom/.wob next to them via the existing open-format search rules.

Implementation:
- New WoweeModelLoader::fromM2Bytes(m2Data, skinData) shares the
  conversion body with fromM2(path, am) via a static helper
  (convertM2ToWom). Lets the extractor convert without standing
  up an AssetManager.
- fromM2(path, am) moved to a separate translation unit
  (wowee_model_fromm2.cpp) so asset_extract doesn't have to
  link the AssetManager dependency.
- WoweeBuildingLoader::fromWMO already takes a WMOModel directly,
  so emitWobFromWmo just needs to read root + group files and
  call save().
- Group sub-files (<base>_NNN.wmo) are skipped during the walk
  since they're merged into the root WMO.
2026-05-06 10:32:17 -07:00
Kelsi
5ed2008621 feat(extract): emit open-format side-files (BLP→PNG, DBC→JSON)
The asset_extract tool now optionally writes wowee open-format
copies next to each extracted proprietary file:
  --emit-png      foo.blp → foo.png
  --emit-json-dbc foo.dbc → foo.json
  --emit-open     shortcut for both

Originals are left untouched, so private servers (AzerothCore,
TrinityCore) that load from the manifest's .blp/.dbc paths
continue to work unchanged. The wowee runtime / editor can now
consume the open formats directly without an extra conversion pass.

Implementation:
- New tools/asset_extract/open_format_emitter.{hpp,cpp} encapsulates
  the post-extract walk + per-file conversion.
- BLP→PNG uses BLPLoader::load + stbi_write_png with the same
  dimension/buffer-size sanity guards the editor's texture exporter
  applies.
- DBC→JSON mirrors the editor's DBCExporter::exportAsJson schema
  (string/float/uint heuristic) so the runtime DBC overlay loader
  can consume the output drop-in.
2026-05-06 10:23:32 -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
a531f70890 fix(dbc): skip non-array rows in loadJSON instead of failing
A JSON DBC with a malformed record (object instead of array, or
a string entry) would call row[col] which throws on non-arrays —
the outer try-catch treated this as a hard failure for the whole
DBC. Skip the row (stays zero-initialized) so a single malformed
record doesn't lose all the rest.
2026-05-06 10:07:49 -07:00
Kelsi
8e80f97bbc fix(wot): cap doodadNames/wmoNames at 65536 + guard non-string entries
Both name lists used n.get<std::string> which throws on non-string
entries (would abort the entire WOT load). Real zones use ~5k names
max; cap at 65536 (uint16 nameId range upper bound) so the cap is
generous but bounded. Guard with is_string so a single bad entry
just gets skipped instead of failing the file.
2026-05-06 10:06:20 -07:00
Kelsi
fc895ab564 fix(wot): cap textures/per-chunk layers + range-check int reads
Three issues:
- textures vector was unbounded (cap at 1024).
- Per-chunk layers vector was unbounded (cap at 8 — WoW ADT
  format supports 4, doubling for headroom).
- texId.get<uint32_t> and holes.get<uint16_t> would throw
  json::type_error on negative or oversize values, aborting the
  entire WOT load. Read as int64, clamp to the target range.
2026-05-06 10:04:45 -07:00
Kelsi
e7462efaf6 fix(wot): cap doodad/WMO placement count at 100k on load
Same defense pattern as the editor JSON loaders. Real ADTs cap at
~64k MDDF entries and ~5k in practice; 100k matches the editor
ObjectPlacer cap so an extreme WOT can't bloat the in-memory
terrain past what the editor itself would accept.
2026-05-06 10:03:08 -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
f76aebc4b9 test(objects): add load-time clamp + uniqueId round-trip tests
- ObjectPlacer load: scale=0 clamped to 1.0, missing scale field
  defaults to 1.0 (locks in the load-side guard at line 346).
- ObjectPlacer save→load: uniqueId values survive a full round
  trip (locks in the explicit uniqueId preservation behavior the
  ADT round-trip relies on).

Adds object_placer.cpp to test_editor_units sources.
2026-05-06 09:40:06 -07:00