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.
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.
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.
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.
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.
setGhostPreview reused modelId 59999 for every preview, but loadModel
returns true without doing anything when the ID is already cached. So
selecting a new NPC kept the old ghost model in GPU memory and createInstance
used the stale model. Added M2Renderer::unloadModel public API and call it
from clearGhostPreview.
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
- TerrainManager loads WOC collision meshes alongside WOT/WHM terrain
from both custom_zones/ and output/ directories
- CollisionData stored per-tile with triangle array + bounds
- isPositionWalkable(x, y): returns whether a world position is on
walkable terrain (barycentric point-in-triangle test)
- getCollisionFlags(x, y): returns per-triangle flags (walkable,
water, steep, indoor) for movement system integration
- Defaults to walkable when no collision data is loaded (backward compat)
- Custom zone players now have proper terrain physics boundaries
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
- WOM→M2 conversion now creates proper render batch (indexStart,
indexCount, vertexStart, vertexCount), texture references, material
with opaque blend mode — WOM models are now actually visible in the
client renderer instead of being invisible geometry-only shells
- 5 new JSON DBC test cases: basic load with strings/ints, float values,
empty records rejection, missing records key rejection, findRecordById
lookup across 3 records
- Total: 278 assertions across 80 test cases, all passing
- 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)
- Renderer::initializeRenderers() now scans custom_zones/ and output/
for zone.json manifests on first initialization
- Logs all discovered custom zones with name, directory, and
NPC/quest availability indicators
- One-time scan (cached after first run)
- Foundation for custom zone selection menu in client UI
Client open format support:
- FULL: WOT/WHM terrain, PNG textures, WOM models (load + render)
- DETECT: zone.json (scanned at startup), WOB buildings, JSON DBC
- 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)
- TerrainManager now checks for .wom files before loading M2 models
- Searches custom_zones/models/ and output/MapName/models/ directories
- Converts WOM vertices/indices to M2Model struct for the renderer
- Full pipeline: editor exports M2→WOM → client loads WOM directly
- Falls back to standard M2 loading if no WOM found
The client can now render custom zone content using entirely
open formats: WOT/WHM terrain + WOM models + PNG textures
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.
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.
Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format
Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)
Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel
NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
(Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON
Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format
Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed
Known issues:
- M2/WMO rendering may not display on first placement (frame index
sync between update/render was misaligned — now fixed but untested
end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
The server can persist a corrupted near-origin position on map 0 (from a
faulty area-trigger destination) across sessions. On re-login it sends the
bad position via LOGIN_VERIFY_WORLD; if the player walks into the offending
trigger again the server re-teleports there, and our heartbeats reinforce
the bad save — creating a permanent teleport loop.
Defenses added:
- handleTeleportAck rejects MSG_MOVE_TELEPORT to near-origin on map 0
(no position update, no ACK, no world reload)
- applyPlayerTransportState rejects player UPDATE_OBJECT MOVEMENT blocks
pushing the same bad position
- sendMovement blocks heartbeats originating from near-origin so the
server cannot persist the bad save
- 10-second area-trigger cooldown after teleport / world entry / login
(replaces the one-shot suppress flag that re-fired on jitter)
- Immediate STOP+HEARTBEAT after teleport ACK / WORLDPORT ACK / login
to sync the real position with the server promptly
- CMSG_AREATRIGGER firing now logged at WARNING level for diagnosis
Re-allocate megaBoneSet_[0..1] in M2Renderer::clear() after vkResetDescriptorPool invalidates all
sets from boneDescPool_. Stale handles were bound to command buffers during rendering, causing
cascading validation errors. Also add ImGui::Dummy() after SetCursorScreenPos in the shaman totem
bar to satisfy ImGui's window boundary extension assertion.
Reject displayId values >100k (corrupted update-field data) to avoid
pointless DBC lookups and log spam. Add fileExists() guard before
using base texture path in 4 equipment compositing code paths that
were falling through without checking, causing excessive "Texture not
found" warnings when users have incomplete MPQ extractions.