Commit graph

3099 commits

Author SHA1 Message Date
Kelsi
185b7b522d fix(wob): sanitize boundRadius + per-group boundMin/Max at save time
Same float-NaN scrub for WoB save matching the WHM/WOC/WOM saves.
Building boundRadius defaults to 1.0 if non-finite; per-group bounds
zero out non-finite components (would otherwise corrupt the cull
frustum).
2026-05-06 06:39:49 -07:00
Kelsi
d25654d11a fix(wom): sanitize boundRadius/min/max floats at save time
Mirrors the WHM and WOC save sanitize. boundRadius defaults to 1.0 if
non-finite (matches load-time default); boundMin/boundMax components
zero out non-finite values. Prevents an in-memory model with a NaN
spike (e.g. mid-edit) from being persisted into the WOM and requiring
load-time cleanup forever after.
2026-05-06 06:36:46 -07:00
Kelsi
663d34af0d fix(woc): sanitize triangle vertices + bounds at save time
In-memory collision can be polluted by addMesh on bad input (e.g. a
WMO with NaN vertex positions). Without this, the save would persist
that NaN into the WOC and the load-time guards would have to clean
it up forever. Now scrubs vertices and bounds at write time, matching
the WHM save sanitize.
2026-05-06 06:35:05 -07:00
Kelsi
5019e21787 fix(wom): writeStr helper truncates length-prefixed strings to fit u16 length
Same fix as WoB save just got. Without truncation a model name or
texture path over 65535 chars would silently get a wrap-around length
and corrupt the file.
2026-05-06 06:25:33 -07:00
Kelsi
361ff712d7 fix(wob): writeStr helper truncates length-prefixed strings to fit u16 length
Without truncation, names over 65535 chars would silently get a wrap-
around length value and produce a corrupt file (the actual string would
be longer than the saved length, shifting everything after it). Single
shared writeStr lambda replaces five copy-pasted (uint16 length + bytes)
write blocks.
2026-05-06 06:23:35 -07:00
Kelsi
be64298218 fix(dbc): cap JSON DBC string sizes (4KB/string, 64MB total) + NaN floats
JSON DBC values are mostly small integers, but a malicious file could
stuff 100MB strings into every cell to OOM the stringBlock (which has
no per-string or total cap). Added 4KB per-string + 64MB total caps —
fields exceeding either are zeroed. Float fields also get NaN scrub
(same pattern as the earlier vertex/keyframe guards).
2026-05-06 06:19:01 -07:00
Kelsi
719951976d fix(wom+wob): reject path traversal in WOM texture paths + WOB material/group texPaths
Same defensive check as the WoB doodad path guard. Texture paths from
hostile WOM/WoB are passed to the asset manager; '..' or absolute paths
could probe outside the assets/ tree. Now cleared on detection — slot
survives but loads no texture (renderer falls back to white).

Single shared rejectTraversal lambda in WoB to avoid copy-paste.
2026-05-06 06:16:54 -07:00
Kelsi
c4463ba96e fix(wob): reject doodad paths with traversal/absolute components
Doodad model paths from a WoB are passed to the asset manager via
outModel.doodadNames. The asset manager only reads files, but '..' or
absolute paths from a hostile WoB could probe for files outside the
expected assets/ tree. Now clears the modelPath on traversal — the
doodad slot survives but loads no model.
2026-05-06 06:14:22 -07:00
Kelsi
b5a9ce7816 fix(assets): cap PNG override texture dimensions at 8K to prevent OOM
stbi_load happily decodes any PNG up to 32K x 32K — at 4 bytes/pixel
that's 4GB which OOMs the editor before the override even returns.
WoW textures top out at 4K; 8K cap leaves headroom for HD upgrades
without enabling abuse. Also widens the wxh multiplication to size_t
to defeat int overflow on 8K x 8K images.
2026-05-06 06:09:13 -07:00
Kelsi
2d8c843704 fix(dbc): cap JSON DBC fieldCount/recordCount to prevent OOM on hostile file
Real DBCs cap at ~250 fields and a few million records (Spell.dbc is
the biggest at ~50K rows). A malicious JSON DBC declaring fieldCount=
1G or recordCount * recordSize > 256MB would OOM the recordData
allocation. Now rejects upfront — JSON DBCs are user-shareable so a
zone export downloaded from a forum should not be able to OOM the
client by including a bad data table.
2026-05-06 06:07:09 -07:00
Kelsi
5b6f59bbbd fix(adt): scrub NaN/inf in MDDF + MODF placement floats
ADT placement positions and rotations are loaded as raw floats from the
binary chunks. A corrupted MDDF/MODF entry could feed NaN into the
M2/WMO instance transform and crash render. Now position/rotation are
scrubbed for both MDDF doodads and MODF buildings; MODF also scrubs the
extentLower/extentUpper bounding box used for cull tests.
2026-05-06 06:05:17 -07:00
Kelsi
c0c1be1c9e fix(wob): cap material/doodad path lengths + portal vertex count
Three more per-record sanity bounds that the per-group sweep didn't
cover:
  - material.texturePath length cap (1KB) — was unbounded
  - doodad.modelPath length cap (1KB) — was unbounded
  - portal.vertexCount cap (4096) — real portals are 4-12 verts;
    >4K is corrupt and would OOM the resize
