Commit graph

3789 commits

Author SHA1 Message Date
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
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
b7e3266c7a fix(m2): NaN guards on createInstanceWithMatrix boundary
Mirrors the createInstance guard. position drives the dedup hash
key (std::round of NaN is implementation-defined) and the matrix
flows into the GPU UBO.
2026-05-06 08:36:11 -07:00
Kelsi
61116e94a5 fix(m2): NaN guards on setInstancePosition and setInstanceTransform
Same boundary-rejection pattern as createInstance. NaN in either
function would corrupt the spatial grid (stale cells pointing at
NaN-bounded instances) and the GPU model-matrix UBO.
2026-05-06 08:34:53 -07:00
Kelsi
f96ea12fe7 fix(m2): reject NaN inputs at M2Renderer::createInstance
Even with all the upstream guards I've been adding, internal callers
or addon-style scripted spawns could pass NaN. Reject at the API
boundary so we never hash-key with NaN coords (std::round of NaN
is implementation-defined) or push a NaN instance into the model-
matrix uniform buffer (GPU crash / origin render).
2026-05-06 08:32:48 -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
Kelsi
ab5d574758 feat(editor): add --scaffold-zone CLI for empty zone bootstrap
Creates custom_zones/<slug>/ with a flat-terrain WHM, default WOT,
and minimal zone.json — score 3/7 on --validate, ready to open in
the GUI for further authoring. Saves the round-trip of launching
the GUI just to make a starter directory.
2026-05-06 08:17:33 -07:00
Kelsi
6657f08252 fix(wob): sanitize portal vertices/group indices on load and save
Three issues addressed:
- NaN portal vertices break the WMO portal-frustum cull, defeating
  the indoor optimization and forcing the whole interior to draw.
- Out-of-range portal.groupA/groupB indices walk past wmo.groups
  during cull; clamp to -1 (invalid) on load.
- Symmetric save-side scrub so we don't persist bad in-memory data.
2026-05-06 08:15:31 -07:00
Kelsi
9d37da45d8 test(wcp): add truncation detection test
Locks in the recent unpack truncation guard. Hand-writes a WCP
that declares dataSize=1000 but only ships 50 bytes; verifies
the unpack returns false AND that no partial file landed on disk.
2026-05-06 08:13:43 -07:00
Kelsi
aa2a70de8d fix(wcp): detect truncated WCP files on unpack
Previously a short read (truncated WCP, partial download, etc.)
would silently write the partial bytes that were read and report
success — leaving the consumer with a half-extracted zone that
would fail in confusing ways at runtime. Check gcount and return
false so the caller can refuse the broken pack.
2026-05-06 08:13:04 -07:00
Kelsi
44777c7d58 fix(mesh): clamp NaN terrain heights to 0 in vertex generation
WHM load already scrubs, but mid-edit terrain can briefly carry
NaN before stitchEdges runs. A single NaN vertex propagates into
normal computations and the chunk's frustum cull, crashing both.
2026-05-06 08:11:43 -07:00
Kelsi
19a4716ec1 fix(sql): scrub NaN coords/orientation when emitting INSERTs
ostream prints NaN as 'nan' which AzerothCore's SQL import rejects
with a syntax error — would silently break the entire export from
a single bad spawn. Defensive scrub at write time, mirroring the
load-side guard pattern used everywhere else.
2026-05-06 08:09:06 -07:00
Kelsi
a0876bbe3d fix(cli): error on non-GUI options that are missing their argument
Previously '--info-wcp' (no path) silently dropped into the GUI
because the option-parse loop's i+1<argc guard hid the typo. Pre-
scan and bail out with a helpful message before trying to start
the editor, so users get fast feedback on bad invocations.
2026-05-06 08:08:05 -07:00
Kelsi
2400271a4f test(brush): add 6 EditorBrush::getInfluence tests
Locks in the recent NaN/zero-radius/zero-falloff guards:
- inner radius returns full influence (1.0)
- at/beyond outer radius returns 0
- rim falls off as 1 - t² (smooth)
- NaN/inf distance rejected (returns 0, not 1 from short-circuit)
- zero/negative/NaN radius rejected
- zero falloff produces hard-edge brush
2026-05-06 08:05:43 -07:00
Kelsi
c9c4665642 fix(water): skip chunks with NaN water height in mesh build
Belt-and-braces — WOT load already scrubs the height to 0 if non-
finite, but if a downstream caller mutates terrain.waterData
in-memory the bad value would leak into the water mesh and Vulkan
would drop the entire batch on a single bad chunk.
2026-05-06 08:04:44 -07:00
Kelsi
55f9616aa6 fix(camera): NaN guards on pivot orbit, setPosition, setYawPitch
Three issues:
- processMiddleMouseMotion: NaN pivot poisons camera position
  permanently; the next frame produces NaN view/proj matrices.
