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.
Adds setPatrolPath() that draws a multi-segment orange ribbon between the
NPC's spawn position and each waypoint, plus diamond markers at each point
(green for start = NPC home, white for waypoints). Renders only while the
NPC is selected and has a patrol path defined.
Added orientation slider in NPC panel with random button. Ctrl+Wheel now
rotates the placement preview (objects and NPCs) instead of zooming —
Shift makes the step finer (5 deg vs 15 deg). Ghost preview now shows
the actual orientation that the placed NPC will have.
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.
Without prepareRender(), animated NPC creature instances had megaBoneOffset=0
which caused the render loop to skip them (filtered out at the bones check).
This is why all loaded NPC M2s produced 0 draw calls despite valid GPU buffers,
material sets, and instance creation. Matches the main game's render flow.
ROOT CAUSE: createInstance() checks mdlRef.isValid() which requires
vertexBuffer != VK_NULL_HANDLE. But vertex buffers are uploaded via
staging and only finalized by waitAllUploads(). Instances were being
created BEFORE the upload flush, so vertexBuffer was still null,
cachedIsValid was set to false, and all instances were skipped during
render (0 draws despite loaded models).
Fix: split rebuildObjects into two phases:
1. Load all models (upload geometry to staging)
2. waitAllUploads + pollUploadBatches (finalize GPU buffers)
3. Create all instances (vertexBuffer is now valid, isValid() = true)
This matches the client's terrain_manager pattern where models are
loaded on background threads and instances created after finalization.
Log showed: models=2, instances=3, draws=0 — models loaded and
instances created but zero draw calls. The M2 renderer skips
instances where cachedIsValid is false, which depends on the GPU
vertex buffer being valid after upload.
The per-model waitAllUploads/pollUploadBatches calls inside the
loading loop may have corrupted the upload batch context (started
at beginUploadBatch but flushed mid-loop). Now all models upload
in a single batch with one final waitAllUploads+pollUploadBatches
at the end of rebuildObjects.
ROOT CAUSE of NPC models not rendering: every NPC placement triggered
an immediate full clear+rebuild of ALL M2 models. During rapid clicking,
this created a destroy-reload cycle where models were cleared faster
than they could render — the log showed rebuild firing every ~200ms
with models loading OK but being destroyed before the next frame.
Fix: debounce rebuilds with a 0.5s timer. Multiple rapid placements
reset the timer, so the rebuild only fires once after the user stops
clicking. Models stay loaded and visible between placements.
Before: click → clear all → reload all → click → clear all → reload...
After: click → click → click → (0.5s pause) → single rebuild
ROOT CAUSE of NPCs not rendering: rebuildObjects() called createInstance()
BEFORE beginFrame(), causing instance SSBO writes to use the wrong frame
index. The M2 renderer writes instance transforms to a per-frame buffer
indexed by getCurrentFrame(), but the frame index isn't valid until after
beginFrame() returns.
This is the same bug documented in the project memory:
"M2 models not rendering (draws=0): update() was called before
beginFrame(), causing frame index mismatch in instance SSBO"
Models loaded correctly (log confirmed 2192v 7926i 8b) but instances
were invisible because their transform data was written to the wrong
frame buffer.
Fix: move the rebuild block after beginFrame(), alongside update().
Release builds set default log level to WARNING — all the NPC loading
diagnostic messages were at LOG_INFO/LOG_DEBUG and completely invisible.
This is why the log showed zero NPC messages despite NPCs being placed.
All NPC loading messages now use LOG_WARNING:
- "NPC rebuild: N creatures to load" — confirms rebuild loop runs
- "NPC M2 OK: path (Nv Ni Nb)" — model loaded successfully
- "NPC loaded from WOM: path" — WOM format used
- "NPC model file not found: path" — file missing
- "NPC model invalid: path" — parse failed
- "NPC M2 loadModel failed: path" — GPU upload failed
- "NPC has empty modelPath: name" — no model selected
NPC model loading diagnostics were at LOG_DEBUG level which doesn't
appear in the default log output. Changed all NPC model loading
messages to LOG_WARNING/LOG_INFO:
- "NPC model file not found" now WARNING (was DEBUG, invisible)
- "NPC has empty modelPath" new WARNING for missing model selection
- "Loading N NPC models..." at loop entry to confirm rebuild runs
- "NPC M2 loaded" at INFO level shows successful loads
This will reveal exactly where the NPC rendering pipeline fails.
Changed NPC model invalid and load success messages from LOG_DEBUG to
LOG_WARNING/LOG_INFO so they appear in the log output. This helps
diagnose why specific creature models fail to render — the log will
show vertex/index/batch counts for each load attempt.
Many WoW instances (Dire Maul, Blackrock Depths, etc.) are WMO-only
maps with no ADT terrain tiles. The editor now handles these:
- loadWMOInstance(): reads WDT, detects WDTF_GLOBAL_WMO flag, loads
the root WMO path from MWMO chunk, places it as an object with
correct position/rotation from MODF chunk
- Automatic fallback: when loadADT fails to find tiles, tries
WMO-only loading before showing error
- Load dialog: "Find Tile" detects WMO-only instances and shows
"WMO-only instance — click Load to open" instead of "not found"
- Camera positioned near the WMO for immediate editing
- Blank terrain floor generated as ground reference
rebuildObjects() calls clearObjects() which clears the M2 renderer,
but didn't start a new upload batch before loading models. The M2
renderer needs an active upload batch context to upload vertex/index
buffers to the GPU. Without it, loadModel may silently fail.
Now calls vkCtx_->beginUploadBatch() after clear and before the
model loading loop. Also adds diagnostic logging when loadModel
fails for NPCs (shows vertex/index/batch counts).
Applied same two fixes from NPC renderer to placed object renderer:
1. Check WOM open format before M2 fallback (custom_zones/output dirs)
2. Always load skin file regardless of initial isValid state
Both placed objects (M2 doodads from ADT import or manual placement)
and NPC creatures now have consistent WOM→M2 fallback pipeline with
proper skin file loading.
NPC creature renderer now checks for WOM open format models in
custom_zones/models/ and output/models/ before falling back to M2
game data files. This completes the WOM integration — both placed
objects (via terrain_manager) and NPC creatures (via editor viewport)
can render from the novel open format.
WOM→M2Model conversion includes bone weights, textures, render batch,
and material setup — same pipeline as the client-side WOM loader.
Loading priority: WOM (open format) → M2 + skin (game data)
WotLK M2 models store geometry in separate .skin files. The NPC
renderer was only loading skin files when M2Loader::load() returned
invalid (empty vertices). But some M2 files have vertices in the
header yet need the skin file for indices, batches, and submeshes.
Now always attempts to load the skin file regardless of initial
isValid() state. This fixes creature models not rendering even
when the M2 and skin files exist on disk.
Also improved debug logging to show vertex/index counts when models
fail to load, making it easier to diagnose remaining issues.
"Count" button next to "Find Tile" scans all 64x64 tile coordinates
and shows how many ADT tiles exist for the selected map. Helps users
understand the scope of a map before loading individual tiles.
Two bugs fixed:
- Loading a new ADT tile now clears all previous objects, NPCs, quests,
path state, and terrain before loading. Was accumulating old state
across multiple loads
- ADT doodad/WMO rotation conversion now matches client's transform:
renderRotX = -adtRotZ, renderRotY = -adtRotX, renderRotZ = adtRotY+180
Was copying raw ADT rotations without coordinate system conversion,
causing models to appear at wrong orientations
- NPC markers reduced from 30-unit poles to 8-unit poles (was
overwhelming and obscuring terrain). Base 1.5u, diamond top 1u
- "Show Position Markers" checkbox in NPC panel to toggle visibility
- Markers hidden when checkbox unchecked — useful when M2 creature
models are rendering and markers are redundant
- Marker alpha reduced for less visual noise
When loading ADT tiles, the editor overrides terrain_.coord with the
filename-derived tile coordinates (instanced maps have arbitrary
internal values). But it wasn't recomputing the per-chunk world
positions to match, causing terrain to render at wrong coordinates.
Now recalculates all 256 chunk positions from the corrected tile
coordinates using the standard WoW formula:
chunkX = (32 - tileX) * 533.33 - cx * 33.33
chunkY = (32 - tileY) * 533.33 - cy * 33.33
This fixes terrain appearing at the wrong location in the editor
when loading instanced maps or tiles with mismatched internal coords.
NPC position markers were silently not rendering when no object was
selected because they relied on the gizmo's pipeline being bound, but
the gizmo only binds its pipeline when active. Now explicitly binds
the water pipeline (same pos+color vertex format with alpha blend)
before drawing NPC markers, ensuring they always appear regardless of
gizmo state.
Note: M2 creature models still require extracted game data files to
render. When model files aren't found, the colored markers (poles with
diamond tops) provide reliable visual feedback for NPC positions.
- --convert-m2 and --convert-wmo now print progress and results to
stdout (was LOG_INFO to log file only, invisible to user)
- Failures return exit code 1 (was always 0, breaking scripting)
- Success output shows vertex/bone counts (M2) or group count (WMO)
- Error messages go to stderr for proper pipe handling
--help, --version, and --list-zones now print to stdout via printf
instead of LOG_INFO which writes to log file only. Users running
CLI commands would see no output at all — critical first-impression
bug for new users.
Also updated --version format list to include WOC.
One-click generation of a complete AzerothCore/TrinityCore server module
from editor zone data. File > Generate Server Module creates:
- sql/01_map.sql: map_dbc + area_table_dbc registration
- sql/02_spawns.sql: creature_template + creature + waypoint_data + quests
- sql/03_teleport.sql: game_tele entry for .tele command
- sql/04_zone_flags.sql: sanctuary/PvP area flags
- conf/mod_wowee.conf.dist: worldserver.conf snippet with zone settings
- README.md: step-by-step server admin installation guide
- module.json: machine-readable module manifest
Server admins can import the SQL files, add the config snippet, and
restart their server to have the custom zone fully operational with
NPC spawns, patrol paths, quests, teleport commands, and zone flags.
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
Private server integration: export creature spawns, patrol waypoints,
and quest definitions as ready-to-import SQL for AzerothCore/TrinityCore.
- creature_template: name, level, health, mana, damage, armor, faction,
npcflag (questgiver/vendor/flightmaster/innkeeper), displayId, scale
- creature: spawn position, orientation, respawn time, wander distance,
movement type (stationary/wander/patrol)
- creature_addon + waypoint_data: patrol path waypoints with delays
- quest_template: title, description, completion text, level, XP, money
- All use ON DUPLICATE KEY UPDATE for safe re-imports
- Auto-exported as spawns.sql alongside other assets on zone save
- File > Export Server SQL menu item for standalone export
- Map ID from zone metadata panel used in spawn table
- exportZoneMap(): renders terrain as colored top-down image with
height-based coloring (blue lowlands → green plains → brown hills →
white peaks), water overlay, hole visualization, doodad markers
- Configurable resolution (128-2048px, default 512)
- Auto-exported as zone_map.png alongside other assets on save
- File > Export Zone Map menu with resolution slider
- Useful for documentation, server admin tools, custom map websites
- Map ID: configurable integer input (0-65535) for private server
integration. Custom zones default to 9000+ to avoid Blizzard conflicts
- Display Name: editable text field for in-game world map/loading screen
- Description: multi-line text field for zone documentation
- Zone Flags: Allow Flying, PvP Enabled, Indoor, Sanctuary checkboxes
- All fields serialized to zone.json under "flags" key
- Info panel shows quest count alongside objects/NPCs
- Zone manifest gains audio fields: musicTrack, ambienceDay,
ambienceNight, musicVolume, ambienceVolume
- Serialized to/from zone.json under "audio" key
- Info panel: collapsable "Zone Audio" section with text inputs for
music/ambience paths and volume sliders
- Preset selector: Elwynn Forest, Durotar, Darkshore, Dungeon, None
- ZoneManifest stored persistently on EditorApp so audio settings
survive between exports (was recreated each save)
- Custom zones can now specify their own background music and ambient
soundscapes via zone.json
- importHeightmapImage(): loads any resolution PNG/JPG/BMP/TGA via
stb_image, supports both 8-bit and 16-bit precision, maps to terrain
vertices with bilinear coordinate mapping
- Both image import and RAW import now wrapped with undo
(recordGeneratorUndo/commitGeneratorUndo)
- UI: File > Import Heightmap now offers "Import Image" (any format)
and "Import RAW" (binary) as separate options
- Enables professional terrain workflows: paint in Photoshop/GIMP,
generate in World Machine/Gaea, import directly as terrain
- Minimap slope overlay: toggle "Show Slopes (collision)" to visualize
steep terrain chunks with red intensity overlay (50 deg threshold)
- Lets zone creators preview walkability before exporting WOC
- About dialog updated to show 7/7 format replacements including WOC
- Help panel documents collision preview and export includes collision
- 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
- README: "7 novel open format replacements" with WOC collision listed
- CHANGELOG: 7/7 format count, WOC entry, validation score 0-7,
328 test assertions across 84 test cases
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
- gitCommit() now uses double quotes for projectDir consistently with
gitPush/gitPull/gitStatus (was single quotes, breaking on paths with
apostrophes like "John's Project")
- Test suite auto-cleans test_output_formats/ directory via Catch2
event listener after all tests complete (was leaving empty dir)
- CHANGELOG: add World Editor section (12.5k+ lines, 6 modes, 30+ tools)
and Novel Open Formats section (6/6 replacements, 309 test assertions)
- README: add World Editor section with build/run/CLI examples, format
summary, and reference to FORMAT_SPEC.md
- Fix dbc_to_csv build: add extern/ to include path for nlohmann/json
(broke when dbc_loader.cpp gained JSON DBC loading support)