2026-05-06 05:52:58 -07:00
Kelsi
93edf1bd55 fix(wob): per-group count sanity bounds + group name length cap
The WoB top-level header sanity bounds catch obviously-bad totals, but
each group's vc/ic/tc was still unbounded. A corrupted group could
declare 4G vertices and OOM the resize before the next group even
started. Now per-group: vc<=1M, ic<=4M, tc<=1K, name<=1KB.
2026-05-06 05:49:15 -07:00
Kelsi
cb7f11f2ea fix(wom): cap total keyframes at 10M to prevent OOM on hostile model
Per-bone-anim cap of 10K keyframes still let a malicious file allocate
up to 1024 anims × 512 bones × 10K keys = 5.24B keyframes — multi-GB
pre-OOM allocation. Now tracks total across the whole model and stops
allocating after 10M (real models stay well under 100K). When the cap
is hit we still seek past the remaining payload to keep file alignment
intact for whatever follows.
2026-05-06 05:46:55 -07:00
Kelsi
a7d62d1af9 fix(woc): reject corrupted triCount + clamp out-of-range tile coords
Header sanity bounds for WOC matching the WOM/WoB ones. A whole-tile
collision mesh maxes at ~32K terrain tris + a few thousand building
overlay tris; triCount > 2M is corrupted and would OOM. Tile coords
are 0..63 in WoW; clamp to 32 with warning when bogus.
2026-05-06 05:44:37 -07:00
Kelsi
90289ba48b fix(wob+wom): reject corrupted header counts before allocating
Adds upfront sanity bounds to both WoB and WOM load:
  WOM: vert<=1M, index<=4M, tex<=1K
  WOB: groups<=4K, portals<=8K, doodads<=64K
Real WoW models stay well under these limits (M2 vert is uint16 anyway).
Without these checks a corrupted header could trigger a multi-GB
allocation and OOM the process before we finish reading the body. Also
caps name length to 1KB on WoB load (already done on WOM).
2026-05-06 05:42:50 -07:00
Kelsi
53780de6da fix(wob): clamp out-of-range group indices + reject absurd texture path lengths
Mirrors the WOM index-clamp + texture-path-length guards. Out-of-range
indices into the WMO group's vertex buffer would crash the GPU draw.
Texture path length over 1KB indicates a corrupted/truncated WoB; clamp
to 0 to prevent allocating 65KB-string buffers per bad entry.
2026-05-06 05:36:20 -07:00
Kelsi
a2eaf3965a fix(wom): clamp out-of-range indices + reject absurd texture path lengths
Out-of-range indices were a silent vector overrun on the GPU side that
could crash the vertex shader on some drivers. Replace with 0 rather
than dropping so triangle counts stay aligned (a degenerate triangle is
harmless, an off-by-one indexing the wrong vertex is silent corruption).

