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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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
New format: WOC (Wowee Open Collision) — binary collision mesh for
custom zone walkability. Magic WOC1 (0x31434F57).
- WoweeCollisionBuilder::fromTerrain() generates collision triangles
from terrain heightmap with slope classification (50 deg threshold)
- Per-triangle flags: walkable (0x01), water (0x02), steep (0x04)
- Respects terrain holes (skips triangles in hole regions)
- Binary save/load with bounds, tile coords, triangle data
- Auto-exported on zone save alongside WOT/WHM/WOM/WOB
- Added to content pack validation (score now 0-7)
- FORMAT_SPEC.md v1.1 updated with WOC binary layout
- 19 new test assertions: flat terrain generation (32k tris all
walkable), save/load round-trip, hole skipping
- 328 total assertions across 84 test cases
- WOB save/load now serializes Material struct fields (flags, shader,
blendMode, texturePath) per group — was saving only texture paths
- FORMAT_SPEC.md v1.1: documents WOT doodad/WMO placements, WOB
material fields, doodad rotation, terrain stamps, WCP file list
- Test coverage: 5 new assertions verify material round-trip (flags,
shader, blendMode all preserved through save→load cycle)
- 260 total assertions across 75 test cases, all passing
Architecture fixes for open format data fidelity:
- WOT now serializes full doodad/WMO placement arrays (positions,
rotations, scale, flags, doodad sets) — was only storing counts,
causing all placed objects to be lost on WOT round-trip
- WOT loader parses placements back into ADTTerrain for client rendering
- WOB Material struct added: preserves WMO material flags, shader type,
and blend mode during WMO→WOB conversion (was geometry-only)
- WOB doodad rotation: quaternion→euler conversion instead of hardcoded
zero (placed doodads inside buildings now retain their orientation)
- importOpen() deduplicated: delegates to pipeline::WoweeTerrainLoader
instead of duplicating 100 lines of parsing code
- WHM binary now includes per-chunk alpha map data (alphaSize + data)
so custom zones render with proper texture blending in the client
- WOT exporter rewritten with nlohmann/json (was manual string concat)
- WOT loader rewritten with nlohmann/json (was naive substring parsing)
- Backward compatible: old WHM files without alpha data still load fine
- Wire WOB buildings into WMO render pipeline (loads→converts→renders)
- Implement JSON DBC loading in DBCFile::loadJSON() with nlohmann/json
- Wire JSON DBC override into AssetManager (custom_zones/output scan)
- Add WMO→WOB conversion with full geometry (fromWMO)
- Replace placeholder WOB export with real WMO→WOB conversion in editor
- Add --convert-wmo CLI flag for batch WMO→WOB conversion
- Store discovered custom zones on Renderer with getCustomZones() accessor
- Add isCustomZone_ member to TerrainManager
All 6 Blizzard format replacements now fully load in the client:
ADT→WOT/WHM, WDT→zone.json, BLP→PNG, DBC→JSON, M2→WOM, WMO→WOB
- WoweeBuildingLoader::toWMOModel() converts WOB groups to WMOModel
with vertices, indices, normals, texCoords, and vertex colors
- TerrainManager now loads WOB files from custom_zones/buildings/
and converts to WMOModel for the WMO renderer pipeline
- WMOGroup indices converted from uint32 to uint16 for renderer compat
Client open format support — 4 of 6 now loading:
- FULL: WOT/WHM terrain, PNG textures, WOM models
- LOAD: WOB buildings (converts to WMOModel, render pipeline TODO)
- DETECT: zone.json (scanned), JSON DBC (scanned)
- TerrainManager now checks for .wob files before loading WMO buildings
(searches custom_zones/buildings/ and output/MapName/buildings/)
- AssetManager::loadDBC() scans custom_zones/*/data/ for JSON DBC
overrides exported by the editor
- WOB detection logs when found (full WOB→WMOModel conversion pending)
- JSON DBC detection logs when found (full JSON→DBCFile loading pending)
Client open format support status:
- WOT/WHM terrain: FULL (loads and renders)
- PNG textures: FULL (override system)
- WOM models: FULL (loads and renders)
- zone.json: DETECTION (CustomZoneDiscovery scans)
- WOB buildings: DETECTION (found, conversion pending)
- JSON DBC: DETECTION (found, loading pending)
ALL 6 BLIZZARD FORMATS NOW HAVE OPEN REPLACEMENTS.
WOB format: binary building file with groups, portals, and doodads.
- WOB1 magic header
- Groups: vertices (pos+normal+uv+color), indices, texture paths,
bounding box, indoor/outdoor flag
- Portals: connect groups with polygon boundaries
- Doodad placements: reference .wom models with transform
WoweeBuildingLoader: load/save/exists for .wob files.
Complete format replacement table:
- ADT → WOT/WHM (terrain heightmaps + metadata)
- WDT → zone.json (map definition)
- BLP → PNG (textures)
- DBC → JSON (data tables)
- M2 → WOM (static models)
- WMO → WOB (buildings with groups/portals/doodads)
The wowee project now has a complete suite of novel, open file
formats for creating and distributing custom WoW content without
any dependency on Blizzard proprietary file formats.
- CustomZoneDiscovery scans directories for zone.json manifest files
- Discovers custom zones in custom_zones/ and output/ directories
- Reports: name, author, description, creature/quest availability
- Client can list all available custom expansions at startup
- Foundation for a zone selection menu in the client UI
The wowee client can now load custom zones exported from the editor
using the novel WOT/WHM format — no Blizzard files needed.
Loading priority in TerrainManager::prepareTile():
1. Check custom_zones/{mapName}/{mapName}_{x}_{y}.wot/.whm
2. Check output/{mapName}/{mapName}_{x}_{y}.wot/.whm (editor output)
3. Fall back to World\Maps\...\*.adt (standard extracted data)
Pipeline:
- WoweeTerrainLoader in src/pipeline/ (shared between client + editor)
- Loads .whm binary heightmap (WHM1 magic, 256 chunks × 145 floats)
- Loads .wot JSON metadata (textures, layers, holes, water)
- Populates the same ADTTerrain struct the mesh generator uses
- obj0 merge only runs for ADT-loaded tiles (custom zones have no obj0)
To use: export zone from editor → files appear in output/ → client
loads them automatically on next terrain request for that map name.
Add complete spell visual pipeline resolving the DBC chain
(Spell → SpellVisual → SpellVisualKit → SpellVisualEffectName → M2)
with precast/cast/impact phases, bone-attached positioning, and
automatic dual-hand mirroring.
Ribbon rendering fixes:
- Parse visibility track as uint8 (was read as float, suppressing
all ribbon edges due to ~1.4e-45 failing the >0.5 check)
- Filter garbage emitters with bone=UINT_MAX unconditionally
- Guard against NaN spine positions from corrupt bone data
- Resolve ribbon textures via direct index, not textureLookup table
- Fall back to bone 0 when ribbon bone index is out of range
Particle rendering fixes:
- Reduce spell particle scale from 5x to 1.5x (was oversized)
- Exempt spell effect instances from position-based deduplication
Spell handler integration:
- Trigger precast visuals on SMSG_SPELL_START with server castTimeMs
- Trigger cast/impact visuals on SMSG_SPELL_GO
- Cancel precast visuals on cast interrupt/failure/movement
M2 classifier expansion:
- Add AmbientEmitterType enum for sound system integration
- Add 20+ foliage tokens, 4 spell effect tokens, isSmallFoliage flag
- Add markModelAsSpellEffect() to override disableAnimation
DBC layouts:
- Add SpellVisualID field to Spell.dbc for all expansion configs
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
When spline parsing consumes the wrong number of bytes, the subsequent
blockCount read lands on garbage data (e.g. 71 instead of ~5 for UNIT).
Previously the parser logged a warning but continued, reading garbage
mask/field data until hitting truncation. Now it returns false for
CREATE_OBJECT blocks with suspicious counts, letting the block loop
skip cleanly to the next entity.
Also downgrade ~44 diagnostic LOG_WARNING messages to LOG_DEBUG across
17 files (equipment, transport, DBC, heartbeat, chat, GO raypick, etc.)
to reduce log noise and make real warnings visible.
- Delete 4 legacy GLSL 330 shaders (basic.vert/frag, terrain.vert/frag)
left over from OpenGL→Vulkan migration — Vulkan equivalents exist as
*.glsl files compiled to SPIR-V by the build system
- Delete orphaned mpq_manager.hpp/cpp (694 lines) — not in CMakeLists,
not included by any file, unreferenced StormLib integration attempt
- Add comments to water.frag.glsl wave constants explaining the
multi-octave noise design: non-axis-aligned directions prevent tiling,
frequency increases and amplitude decreases per octave for natural
water appearance
- vk_pipeline: extract kColorWriteAll constant from 4 duplicated RGBA
bitmask expressions across blend mode functions, with why-comment
- frustum: name kMinNormalLenSq epsilon (1e-8) with why-comment —
prevents division by zero on degenerate planes
- dbc_loader: add why-comment on DBC field width validation — all
fields are fixed 4-byte uint32 per format spec
- pin_auth: replace 0x30 hex literal with '0' char constant, add
why-comment on ASCII encoding for server HMAC compatibility
- m2_loader: define kM2SeqFlagEmbeddedData (0x20) with why-comment —
when clear, keyframe data lives in external .anim files and M2 offsets
are file-relative (reading them from M2 produces garbage). Replaces
3 bare hex literals across parseAnimTrack and ribbon emitter parsing
- audio_engine: replace empty for-loop iterator advance with
std::advance() for clarity
- asset_manager: add size guard before fsPath.substr(size-4) in
tryLoadPngOverride — resolveFile could theoretically return a
path shorter than the extension
- wmo_loader: name kDoodadNameIndexMask (0x00FFFFFF) with why-comment
explaining the 24-bit name index / 8-bit flags packing and MODN
string table reference
- window: add why-comment on LOG_WARNING usage during shutdown —
intentionally elevated so teardown progress is visible at default
log levels for crash diagnosis
offset + length was computed in uint32_t before comparing to size_t.
A crafted M2 with offset=0xFFFFFFFF, length=2 wraps to 1 in uint32,
passing the check and reading out of bounds. Now uses size_t arithmetic,
matching the readArray fix from an earlier round.
count * sizeof(T) was computed in uint32_t — a large count value from a
crafted WMO file could wrap to a small number, pass the bounds check,
then attempt a multi-GB allocation causing OOM/crash. Now uses 64-bit
arithmetic with a 64MB sanity cap, matching the M2 loader pattern.
Final sweep across mpq_manager, application, auth_screen, wmo_renderer,
character_renderer, and terrain_manager. All now use the unsigned char
cast pattern. No remaining bare ::tolower/::toupper or std::tolower(c)
calls on signed char in the codebase.
std::strlen on raw MWMO chunk data has no upper bound if the chunk
lacks a null terminator (truncated/corrupt WDT file). Replaced with
strnlen bounded by chunkSize, matching the ADT parser fix in d776226f.
static int logCount/batchLogCount inside the per-group parse loop
accumulated globally, so after the first WMO with many sub-chunks
loaded, no subsequent WMO group would ever log. Changed to function-
local / loop-index-based throttle so each group gets its own window.
The doodad set name read used raw memcpy(20 bytes) bypassing the safe
read<T> template that returns {} on OOB. A truncated WMO file would
read past the vector's storage. Added bounds check before the memcpy.
ARGB8888 decompression read pixelCount*4 bytes from mipData without
checking that mipSize was large enough — a truncated BLP caused heap
OOB reads. Also, 'int pixelCount = width * height' overflowed for
large dimensions (signed int UB). Now validates dimensions <= 4096,
uses uint32_t arithmetic, and checks mipSize >= required for ARGB8888.
1. MCNK sub-chunk bounds checks didn't account for the 8-byte header
skip, so parseMCVT/parseMCNR could read up to 8 bytes past the
validated buffer when sub-chunk headers are present (the common case).
2. parseMTEX/parseMMDX/parseMWMO used unbounded strlen on raw chunk
data. A truncated file without a null terminator would read past the
chunk boundary. Replaced with strnlen bounded by remaining size.
Also removes dead debug code: empty magic buffer copy, cathedral WMO
search, and Stormwind placement dump (which also had ::toupper UB).
readArray was called inside the loop on every iteration, re-parsing the
entire flat key array via memcpy. For a model with 200 sequences and
10k keys this produced ~24MB of redundant copying. Now reads once before
the loop (matching how allTimestamps was already handled).