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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
WOM1/WOM2 had a single mesh with one texture, which lost the multi-submesh
structure of complex M2 models (body+hair+eyes+armor each need different
textures and blend modes).
WOM3 adds a Batch array: each batch has indexStart/indexCount + a textureIndex
into texturePaths + blendMode + flags. Loader is fully backward compatible:
WOM1/WOM2 files still load, and WOM3 with no batches block falls back to a
single full-mesh batch. fromM2 now extracts batches with materials, and toM2
emits matching M2 batches so the renderer can draw them correctly.
Adds WoweeModelLoader::toM2() and tryLoadByGamePath() to deduplicate the
identical conversion code that lived in editor_viewport for both objects
and NPCs. Cuts ~70 lines of duplicated logic and makes WOM->M2 reusable
across the codebase.
Upgrades WOM from geometry-only (WOM1) to fully animated (WOM2):
- WOM2 magic (0x324D4F57) for animated models, WOM1 for static
- Vertex extended: +boneWeights[4] +boneIndices[4] (40 bytes vs 32)
- Bone data: keyBoneId, parentBone, pivot, flags per bone
- Animation data: per-sequence per-bone keyframes with translation,
rotation (quaternion), scale at millisecond timestamps
- fromM2() now preserves all skeletal data: bone hierarchy, weights,
and per-sequence keyframes from M2 animation tracks
- Backward compatible: WOM1 files load without bone data (32-byte
vertices read and padded with default bone weights)
- FORMAT_SPEC.md updated with WOM2 binary layout