Texture path length over 1KB is almost certainly a corrupted or
truncated file — was previously read into a 65KB-string allocation per
entry which could exhaust memory on a malicious file.
2026-05-06 05:34:41 -07:00
Kelsi
fd4354c17d fix(wom): fromM2 sanitizes vertex floats during conversion (matches WOB)
Same NaN scrub during fromM2 conversion that fromWMO got. Ensures a
corrupt source M2 (mangled MPQ block, partial extraction) doesn't
silently produce a NaN-laced WOM. Also feeds the cleaned positions
into boundMin/boundMax so the saved WOM bounds are clean too.
2026-05-06 05:32:47 -07:00
Kelsi
7acde76025 fix(wob): fromWMO sanitizes vertex floats during conversion
Sanitize at conversion time too, not just on WoB load. Avoids the
case where a corrupt source WMO (extracted from a partially-decoded
MPQ) silently poisons the WOB the editor exports — and the WOB
load-time guard from the previous commit only catches it on later
reload, not during the first conversion. Also catches the boundRadius
calculation which would otherwise inherit a NaN from one bad vertex.
2026-05-06 05:29:30 -07:00
Kelsi
2b5f69187e fix(woc): fromTerrain skips degenerate triangles before normalize
The walkability classifier did glm::normalize(cross(...)) without
guarding for zero-length cross. A flat-on-itself triangle (e.g. all
three vertices at the same height in a hole-edge case) produces NaN
normal, NaN walkability flag, and crashes the downstream nz check.
Now the cross-length is computed once and the triangle is skipped if
it's effectively zero.
2026-05-06 05:26:46 -07:00
Kelsi
70bbed4222 fix(woc): scrub NaN triangle vertices + skip degenerate triangles on load
NaN vertices in collision triangles produce NaNs in ray-triangle
intersection (used by movement collision queries), making the player
phase through walls or fall through the floor. Degenerate triangles
(zero-area, collinear) similarly produce NaN normals. Now both are
sanitized/skipped on load and the count is reported in the log.
2026-05-06 05:24:46 -07:00
Kelsi
a3fb267e0b fix(wom): sanitize per-keyframe translation/rotation/scale + movingSpeed NaN
Bone interpolation returns NaN for any NaN input. A single bad keyframe
in any animation would corrupt the entire skeleton during playback —
even bones that weren't being keyed in that animation got NaN final
matrices via parent-chain multiplication. Also catches movingSpeed which
leaks into the engine's displacement maths.
2026-05-06 05:22:32 -07:00
Kelsi
f0abd1794b fix(wom): sanitize bone pivot NaN + clamp out-of-range parentBone
Bones with NaN pivots produce broken skeleton matrices that ripple into
every child bone via the parent-chain multiplication. Out-of-range
parentBone indices would cause a use-after-free during bone-matrix
computation. Both now defensively clamped.
2026-05-06 05:19:24 -07:00
Kelsi
15648e21ec fix(wob): per-vertex NaN/inf scrub on load (matches WOM)
Same NaN guard as the just-applied WOM one. Sanitizes position/normal/
texCoord/color components after the bulk read. WMO renderer's matrix
math is sensitive — a single NaN position could desync the entire
group's draw state.
2026-05-06 05:16:16 -07:00
Kelsi
29ae850307 fix(wom): per-vertex NaN/inf scrub on load
Even after the bound-field guards, individual vertex floats (position,
normal, texCoord) could still poison the GPU. NaN positions would crash
the M2 vertex shader on some drivers (silent device-lost). Now each
component defaults to 0 (or 1 for normal Z) when non-finite — vertex
ends up at origin instead of corrupting the whole pipeline.
2026-05-06 05:14:35 -07:00
Kelsi
1467417b11 fix(wom): sanitize boundRadius / boundMin / boundMax against NaN/inf on load
WOM bound fields drive M2 culling and collision AABBs — non-finite
values would either cull the model out entirely or crash the cull math.
Now boundRadius defaults to 1.0 when invalid, and each boundMin/boundMax
component defaults to 0 when non-finite.
2026-05-06 05:12:53 -07:00
Kelsi
de983c2728 fix(whm): replace NaN/inf chunk base + per-vertex heights with 0.0 on load
A WHM with non-finite height values would produce non-finite vertex
positions in the terrain mesh, breaking collision queries, pathing,
and the GPU's matrix math. Both the chunk base (one float per chunk)
and the 145 per-vertex heights are now individually validated.
2026-05-06 05:10:04 -07:00
Kelsi
199f18cdac fix(wob): clamp doodad scale to 1.0 when loaded value is 0/NaN/inf
Without this guard, a corrupted or partially-written WoB with scale=0
would render the doodad at zero size (invisible) and a NaN/inf would
crash the renderer's matrix math. Now defaults to 1.0 for any non-
finite or near-zero value.
2026-05-06 04:57:09 -07:00
Kelsi
db068d480b feat(wob): tryLoadByGamePath helper, used by editor + terrain_manager
Mirrors the WOM tryLoadByGamePath API: probes custom_zones/buildings/ +
output/buildings/ by default, with optional extraPrefixes (e.g. per-zone
output/<map>/buildings/) checked first. Both the editor and the main
game's terrain_manager now use the helper, removing duplicate inline
lookup loops in two more places.
2026-05-06 04:10:12 -07:00
Kelsi
f36309a96f feat(wom): tryLoadByGamePath accepts extraPrefixes for per-zone search roots
The shared helper only probed custom_zones/models/ + output/models/, but
the editor's exportZone writes to output/<map>/models/. Added an
extraPrefixes parameter that's tried before the defaults — main game's
terrain_manager now passes ['output/<map>/models/', 'custom_zones/<map>/
models/'] so per-zone WOM exports override globals. Also removes the
last duplicate WOM-loading code from terrain_manager.
2026-05-06 04:07:16 -07:00
Kelsi
1d05bb0f13 fix(terrain): main game also propagates MODF scale to WMO instances
Mirrors the editor's WMO scale fix. WMOReady gains a scale field that
is computed from the loaded MODF placement.scale (u16 / 1024) and
forwarded to wmoRenderer->createInstance(). Without this the main
game ignored MODF scale even on WotLK ADTs that use it.
2026-05-06 03:55:14 -07:00
Kelsi
db1968f2cc feat(adt): preserve MODF nameSet + scale fields across load/save round-trip
WMOPlacement struct gains nameSet and scale fields (defaulting to 0 and
1024 = 1.0). The loader now reads them when the entry is the full 64
bytes (WotLK+); the writer emits the actual values rather than always
hard-coding (0, 1024). Older expansions still round-trip cleanly because
defaults match the previous behaviour.
2026-05-06 03:37:13 -07:00
Kelsi
5241bbd669 perf(editor): cache M2/WMO models across rebuilds, only clear instances
The editor's rebuildObjects path was destroying every cached model and
re-uploading it on every (debounced) change. Added M2Renderer::clearInstances
that drops only the instance list while keeping models loaded. Editor's
clearObjects switches to clearInstances (M2) + clearInstances (WMO),
and persistent path->modelId maps survive across rebuilds. clearTerrain
fully evicts when loading a new zone.
2026-05-06 03:23:06 -07:00
Kelsi
497997c50a fix(wob): doodad euler->quat uses glm convention to round-trip with fromWMO
Manual qx*qy*qz product was XYZ intrinsic, but fromWMO uses
glm::eulerAngles which returns YXZ Tait-Bryan extrinsic. The mismatch
introduced rotation drift on every save/load cycle. Using glm::quat
constructed from euler radians directly applies the inverse convention
of eulerAngles so the round-trip is clean.
2026-05-06 03:05:35 -07:00
Kelsi
560b4a9d35 feat(assets): PNG override probes custom_zones/textures and output/textures
The previous implementation required the .blp to exist in the asset
manifest before the PNG sidecar could be found. That prevented
custom-zone exports from shipping PNG-only textures (which is the whole
point of the open-format pipeline). Now if the manifest lookup fails
the loader probes well-known custom-zone roots so PNG-only assets work
out of the box.
2026-05-06 03:04:12 -07:00
Kelsi
a49b35e41b fix(wob): aggregate unique materials across ALL groups in toWMOModel
Previously only group[0]'s materials were emitted. Buildings with
group-specific materials (e.g., the indoor groups using a different
texture set than outdoor) lost those materials, leaving batches in
later groups pointing to materialIndex out of range. Now dedupes by
(texture, blend, flags) across every group.
2026-05-06 02:37:10 -07:00
Kelsi
eadb6a5886 feat(woc): add WMO collision meshes to exported zone collision
WoweeCollision previously only contained terrain triangles; placed WMO
buildings had no collision in the exported zone, so players could walk
through walls. Added WoweeCollisionBuilder::addMesh() that transforms a
local-space mesh into world space with slope-based walkability flags,
and the editor's exportZone now walks every placed WMO and feeds each
group's geometry through it. Indoor vs outdoor groups are tagged via
the WMO group flag.
2026-05-06 02:33:22 -07:00
Kelsi
4578bbc0d1 fix(wom): toM2 converts .png texture paths back to .blp for renderer
Same fix as WoB: M2Renderer's PNG override is keyed on .blp extension.
fromM2 writes .png paths to signal intent (PNG export pipeline), but
toM2 must convert back so the runtime engages the override and finds
the actual texture file.
2026-05-06 02:16:28 -07:00
Kelsi
aa6b692a58 fix(wob): convert .png texture paths back to .blp in toWMOModel
WMORenderer's PNG override system only triggers when the requested
texture path ends in .blp — it strips the .blp and probes for .png.
WoB stores .png paths (because fromWMO converts .blp->.png at conversion
time), so passing them straight through to the WMOModel meant the
override wasn't engaged and textures didn't load. Now converts back to
.blp so the existing override pipeline picks up the PNG file.
2026-05-06 02:15:13 -07:00
Kelsi
9d200fbe7b fix(wom): fromM2 always merges .skin file regardless of M2 isValid state
WotLK M2s store the header in .m2 but geometry in .skin. fromM2 only
loaded the skin when isValid() returned false, which it does for those
WotLK files — but missed the case where M2Loader::load happened to
populate enough that isValid() was true (older format M2s with newer
features). Now always merges skin data when present, matching the
editor's viewport loader behaviour.
2026-05-06 02:12:45 -07:00
Kelsi
318f255918 fix(wob): emit default doodadSet so WMO renderer draws WoB doodads
WMORenderer uses doodadSets[0] to select which slice of the doodads
array to render. Without a set entry the renderer skipped every doodad
even when toWMOModel populated them. Now emits a default set named
'Set_$DefaultGlobal' covering all doodads — matches Blizzard's default
set name and convention.
2026-05-06 02:09:24 -07:00
Kelsi
f2bbc8d60f feat(wob): toWMOModel restores doodad placements (interior props)
WoB stored doodad placements but toWMOModel never reconstructed them in
the WMOModel, so converted-back buildings rendered as empty shells.
Now rebuilds doodadNames + doodads with M2 path conversion (.wom -> .m2
for the runtime that hasn't picked up WOM-aware doodad loading yet) and
euler->quaternion rotation conversion.
2026-05-06 02:07:44 -07:00
Kelsi
804c7d3d60 fix(wom): toM2 handles WOM3 batches with empty textureLookup safely
Previously `m.textureLookup.size() - 1` would underflow to UINT_MAX when
texturePaths was empty, then std::min would clamp the bad value into the
batch. Renderer would either crash or sample bogus memory. Now treats an
empty lookup as textureIndex=0 (white-texture fallback path).
2026-05-06 02:00:52 -07:00
Kelsi
7d3eb59893 fix(wob): toWMOModel restores materials, textures, portals, group flags
Previously toWMOModel only copied vertex/index data — materials and
textures were dropped, so a converted-back WMO would render textureless
white walls. Now rebuilds the global texture index from material paths,
emits one WMOMaterial per WoB material, copies group bounds + outdoor
flag, and reconstructs MOPR portal refs from groupA/groupB so PVS data
survives round-trip.
2026-05-06 01:56:47 -07:00
Kelsi
a711a92875 refactor(terrain): use WoweeModelLoader::toM2 for WOM loading in main game
terrain_manager.cpp had a 70-line duplicate of the WOM->M2 conversion that
ignored WOM3 multi-batch support. Replaced with a single toM2() call.
Also extended toM2 to copy bones and animation sequence headers so the
shared helper now produces a fully renderable M2Model from any WOM
version, with main game and editor both using the same code path.
2026-05-06 01:38:31 -07:00
Kelsi
23951d4075 fix(wom): fromM2 sets version=3 when batches were extracted
Without this fromM2 always wrote version=2 even when batches were
populated, causing the version field on the in-memory model to lie
about its content. The save() magic-byte selection happens off the
batches/animation flags directly so the on-disk file is still correct,
but loaders that key off model.version saw stale info.
2026-05-06 01:36:37 -07:00
Kelsi
13a7adffab fix(wom): validate WOM3 batch ranges to reject corrupt files safely
Without bounds checks, a corrupted WOM3 file with invalid indexStart/
indexCount/textureIndex would feed bad ranges to the M2 renderer and
crash on draw. Now each batch is validated against the loaded indices
and texturePaths arrays; out-of-range batches are warned and dropped.
2026-05-06 01:32:59 -07:00
Kelsi
6f88eb270a feat(wob): extract portals from WMO during conversion
WoweeBuildingLoader::fromWMO previously left the portals array empty,
losing portal/PVS data essential for indoor visibility culling. Now reads
the WMO portal vertex polygons and pairs them with their two adjacent
groups via MOPR refs, populating WoweeBuilding::Portal fully.
2026-05-06 01:20:29 -07:00
Kelsi
e342f2ba55 chore(wmo): demote 'cleared all WMO models' message to INFO level 2026-05-06 01:16:18 -07:00