- Erode brush mode: simulates water erosion by moving height downhill
based on slope, creating natural drainage patterns and gullies
- NPC JSON loader: File > Load NPCs parses saved creatures.json back
into the spawn list (round-trip save/load now works)
- Auto-save: every 5 minutes when unsaved changes exist, exports the
full zone (ADT + WDT + creatures) to the output directory
- Sculpt mode now has 6 brush types: Raise/Lower/Smooth/Flatten/Level/Erode
- Noise Generator in Sculpt panel: applies procedural value noise
with configurable frequency, amplitude, octaves, and seed
to create hills/valleys across entire tile instantly
- Sky/Lighting presets: View > Sky menu with Day (blue sky, high sun),
Dusk (orange, low sun), Night (dark blue, moonlight)
- Viewport clear color and light direction now configurable at runtime
- Noise uses smoothstep interpolation with octave fractal layering
- Punch Hole / Fill Hole buttons in Sculpt panel: creates terrain
holes (4x4 bitmask) for cave entrances, mine shafts, etc.
Uses brush radius to determine affected area.
- Recent Textures: paint panel shows last 6 used textures as quick-
select buttons (no need to re-search the full list)
- Holes saved in ADT format (MCNK holes field) and respected by
the mesh generator (triangles skipped at hole positions)
- Minimap window: 16x16 chunk grid colored by average height
(blue=low, green=mid, brown=high, blue overlay=water)
- NPC patrol path UI: add waypoints at cursor, view path list,
delete individual points or clear entire path
- Sculpt flatten "Pick" button: click to set target height from
cursor position instead of typing manually
- Height range displayed in minimap footer
- Terrain normals recalculated after height changes (smooth lighting
on sculpted terrain instead of flat-shaded appearance)
- Ghost preview and brush indicator cleared when switching modes
(prevents stale model instances or circles persisting)
- File > Clear All resets undo history and selections
- Normal computation uses finite differences from neighbor heights,
handles both outer (9x9) and inner (8x8) vertex grid positions
- Object scatter tool: place N copies of selected M2/WMO in a radius
with random rotation and scale range (Min/Max Scale slider)
- Camera bookmarks: F5 saves current position, View > Load Bookmark
to jump back — useful for working on different parts of a large zone
- Shortcut hints shown at bottom of Object panel
(G=move, R=rotate, T=scale, Del=remove)
- DragFloatRange2 for min/max scale in scatter UI
- Scatter tool: place N creatures in a radius around cursor position
with random rotation and uniform disk distribution
- File > Add Adjacent Tile: creates and exports a blank tile N/S/E/W
of current (foundation for multi-tile zone editing)
- Scatter UI: count slider (1-30), radius slider (10-200)
- Scatter places all copies with same stats/behavior as template
- Yellow circle renders at cursor position showing brush radius
- Visible in Sculpt, Paint, and Water modes
- Built from 48-segment quad strip slightly above terrain surface
- Renders through the water pipeline (alpha-blended, depth-tested)
- Disappears when cursor leaves terrain or in Object/NPC modes
- Brush VB cleaned up properly on shutdown
- Ctrl+S quick-saves entire zone (ADT + WDT + creatures.json) to output/
- File > Export Zone dialog saves all data to chosen directory
- exportZone() bundles ADT, WDT, and NPC spawns in one operation
- Info panel shows cursor world coordinates for placement debugging
- Info panel shows NPC count alongside object count
- Save state indicator: "Saved" (green) vs "* Unsaved (Ctrl+S)" (yellow)
- File menu reorganized: Quick Save + Export Zone replaces separate ADT/WDT
- Ctrl+Z in Object/NPC mode undoes last placement (50-deep stack)
- "Snap Ground" button raycasts straight down to place object on terrain
- Useful for objects placed too high or moved off terrain surface
- Undo stack adjusts indices when objects are removed mid-stack
- Raycast AABB now uses actual min/max vertex heights per chunk
instead of fixed ±200 padding (fixes misses on sculpted terrain)
- Right-click context menu opens correctly (deferred popup via flag
since ImGui::OpenPopup must be called within ImGui frame)
- Keyboard shortcuts: G=Move, R=Rotate, T=Scale, X/Y=axis lock,
Escape=deselect, Delete works in any mode for objects/NPCs
- Delete key now removes selected NPC in NPC mode too
- Ghost preview now shows in NPC mode (follows cursor with creature model)
- Added scale field to CreatureSpawn (default 1.0, slider 0.5-10x)
- NPC instances render at their configured scale
- Scale included in JSON save format
- M2Renderer::update() now runs AFTER beginFrame() so getCurrentFrame()
returns the correct frame index — fixes instance SSBO mismatch that
caused draws=0 despite loaded models
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.
- architecture.md: add chat system modules (src/ui/chat/), world map
modules (src/rendering/world_map/), CatmullRomSpline (src/math/),
transport decomposition, and updated namespace list
- status.md: update timestamp to 2026-04-14, add recent refactors
section and world map known gaps
- CHANGELOG.md: add detailed entries for PRs #58-63 covering
architecture, features, bug fixes, and 19 new test files
- TESTING.md: expand test suite layout from 8 to 27 files organized
by category (core, animation, transport, world map, chat)
- CONTRIBUTING.md: update namespace table, testing section, and key
files list to reflect new module directories
- README.md: update status timestamp to 2026-04-14
- Remove stale kVOffset (-0.15) from zone_highlight_layer hover detection;
the offset was removed from rendering but left in the hit-test path,
shifting hover ~15% vertically
- Add null guard for cachedGameHandler_ in ChatPanel::inputTextCallback
to prevent dereference before first render frame
- Zero WindowBorderSize in world map ImGui window to eliminate gap
between window edge and map content
- Replace hardcoded cosmic highlight multipliers with displayH×displayH
square rendering, preserving 1:1 aspect ratio at any resolution
- Skip transport waypoints where serverToCanonical zeroes nonzero input
instead of silently building paths with broken (0,0,0) coordinates
- Use length-squared check (posLenSq > 1.0) for spline endpoint
validation instead of per-component != 0 comparison, so entities
near the world origin are no longer skipped
- Fix off-by-one in ChatPanel::insertChatLink buffer capacity check
- Remove the -0.15 vertical offset (kVOffset) from coordinate_projection,
coordinate_display, and zone_highlight_layer; continent UV math is now
identical to zone UV math
- Switch world_map_facade aspect ratio to MAP_W/MAP_H (1002×668) and crop
the FBO image with MAP_U_MAX/MAP_V_MAX instead of stretching the full
1024×768 FBO
- Account for ImGui title bar height (GetFrameHeight) in window sizing and
zone highlight screen-space rect coordinates
- Add ZMP 128×128 grid pixel-accurate hover detection in zone_highlight_layer;
falls back to AABB when ZMP data is unavailable
- Upgrade PlayerMarkerLayer with full Vulkan lifecycle (initialize,
clearTexture, destructor); loads MinimapArrow.blp and renders a rotated
32×32 textured quad via AddImageQuad; red triangle retained as fallback
- Expose arrowRotation_ / arrowDS_ accessors on Minimap; clean up arrow DS
and texture in Minimap::shutdown()
- Wire PlayerMarkerLayer::initialize() into WorldMapFacade::initialize()
- Update coordinate-projection test: continent and zone UV are now equal
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Break the monolithic 1360-line world_map.cpp into 16 focused modules
under src/rendering/world_map/:
Architecture:
- world_map_facade: public API composing all components (PIMPL)
- world_map_types: Vulkan-free domain types (Zone, ViewLevel, etc.)
- data_repository: DBC zone loading, ZMP pixel map, POI/overlay storage
- coordinate_projection: UV projection, zone/continent lookups
- composite_renderer: Vulkan tile pipeline + off-screen compositing
- exploration_state: server mask + local exploration tracking
- view_state_machine: COSMIC→WORLD→CONTINENT→ZONE navigation
- input_handler: keyboard/mouse input → InputAction mapping
- overlay_renderer: layer-based ImGui overlay system (OCP)
- map_resolver: cross-map navigation (Outland, Northrend, etc.)
- zone_metadata: level ranges and faction data
Overlay layers (each an IOverlayLayer):
- player_marker, party_dot, taxi_node, poi_marker, quest_poi,
corpse_marker, zone_highlight, coordinate_display, subzone_tooltip
Fixes:
- Player marker no longer bleeds across continents (only shown when
player is in a zone belonging to the displayed continent)
- Zone hover uses DBC-projected AABB rectangles (restored from
original working behavior)
- Exploration overlay rendering for zone view subzones
Tests:
- 6 new test files covering coordinate projection, exploration state,
map resolver, view state machine, zone metadata, and integration
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
TransportManager decomposition:
- Extract TransportClockSync: server clock offset, yaw flip detection,
velocity bootstrap, client/server mode switching
- Extract TransportAnimator: spline evaluation, Z clamping, orientation
from server yaw or spline tangent
- Slim TransportManager to thin orchestrator delegating to ClockSync and
Animator; add pushTransform() helper to deduplicate WMO/M2 renderer calls
- Remove legacy orientationFromSplineTangent (now uses
CatmullRomSpline::orientationFromTangent)
Entity path following upgrade:
- Replace pathPoints_/pathSegDists_ linear lerp with
std::optional<CatmullRomSpline> activeSpline_
- startMoveAlongPath builds SplineKeys with distance-proportional timing
- updateMovement evaluates CatmullRomSpline for smooth Catmull-Rom
interpolation matching server-side creature movement
- Reset activeSpline_ on setPosition/startMoveTo to prevent stale state
Tests:
- Add test_transport_components (9 cases): ClockSync client/server/reverse
modes, yaw flip detection, Animator position eval, server yaw, Z clamping
- Link spline.cpp into test_entity for CatmullRomSpline dependency
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Extract CatmullRomSpline (include/math/spline.hpp, src/math/spline.cpp) as a
standalone, immutable, thread-safe spline module with O(log n) binary segment
search and fused position+tangent evaluation — replacing the duplicated O(n)
evalTimedCatmullRom/orientationFromTangent pair in TransportManager.
Consolidate 7 copies of spline packet parsing into shared functions in
game/spline_packet.{hpp,cpp}: parseMonsterMoveSplineBody (WotLK/TBC),
parseMonsterMoveSplineBodyVanilla, parseClassicMoveUpdateSpline,
parseWotlkMoveUpdateSpline, and decodePackedDelta. Named SplineFlag constants
replace magic hex literals throughout.
Extract TransportPathRepository (game/transport_path_repository.{hpp,cpp}) from
TransportManager — owns path data, DBC loading, and path inference. Paths stored
as PathEntry wrapping CatmullRomSpline + metadata (zOnly, fromDBC, worldCoords).
TransportManager reduced from ~1200 to ~500 lines, focused on transport lifecycle
and server sync.
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Add proper waypoint support to entity movement:
- Parse intermediate waypoints from MonsterMove packets in both WotLK
and Vanilla paths. Uncompressed paths store absolute float3 waypoints;
compressed paths decode TrinityCore's packed uint32 deltas (11-bit
signed x/y, 10-bit signed z, ×0.25 scale, waypoint = midpoint − delta)
with correct 2's-complement sign extension.
- Entity::startMoveAlongPath() interpolates along cumulative-distance-
proportional segments instead of a single straight line.
- MovementHandler builds the full path (start → waypoints → destination)
in canonical coords and dispatches to startMoveAlongPath() when
waypoints are present.
- Snap entity x/y/z to moveEnd in the dead-reckoning overrun phase
before starting a new movement, preventing visible teleports when the
renderer was showing the entity at its destination.
- Clamp creature and player entity Z to the terrain surface via
TerrainManager::getHeightAt() during active movement. Idle entities
keep their server-authoritative Z to avoid breaking flight masters,
elevator riders, etc.
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
- Mark swapchain dirty in Application's SDL resize handler (was only done
in Window::pollEvents which is never called)
- Skip swapchain recreation when window is minimized (0×0 extent violates
Vulkan spec and crashes vmaCreateImage)
- Guard aspect ratio division by zero when height is 0
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
- Delegate GameHandler::getQuestGiverStatus() to QuestHandler instead of
reading from GameHandler's own empty npcQuestStatus_ map
- Immediately add quest to local log in acceptQuest() instead of waiting
for field updates, fixing quests not appearing after accept
- Handle duplicate accept path (server already has quest) by also adding
to local log
- Remove early return on empty questLog_ in applyQuestStateFromFields()
- Re-query nearby quest giver NPC statuses on abandon so markers refresh
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Add Up/Down arrow keys to cycle through character list and Enter to
select. Claim arrow key ownership via SetKeyOwner to prevent ImGui nav
from moving focus to other widgets. Lock Enter key until release to
prevent the keypress from activating chat on the game screen.
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Use destination position (getLatest) instead of dead-reckoned position
(getX/Y/Z) during the overrun window to avoid visible forward-drift and
backward-snap. Only fall back to position-change movement detection for
entities without active movement tracking, preventing residual velocity
drift from keeping walk/run animation playing after arrival.
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Add resetForBack() to RealmScreen that clears selection state without
resetting autoSelectAttempted, preventing single-realm auto-connect from
re-firing when the user navigates back from the character screen.
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
The geoset normalization stripped all group 15 (cloak) submeshes but
only re-added them when a cape was equipped. NPCs without capes lost
the "no cape" back panel (geoset 1501), exposing the single-sided
torso mesh. Always add either the cape or no-cape geoset.
Also load and apply cape texture overrides for NPCs that do have capes
equipped via CreatureDisplayInfoExtra, matching the player path.
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>
HiZ occlusion culling built ~11 mip levels with per-level barriers
behind a blocking vkWaitForFences every frame — the main frame-rate
bottleneck. Disable the pyramid build and fall back to GPU frustum-
only culling which is nearly free behind the fence.
WMO interiors now receive full-strength directional shadows but clamp
minimum brightness at 0.45 with a 0.35 ambient floor, so interiors
get real shadow contrast without going too dark.
dylibbundler misses Homebrew's FFmpeg libraries, causing a launch crash:
Library not loaded: libavformat.62.12.100.dylib
Add a manual copy block for libavformat, libavcodec, libavutil,
libswscale, and libswresample — same pattern already used for Vulkan
and OpenSSL. Applied to both build.yml and release.yml.
Two sources of visible chunk-boundary squares:
1. Derivative-based bump mapping (bumpStrength=9) used dFdx/dFdy which
are invalid across draw-call boundaries, producing strong normal
discontinuities at every chunk edge. Fade bump to zero near chunk
edges using LayerUV as the chunk-space distance metric.
2. sampleAlpha used an abrupt step() to switch between point-sampled
and 4-tap-blurred alpha, creating a visible ring 2 texels from each
chunk edge. Replace with smoothstep transition and a 5-tap average
that includes the center sample.
All M2 pipelines used VK_CULL_MODE_NONE, so back-facing polygons always
rendered. On NPCs whose torso meshes are single-layer geometry this
made the interior cavity visible through the back.
Create backface-culled pipeline variants (VK_CULL_MODE_BACK_BIT) and
select them at draw time unless the material has the TwoSided flag
(0x04). Foliage/ground-detail forceCutout batches and the shadow
pipeline keep VK_CULL_MODE_NONE since those cards are inherently
two-sided.
M2Renderer::clear() reset descriptor pools but left the texture cache
and failed-texture tracking intact. On re-login the stale cache filled
the budget, failedTextureRetryAt_ blocked reloads, and the client
entered an infinite model-load loop. Match the cleanup already done in
shutdown() and CharacterRenderer::clear().