- setPosition: no input validation — used by bookmark restore and
  fly-to-target which could be passed a NaN target.
- setYawPitch: same; also clamp pitch to [-89, 89] to match the
  mouse-motion path so a saved bookmark with bad pitch doesn't
  roll the camera upside down.
2026-05-06 08:03:57 -07:00
Kelsi
d03c96e3bd test(wcp): add unpack security tests
6 tests covering ContentPacker::unpackZone defenses:
- absurd fileCount header rejected
- absurd infoSize header rejected (16MB cap)
- relative path traversal ('../../etc/passwd_clone') rejected
- absolute paths ('/tmp/...') rejected
- malicious zone name slugified instead of escaping destDir
- bad magic rejected

Each test confirms the defense fires AND that no escape file
landed outside the test dir.
2026-05-06 08:03:02 -07:00
Kelsi
98f2a6c3bf fix(gizmo): hide on NaN target instead of building NaN geometry
setTarget previously stored the position raw, then updateBuffers
ran glm::normalize on axis offsets. NaN target → NaN normalized
axes → NaN gizmo vertices → Vulkan validation drops the whole
draw and the gizmo is invisible regardless of target value.

Hide the gizmo upfront so the user sees no gizmo (which is the
intent of the NaN handling) without leaking garbage into the
vertex buffer.
2026-05-06 08:01:23 -07:00
Kelsi
cdc9bb94ee fix(npc): NaN guards in NpcSpawner::selectAt distance test
Same NaN-comparison short-circuit pattern: NaN worldPos or NaN
spawn position would short-circuit dist < bestDist (NaN < x is
false), so it never updates bestIdx — but if every entry had NaN
the bestIdx stays -1 (correct) only because the first comparison
also fails. Belt and braces: skip NaN entries explicitly.
2026-05-06 08:00:36 -07:00
Kelsi
94469592f2 fix(objects): NaN guards in selectAt ray-sphere test
Without these, NaN ray or NaN object position would short-circuit
the disc < 0 early-out (NaN comparisons return false) and select
the object at a garbage t — silently 'picking' arbitrary objects.
2026-05-06 07:59:33 -07:00
Kelsi
0f15d0f3a0 fix(terrain): reject NaN rays in raycastTerrain
Without this, the AABB tests divide by ray.direction components and
NaN propagates through tmin/tmax into the triangle intersection,
returning undefined behavior at the hit position.
2026-05-06 07:58:56 -07:00
Kelsi
493cb68ddc fix(viewport): defensive NaN checks in patrol path ribbon
len < 0.001f returns false for NaN — same short-circuit class of
bug as elsewhere. Reject non-finite endpoints upfront and double-
check the computed length before dividing.
2026-05-06 07:57:54 -07:00
Kelsi
ba017193db fix(viewport): skip NPCs with NaN position in marker update
Same defensive pattern as updateObjectMarkers — non-finite NPC
position would produce NaN vertex positions and Vulkan would drop
the entire NPC marker batch, hiding every NPC marker in the zone.
2026-05-06 07:57:19 -07:00