Commit graph

3817 commits

Author SHA1 Message Date
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
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
d85073241e test(dbc): add hardening tests for absurd recordCount/fieldCount
Locks in the recent DBC overflow guards:
- recordCount=1B + recordSize=1024 (would overflow uint32 product)
- fieldCount=65535 (would multiply to 256KB record size)

Both load() calls return false instead of allocating tiny buffers
that get memcpy'd from TB of memory.
2026-05-06 09:34:39 -07:00
Kelsi
269e0a02ef fix(dbc): range-check JSON DBC integer fields per-cell
val.get<uint32_t>() throws on negative or > UINT32_MAX. The
outer try-catch would then abort the entire JSON DBC load on a
single bad cell. Read as int64_t, clamp to [0, UINT32_MAX], and
zero out anything out of range — matches the per-field NaN scrub
applied to floats one branch up.
2026-05-06 09:33:17 -07:00
Kelsi
64b85ff9ff fix(whm): reject load on overlong per-chunk alphaSize
Same load-desync pattern as elsewhere — alphaSize > 65536 silently
skipped the read but the actual alpha bytes were still on disk, so
the next chunk's baseHeight float read would parse alpha bytes.
Now rejects the load with LOG_ERROR.
2026-05-06 09:31:36 -07:00
Kelsi
9facea14a7 fix(dbc): reject absurd header values + use 64-bit size math
DBCFile::load multiplied recordCount * recordSize as uint32 (line
108), so a header with recordCount=1B and recordSize=1024 would
wrap to a tiny size — resize allocates ~tiny, memcpy reads ~TB
of memory and crashes.

Reject impossible header values up front (10M records / 1024
fields / 16KB record / 256MB string block) and use uint64_t for
the file-size sanity check + size_t for the resize/memcpy product
so the bounds-check is the only path that allows large counts.
2026-05-06 09:29:24 -07:00
Kelsi
9f2e8e1979 test(wob): add 2 string-length reject tests
Locks in the recent silent-corruption fix:
- Overlong building name (5000 > 1024) → load returns invalid
  instead of silently zeroing the length and reading 5000 stale
  bytes as the next group's name+counts.
- Overlong group name (9999 > 1024) → same.

Catches regression of the silent-desync defect that affected 7
length fields across the WoB and WOM loaders.
2026-05-06 09:26:08 -07:00
Kelsi
bbd2e0502b fix(wom): reject load on out-of-range string lengths
Same silent-corruption pattern as WoB: model.name had no length
check at all (would happily allocate 64KB), and texture paths
silently zeroed pathLen on overflow leaving the actual bytes on
disk to shift the rest of the file. Now reject with LOG_ERROR.
2026-05-06 09:24:55 -07:00
Kelsi
d818ff382c fix(wob): reject load on out-of-range string lengths
Building name, group name, group texture path, material texture
path, and doodad model path all had the same defect: when the
length field exceeded 1024 the loader silently set the local
counter to 0 and skipped the read — but the actual string bytes
were still on disk, so the next read interpreted them as the next
length+data pair and the whole rest of the file desynced.

Now reject the whole load on each oversize length with an explicit
LOG_ERROR. Save caps at 1024 so this only triggers on hand-crafted
or future-version files, but the failure mode was severe enough
(silent zone corruption, not a clean error) to warrant the fix.
2026-05-06 09:23:19 -07:00
Kelsi
17f67e3ec8 fix(wob): reject load on out-of-range material count
Previously load silently skipped the materials block when mc > 256,
leaving the file pointer right after the count — the next group's
name would then read material bytes as garbage and the rest of the
file would shift. Save now caps at 256 (so the asymmetry shouldn't
trigger from our own writer), but a hand-crafted or future-version
WoB could still hit it.
2026-05-06 09:19:24 -07:00
Kelsi
0bf7c8ac3f test(open-formats): add 2 round-trip cap tests
Locks in the recent save-side count caps:
- WoB save with 1500 texture paths → load reads exactly 1024,
  first/last entries match what was written before the cap fired.
- WOC save with tileX/tileY=200 → load reads tileX/tileY=32
  (clamped at write, no warning on the second reload).

Catches the asymmetry that would silently drop everything past
the load limit.
2026-05-06 09:17:48 -07:00
Kelsi
63dbfe74b0 fix(wom): cap top-level vert/index/tex counts on save
Top-level WOM save was writing raw model.vertices/.indices/.texturePaths
sizes; load enforces 1M / 4M / 1024 limits. A pathological model would
emit a header rejected on load, leaking the rest of the file body.

Cap each count at the load limit and iterate the WOM1 vertex block +
texture-path block by index so the body matches the header.
2026-05-06 09:16:43 -07:00
Kelsi
b85734e311 fix(wom): cap WOM3 batch count at load limit (4096) on save
Same per-section cap pattern. The loader caps batchCount at 4096;
save iterated all validBatches without checking. A model with
>4096 batches would write a header rejected on round-trip.
2026-05-06 09:15:02 -07:00
Kelsi
0547ab882a fix(wob): cap per-portal vertex count on save to match load limit
Same per-section cap pattern. Real portals carry 4-12 verts; the
load enforces 4096 max. Save previously wrote raw size() so a
huge portal would write a header the loader rejects.
2026-05-06 09:13:34 -07:00
Kelsi
30d1acbeee fix(wob): cap per-group vertex/index/texture counts on save
Per-group counts were uncapped on save while load enforced 1M
vertices, 4M indices, 1024 texture paths. A single huge group
exceeded any cap would write a header the loader rejects, leaking
the rest of the file body into a misread chain.

