Previously just teleported camera 30u directly above the target — the user
still had to manually look down to see anything. Now positions the camera
back along the current view direction, slightly elevated, and aims it at
the selection so the target is visible immediately. Removes the round-trip
through manual rotation after every Fly To.
Character body/skin textures live in CharSections-composed paths that
don't exist as standalone BLPs. Exporting a zone with many character
NPCs would spam hundreds of warnings. Now logs the first 5, suppresses
the rest, and reports the total skipped count in the summary line.
Each MODF entry in ADT files is 64 bytes (per MODF spec). The writer was
emitting only 60 bytes, missing the trailing nameSet(u16) + scale(u16).
The loader uses entrySize=64 to advance per record, so any saved ADT with
more than one WMO placement misaligned every subsequent record on reload.
Now pads with nameSet=0 and scale=1024 (=1.0 fixed-point).
SQL export uses CreatureSpawn.displayId for creature_template.modelid1.
Without it AzerothCore can't render the creature in-game. Added an
InputInt + warning text so users can set the displayId manually until
auto-discovery from CreatureDisplayInfo.dbc lands.
ADT loading transforms placement rotations:
obj.rot = (-dp.rot[2], -dp.rot[0], dp.rot[1] + 180)
syncToTerrain previously wrote them back unchanged, so save->load would
flip and shift orientation. Now applies the inverse:
dp.rot = (-obj.rot.y, obj.rot.z - 180, -obj.rot.x)
which round-trips identically.
ADT loading converts MDDF/MODF positions from ADT space to render/world
space via core::coords::adtToWorld, but syncToTerrain wrote object
positions back as raw render coords. So saving and reloading would
displace every placed object/WMO, accumulating each save cycle. Now
calls worldToAdt() to round-trip cleanly.
Marker geometry now reacts to npc.selected: 2.5x base radius (vs 1.5x),
saturated yellow with cyan tinge, and full alpha. Marker rebuild also
fires on selection change so the highlight appears immediately rather
than only after the next placement.
Mirror the M2 fix: the editor was skipping the WMO renderer's per-frame
state advance, so material UBO updates and frame ID tracking were stale
relative to the main game's render flow. Most visible effect is that
material setting toggles wouldn't propagate to the GPU.
Auto-save was gated on terrainEditor.hasUnsavedChanges() so a session
where the user only edited NPCs or quests would lose data on crash.
Added autoSavePendingChanges_ flag flipped by every objectsDirty_ = true
site (and markObjectsDirty), cleared by quickSave. Auto-save now fires
on either dirty signal.
Spawned NPCs reference CreatureDisplayInfo, CreatureModelData, faction
templates, etc. Without exporting these the JSON DBC pack only covered
terrain data and exported zones couldn't resolve creature display IDs
on a clean install. Added: CreatureDisplayInfo, CreatureModelData,
CreatureType, CreatureFamily, FactionTemplate, Faction, ItemDisplayInfo.
WMO buildings reference M2 doodads (chairs, candles, banners) via the
MODD chunk. Their textures live in those M2 files, not the WMO root.
Now collectWMOTextures walks every doodad name and collects M2 textures
recursively so exported buildings include all their interior decoration
textures.
Placed WMO buildings reference textures (walls, floors, decorations) that
were not being exported alongside the WOB files. Added collectWMOTextures()
which loads the root WMO + all group files and gathers every texture path,
then folds these into the same PNG export pass that handles terrain and M2
textures. Exported zones now have every texture they need across all model
types.
The scatter tool spawned creatures all at the cursor's z-height which
made them hover when scattered over uneven terrain. Added a Snap to
Ground checkbox (defaults to on) that raycasts each scattered NPC and
places it on the actual surface.
Selected NPCs now display the move/rotate/scale gizmo and respond to
drag operations. Rotate uses only the yaw axis (NPCs have a single
orientation field, not full euler rotation). Move/scale work the same
way as objects.
Right-click context menu on a selected NPC now mirrors the object menu:
Snap to Ground drops it to terrain elevation, Face Camera rotates the NPC
to face the current camera position. Also annotates Duplicate with the
Ctrl+D shortcut hint.
The editor's orientation field is stored in degrees (matches the UI slider
and the M2 renderer's glm::radians() call), but AzerothCore's creature.
orientation column expects radians. Without conversion every exported NPC
faces the wrong direction in-game (off by 57x).
The patrol list now shows total loop distance and gives each waypoint a
draggable wait-time field (0-60s). Helps tune patrol pacing without re-saving
the JSON manually.
Once an NPC is placed there was no way to resize it without removing and
re-placing. Added a Scale DragFloat in the selected-creature editor with
the same 0.1-50 range used for placed objects.
Plain left-click within 4u of an existing NPC now selects that NPC rather
than dropping a new spawn on top. Shift+click forces placement at the cursor
even if it overlaps an existing NPC, preserving the rapid-placement workflow.
When an NPC with Patrol behavior is selected in NPC mode, pressing W
appends a waypoint at the current cursor position. Faster than clicking
the panel button for laying out long routes.
TextureExporter::collectUsedTextures only picked up terrain textures, so
exported zones were missing every texture referenced by NPC creature models
and placed M2 doodads. Added collectM2Textures() and unified the export
collection to include terrain + all referenced M2 paths, so the rendered
zone is fully self-contained in the PNG/WOM open formats.
Previously only placed M2 objects were converted to the WOM open format.
NPC creature models stayed as references to game M2/skin files, which
meant exported zones still depended on proprietary Blizzard assets to
render their NPCs. Now the exporter walks both placed objects and NPC
spawns and emits a WOM for every unique M2 path, making zones fully
self-contained.
NPC markers now show a yellow ground-triangle pointing in the orientation
direction so users can see facing without selecting. Object panel gained
the Ctrl+Wheel rotation hint to match the NPC panel.
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.
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.