Cap counts at the load limits and iterate the texture-path block
by index so the body matches the header on round-trip.
2026-05-06 09:12:17 -07:00
Kelsi
2cd69d677a fix(wob): cap group/portal/doodad header counts on save
WoB load enforces 4096 groups / 8192 portals / 65536 doodads. Save
previously wrote raw size() and iterated all entries — a build
exceeding any cap would be rejected wholesale on round-trip.

Cap each count at the load limit and use indexed loops so the
written body matches the header count even if the in-memory data
goes over.
2026-05-06 09:10:14 -07:00
Kelsi
c5008750ce fix(woc): cap triangle count and clamp tile coords on save
WOC load caps tris at 2M and clamps tile coords to 0..63. Save
previously wrote raw size() and tileX/Y — a >2M-tri collision
would be silently rejected on round-trip, and OOR tile coords
would log a warning every reload. Cap at save and reuse the
load-side clamp so the on-disk file is round-trip clean.
2026-05-06 09:08:07 -07:00
Kelsi
241722feaa fix(wom): cap bone/animation counts at save (matches load limits)
WOM load caps bones at 512 and animations at 1024. Save previously
wrote raw size() and iterated all entries — a model with >512 bones
would write fine but truncate on round-trip, and the post-truncation
keyframe data would be misread as the next animation.

Cap both counts at save and iterate using the capped value so the
per-bone keyframe block stays aligned with what load expects.
2026-05-06 09:06:46 -07:00
Kelsi
4a534c24e8 fix(wob): cap material count at 256 on save (matches load limit)
Save previously wrote raw materials.size() as the count, then iterated
all materials. Load caps at 256, so a build with >256 materials would
write fine but truncate on round-trip and the post-truncation block
would be misread as the next group's data. Cap at save and only write
the first 256.
2026-05-06 09:04:18 -07:00
Kelsi
4d6c65ab73 test(wob): add save-side scrub tests for NaN portals and vertices
Two new round-trip tests verify the save-side hardening:
- NaN portal vertices and out-of-range groupA/groupB indices are
  cleaned by save → load reads back finite verts and groupA/B = -1.
- NaN bld.boundRadius, group bounds, and vertex position/normal
  are scrubbed to safe defaults (1.0 boundRadius, zero pos, +Z up).

Locks in the recent WoB scrub work and ensures the on-disk format
stays self-consistent.
2026-05-06 09:00:03 -07:00
Kelsi
1d1cbafa95 test(camera): add 6 setter/basis-degeneracy tests
Locks in the recent Camera setter NaN/range guards and the
getRight/getUp fallback when forward is parallel to world up:
- setPosition rejects NaN/inf
- setRotation rejects NaN
- setFov rejects NaN/0/negative/>=180
- setAspectRatio rejects NaN/<=0
- getRight/getUp return finite at +/-89 pitch (clamped path)
- getRight/getUp degrade safely at exactly +/-90 (crosses to zero)

Brings ctest target count to 30.
2026-05-06 08:58:55 -07:00
Kelsi
500ad2e711 fix(camera): NaN/range guards on Camera setters
setPosition/setRotation/setAspectRatio/setFov now reject:
- NaN/inf inputs (would produce NaN view/proj matrix → frozen GPU
  on some drivers, garbage frustum culling everywhere)
- aspectRatio <= 0 (degenerate perspective)
- fov <= 0 or >= 180 (degenerate perspective)

Camera is constructed and set from many code paths; pushing the
guards into the setters means none of them need to remember.
2026-05-06 08:57:31 -07:00
Kelsi
39f4a433ff fix(camera): NaN-safe getRight/getUp when forward ~= world up
If forward is parallel to (0,0,1) — camera staring straight up or
down — the cross product is zero and glm::normalize returned NaN.
That NaN flowed into glm::lookAt and produced a NaN view matrix.

The editor camera clamps pitch to +/-89 so it doesn't trigger,
but other call sites or scripted test paths could construct a
Camera at +/-90 and immediately blow up. Length-check the cross
and fall back to world +X / +Z.
2026-05-06 08:55: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
cfd257aa78 fix(m2): skip NaN vertices when computing tight model bounds
glm::min/max on NaN is implementation-defined, so a single bad
vertex would propagate NaN into the camera-occlusion and culling
AABB used by the runtime. WOM/M2 loaders already scrub but defense
in depth catches anything they miss. Falls back to a unit box if
every vertex is bad.
2026-05-06 08:51:31 -07:00
Kelsi
61fb486b9e fix(m2): degenerate-normal guard during walkable-floor raycast
The matrix-transformed normal could be near-zero if the M2 instance
has a degenerate scale; glm::normalize then returns NaN that
contaminates the slope check (NaN < 0.35 is false → no early-out)
and bestNormalZ goes NaN, breaking the walkable-floor heuristic.

Length-check the transformed normal and fall back to the (0,0,1)
flat default — same pattern as the WMO renderer.
2026-05-06 08:49:26 -07:00
Kelsi
ae20fbf621 fix(wmo): degenerate-normal guard during tangent generation
A WMO vertex with zero-length or NaN normal would produce a NaN
normalized normal, contaminating the Gram-Schmidt tangent for the
whole vertex and producing visibly broken normal mapping for the
affected face. Length-check before normalize and fall back to
(0,0,1) when degenerate.
2026-05-06 08:47:31 -07:00
Kelsi
86c544b841 fix(wmo): degenerate-portal guard during portal plane build
A portal whose first three vertices are coincident or collinear
produces a zero cross product and glm::normalize returns NaN. The
NaN propagates into the portal-frustum cull (every interior group
either always-visible or never-visible depending on plane orientation).

Use the same length-check pattern as the editor's spline/path code:
zero cross → fall back to (0,0,1) up-axis.
2026-05-06 08:46:20 -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