Add a generic AddonEventCallback to GameHandler for firing named events
with string arguments directly from game logic. Wire it to the addon
system in Application.
New events fired:
- PLAYER_TARGET_CHANGED — when target is set or cleared
- PLAYER_LEVEL_UP(newLevel) — on level up
The generic callback pattern makes it easy to add more events from
game_handler.cpp without touching Application/AddonManager code.
Total addon events: 16 (2 world + 12 chat + 2 gameplay).
Wire chat messages to the addon event system via AddonChatCallback.
Every chat message now fires the corresponding WoW event:
- CHAT_MSG_SAY, CHAT_MSG_YELL, CHAT_MSG_WHISPER
- CHAT_MSG_PARTY, CHAT_MSG_GUILD, CHAT_MSG_OFFICER
- CHAT_MSG_RAID, CHAT_MSG_RAID_WARNING, CHAT_MSG_BATTLEGROUND
- CHAT_MSG_SYSTEM, CHAT_MSG_CHANNEL, CHAT_MSG_EMOTE
Event handlers receive (eventName, message, senderName) arguments.
Addons can now filter, react to, or log chat messages in real-time.
Implement the WoW-compatible event system that lets addons react to
gameplay events in real-time:
- RegisterEvent(eventName, handler) — register a Lua function for an event
- UnregisterEvent(eventName, handler) — remove a handler
- fireEvent() dispatches events to all registered handlers with args
Currently fired events:
- PLAYER_ENTERING_WORLD — after addons load and world entry completes
- PLAYER_LEAVING_WORLD — before logout/disconnect
Events are stored in a __WoweeEvents Lua table, dispatched via
LuaEngine::fireEvent() which is called from AddonManager::fireEvent().
Error handling logs Lua errors without crashing.
Updated HelloWorld addon to use RegisterEvent for world entry/exit.
Foundation for WoW-compatible addon support:
- Vendor Lua 5.1.5 source as a static library (extern/lua-5.1.5)
- TocParser: parses .toc files (## directives + file lists)
- LuaEngine: Lua 5.1 VM with sandboxed stdlib (no io/os/debug),
WoW-compatible print() that outputs to chat, GetTime() stub
- AddonManager: scans Data/interface/AddOns/ for .toc files,
loads .lua files on world entry, skips LoadOnDemand addons
- /run <code> slash command for inline Lua execution
- HelloWorld test addon that prints to chat on load
Integration: AddonManager initialized after asset manager, addons
loaded once on first world entry, reset on logout. XML frame
parsing is deferred to a future step.
loadOnlineWorldTerrain re-registers the death/respawn/swing callbacks,
overriding the ones from setupUICallbacks. The world-load versions only
checked creatureInstances_, so the player lookup fix from the previous
commit was silently reverted whenever the world loaded. Now both
registration sites check playerInstances_ as a fallback.
Death, respawn, and melee swing callbacks only checked
creatureInstances_, so online players never played death animation when
killed, never returned to idle on resurrect, and never showed attack
swings. Extended all three callbacks to also check playerInstances_.
Also extended the game_handler death/respawn callback triggers to fire
for PLAYER entities (not just UNIT), and added spawn-time death
detection for players that are already dead when first seen.
Online players had no animation state machine — once Run started from a
movement packet, it never transitioned back to Stand/Idle. This mirrors
the creature sync loop: position, orientation, and locomotion animation
(Run/Walk/Swim/Fly ↔ Stand/SwimIdle/FlyIdle) are now driven per-frame
based on Entity::isActivelyMoving() state transitions.
Also cleans up creatureRenderPosCache_ on player despawn.
Creatures were stuck in Run/Walk animation during the dead-reckoning
overrun window (up to 2x movement duration). The animation check used
isEntityMoving() which stays true through dead reckoning, causing
creatures to "run in place" after reaching their destination.
Add isActivelyMoving() which is true only during the active
interpolation phase (moveElapsed < moveDuration), and use it for
animation state transitions. Dead reckoning still works for position
extrapolation — only the animation now correctly stops at arrival.
Replace processAllReadyTiles() with bounded processReadyTiles() in the
same-map teleport and reconnect paths. processAllReadyTiles finalizes
every pending tile synchronously with a GPU sync wait, which caused
8+ second main-thread stalls when many tiles were queued. The bounded
version processes 1-4 tiles per call with async GPU upload — remaining
tiles finalize incrementally over subsequent frames.
setOnlinePlayerEquipment used wrong geoset ID ranges for boots (402+ instead
of 501+), gloves (301+ instead of 401+), and chest/sleeves (501+ instead of
801+), and was missing bare-shin (502), bare-wrist (801), and bare-leg (1301)
defaults. This caused other players to render with missing shin/wrist geometry
and wrong geosets when wearing equipment (the "shin mesh" gap in status.md).
Now mirrors the CharacterPreview::applyEquipment logic exactly:
- Group 4 (4xx) forearms/gloves: default 401, equipment 401+gg
- Group 5 (5xx) shins/boots: default 502, equipment 501+gg
- Group 8 (8xx) wrists/sleeves: default 801, equipment 801+gg
- Group 13 (13xx) legs/pants: default 1301, equipment 1301+gg
gameObjectDisplayIdWmoCache_ was not cleared on world unload/transition,
causing stale WMO model IDs (e.g. 40006, 40003) to be looked up after
the renderer cleared its model list, resulting in "Cannot create instance
of unloaded WMO model" errors on zone re-entry.
Changes:
- Clear gameObjectDisplayIdWmoCache_ alongside other GO caches on world reset
- Add WMORenderer::isModelLoaded() for cache-hit validation
- Inline GO WMO path now verifies cached model is still renderer-resident
before using it; evicts stale entries and falls back to reload
When the M2 model cache is full (>6000 entries), loadModel() returns
false and the model is never added to the GPU cache. The WMO instance
doodad path was calling createInstanceWithMatrix() unconditionally,
generating hundreds of "Cannot create instance: model X not loaded"
warnings on zone entry. Add the same guard already present in the
terrain doodad path.
TBC races like Draenei use version-264 M2 files with no embedded skin;
indices come from a separate .skin file loaded after M2::load().
The premature isValid() check (which requires non-empty indices) always
failed for WotLK-format character models, making Draenei (and Blood Elf)
players invisible.
Fix: only check vertices.empty() right after load(), then validate fully
with isValid() after the skin file is loaded.
The failed-model cache introduced in f855327 would persist across map
changes, permanently suppressing models that failed on one map but might
be valid assets on another (or after a client update). Clear it in the
world reset path alongside the existing gameObjectDisplayIdModelCache_
clear, so model loads get a fresh attempt on each zone change.
Two follow-up fixes for the ribbon emitter implementation and the
transport-doodad stall fix:
1. loadModel() rejected any M2 with no vertices AND no particles, but
ribbon-only spell-effect models (e.g. weapon trail or aura ribbons)
have neither. These models were silently invisible even though the
ribbon rendering pipeline added in 1108aa9 is fully capable of
rendering them. Extended the guard to also accept models that have
ribbon emitters, matching the particle-emitter precedent.
2. processPendingTransportDoodads() ignored the bool return of
loadModel(), calling createInstance() even when the model was
rejected, generating spurious "Cannot create instance: model X not
loaded" warnings for every failed doodad path. Check the return
value and continue to the next doodad on failure.
Three root causes identified from wowee.log crash at frame 134368:
1. processPendingTransportDoodads() was doing N separate synchronous
GPU uploads (vkQueueSubmit + vkWaitForFences per texture per doodad).
With 30+ doodads × multiple textures, this caused the 489ms stall in
the 'gameobject/transport queues' update stage. Fixed by wrapping the
entire batch in beginUploadBatch()/endUploadBatch() so all texture
layout transitions are submitted in a single async command buffer.
2. Game objects whose M2 model has no geometry/particles (empty or
unsupported format) were retried every frame because loadModel()
returns false without adding to gameObjectDisplayIdModelCache_.
Added gameObjectDisplayIdFailedCache_ to permanently skip these
display IDs after the first failure, stopping the per-frame spam.
3. renderM2Ribbons() only checked ribbonPipeline_ != null, not
ribbonAdditivePipeline_. If additive pipeline creation failed, any
ribbon with additive blending would call vkCmdBindPipeline with
VK_NULL_HANDLE, causing VK_ERROR_DEVICE_LOST on the GPU side.
Extended the early-return guard to cover both ribbon pipelines.
Previously SMSG_OPEN_LFG_DUNGEON_FINDER was consumed silently with no UI
response. Now it fires an OpenLfgCallback wired to openDungeonFinder() on
the GameScreen, so the dungeon finder window opens as the server requests.
- Add triggerShake(magnitude, frequency, duration) to CameraController
- Apply envelope-decaying sinusoidal XYZ offset to camera in update()
- Handle SMSG_CAMERA_SHAKE opcode in GameHandler dispatch
- Translate shakeId to magnitude (minor <50: 0.04, larger: 0.08 world units)
- Wire CameraShakeCallback from GameHandler through to CameraController
- Shake uses 18Hz oscillation with 30% fade-out envelope at end of duration
Look up tabard display ID from CreatureDisplayInfoExtra and map to
geoset variant via ItemDisplayInfo.dbc to select correct tabard
meshes. Falls back to hardcoded 1201 if DBC lookup unavailable.
Improves NPC appearance variety with proper scope handling.
The itExtra variable is not in scope at the tabard rendering site.
Reverted to original hardcoded 1201 fallback which is working reliably.
DBC variant approach requires refactoring variable scope.
Reads equipped tabard display ID from CreatureDisplayInfoExtra (slot 9)
and looks up the corresponding geoset group in ItemDisplayInfo.dbc to
select the correct tabard variant. Falls back to hardcoded 1201 if DBC
unavailable. Improves NPC appearance variety without risky features.
Remove redundant helmet attachment code path (lines 6490-6566) that was
disabled and inferior to the main path. The main path (enabled in Loop 25)
provides better fallback logic by trying attachment points 0 and 11,
includes proper logging, and has undergone validation.
This consolidation reduces code duplication by 78 lines, improves
maintainability, and eliminates potentially wasteful spawn-time overhead
from the disabled path.
Add fallback logic to use bone 0 for head attachment point (ID 11) when models
don't have it explicitly defined. This improves helmet rendering compatibility
on humanoid NPC models that lack explicit attachment 11 definitions. Re-enable
helmet attachments now that the fallback logic is in place.
Enable tabard mesh rendering for NPCs by reading geoset variant from
ItemDisplayInfo.dbc (slot 9). Tabards now render like other equipment
instead of being disabled due to the previous flickering issue.
Reads OBJECT_FIELD_SCALE_X (field 4, cross-expansion) from CREATE_OBJECT
update fields and passes it through the full creature and game object spawn
chain: game_handler callbacks → pending spawn structs → async load results
→ createInstance() calls. This gives boss giants, gnomes, children, and
other non-unit-scale NPCs correct visual size, and ensures scaled GOs
(e.g. large treasure chests, oversized plants) render at the server-specified
scale rather than always at 1.0.
- Added OBJECT_FIELD_SCALE_X to UF enum and all expansion update_fields.json
- Added float scale to CreatureSpawnCallback and GameObjectSpawnCallback
- Propagated scale through PendingCreatureSpawn, PreparedCreatureModel,
PendingGameObjectSpawn, PreparedGameObjectWMO
- Used scale in charRenderer/m2Renderer/wmoRenderer createInstance() calls
- Sanity-clamped raw float to [0.01, 100.0] range before use
- Animation stutter: skip playAnimation(Run) for the local player in the
server movement callback — the player renderer state machine already manages
it; resetting animTime on every movement packet caused visible stutter
- Resolution crash: reorder swapchain recreation so old swapchain is only
destroyed after confirming the new build succeeded; add null-swapchain
guard in beginFrame to survive the retry window
- Memory cap: reduce cache budget from 80% uncapped to 50% hard-capped at
16 GB to prevent excessive RAM use on high-memory systems
- Spell tooltip: suppress "Drag to action bar / Double-click to cast" hints
when the tooltip is shown from the action bar (showUsageHints=false)
- M2 collision: add watermelon/melon/squash/gourd to foliage (no-collision);
exclude chair/bench/stool/seat/throne from smallSolidProp so invisible chair
bounding boxes no longer trap the player
- Parse SMSG_ALL_ACHIEVEMENT_DATA on login to populate earnedAchievements_ set
- Pass achievement name through callback so toast shows name instead of ID
- Add renderItemTooltip(ItemQueryResponseData) overload for loot/non-inventory contexts
- Loot window now shows full item tooltip on hover (stats, sell price, bind type, etc.)
Camera controller / sitting:
- Any movement key (WASD/QE/Space) pressed while sitting now clears the
sitting flag immediately, matching WoW's sit-to-stand-on-move behaviour
- Added StandUpCallback: when the player stands up via local input the
callback fires setStandState(0) → CMSG_STAND_STATE_CHANGE(STAND) so
the server releases the sit lock and restores normal movement
- Fixes character getting stuck in sit state after accidentally
right-clicking a chair GO in Goldshire Inn (or similar)
Nameplates:
- Use getRenderPositionForGuid() (renderer visual position) as primary
source for nameplate anchor, falling back to entity X/Y/Z only when
no render instance exists yet; keeps health bars in sync with the
rendered model instead of the parallel entity interpolator
- Add ITEM_FIELD_DURABILITY (60) and ITEM_FIELD_MAXDURABILITY (61) to
update_field_table.hpp enum and wotlk/update_fields.json
- Add curDurability/maxDurability to OnlineItemInfo and ItemDef structs
- Parse durability fields in OBJECT_CREATE and OBJECT_VALUES handlers;
preserve existing values on partial updates (fixes stale durability
being reset to 0 on stack-count-only updates)
- Propagate durability to ItemDef in all 5 rebuildOnlineInventory() paths
- Implement GameHandler::repairItem() and repairAll() via CMSG_REPAIR_ITEM
(itemGuid=0 repairs all equipped items per WotLK protocol)
- Add canRepair flag to ListInventoryData; set it when player selects
GOSSIP_OPTION_ARMORER in gossip window
- Show "Repair All" button in vendor window header when canRepair=true
- Display color-coded durability in item tooltip (green >50%, yellow
>25%, red <=25%)
- application.cpp creature sync loop: use entity->isEntityMoving() alongside
planarDist to detect movement; entities > 150u have stale getX/Y/Z (distance
culled in GameHandler::update) but isEntityMoving() correctly reflects active
startMoveTo paths from SMSG_MONSTER_MOVE. Fixes distant NPCs playing Stand
while creatureMoveCallback drives their renderer to Run.
- Switch sync loop to getLatestX/Y/Z (server-authoritative destination) for
both the distance check and renderPos so creature positions are never stale
from cull lag, and don't call moveInstanceTo when only entityIsMoving (no
planarDist): the renderer's spline-driven move from creatureMoveCallback is
already correct and shouldn't be cancelled by the per-frame sync.
- game_screen.cpp: replace scratch-built ring-burst level-up overlay with a
simple "You have reached level X!" centered text (WoW style). The actual 3D
visual is already handled by Renderer::triggerLevelUpEffect (LevelUp.m2).
- Add setMovementPitch() and isSwimming() to GameHandler
- In the per-frame sync block, derive the pitch angle from the camera's
forward vector (asin of the Z component) and write it to movementInfo.pitch
whenever FLYING or SWIMMING flags are set — the server includes the pitch
field in those packets, so sending 0 made other players see the character
flying perfectly flat even when the camera was pitched
- Also tilt the mount model (setMountPitchRoll) to match the flight direction
during player-controlled flight, and reset to 0 when not flying
- Add getServerTurnRate() accessor and turnRateOverride_ field so the
keyboard turn speed respects SMSG_FORCE_TURN_RATE_CHANGE from server
- Convert rad/s → deg/s before applying to camera yaw logic
- Fix SMSG_SPLINE_SET_RUN_BACK/SWIM/FLIGHT/FLIGHT_BACK/SWIM_BACK/WALK/
TURN_RATE handlers: all previously discarded the value; now update the
corresponding serverXxxSpeed_ / serverTurnRate_ field when GUID matches
playerGuid (camera controller syncs these every frame)
SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE was already ACK'd and stored in
serverFlightBackSpeed_, but the value was never accessible or synced
to the CameraController. Backward flight movement always used forward
flight speed (flightSpeedOverride_), making it faster than the server
intended.
- Add getServerFlightBackSpeed() accessor in GameHandler
- Add flightBackSpeedOverride_ field and setter in CameraController
- Apply it in the fly movement block: backward-only flight uses the
back speed; forward or strafing uses the forward speed as WoW does
- Fallback: 50% of forward flight speed when override is unset
- Sync per-frame in application.cpp alongside the other speed overrides
Backward swimming was using 50% of forward swim speed as a hardcoded
fallback. Wire up the server-authoritative swim back speed so Warlock
Dark Pact, buffs, and server-forced speed changes all apply correctly
when swimming backward.
- game_handler.hpp: add getServerSwimBackSpeed() accessor
- camera_controller.hpp: add swimBackSpeedOverride_ field + setter
- camera_controller.cpp: apply swimBackSpeedOverride_ when player
swims backward without forward input; fall back to 50% of swim speed
- application.cpp: sync swim back speed each frame
When the server sets MovementFlags::HOVER (SMSG_MOVE_SET_HOVER), the
player now floats 4 yards above the nearest ground surface instead of
standing on it. Uses the existing floor-snap path with a HOVER_HEIGHT
offset applied to the snap target.
- game_handler.hpp: add isHovering() accessor (reads HOVER flag from
movementInfo.flags, which is already set by handleForceMoveFlagChange)
- camera_controller.hpp: add hoverActive_ field and setHoverActive()
- camera_controller.cpp: apply HOVER_HEIGHT = 4.0f offset at floor snap
- application.cpp: sync hover state each frame alongside other movement
states (gravity, feather fall, water walk, flying)
Previously only run speed was synced. Now all server-driven movement
speeds are forwarded to the camera controller each frame:
- runSpeedOverride_: server run speed (existing)
- walkSpeedOverride_: server walk speed (Ctrl key movement)
- swimSpeedOverride_: swim speed (Swim Form, Engineering fins)
- flightSpeedOverride_: flight speed (epic vs normal flying mounts)
- runBackSpeedOverride_: back-pedal speed
Each uses the server value when non-zero/sane, falling back to the
hardcoded WoW default constant otherwise.
serverFlightSpeed_ (from SMSG_FORCE_FLIGHT_SPEED_CHANGE) was stored but
never synced to CameraController. Add getServerFlightSpeed() accessor,
flightSpeedOverride_ field, and use it in the flying physics path so
normal vs epic flying mounts actually move at their correct speeds.
When CAN_FLY + FLYING movement flags are both set (flying mounts, Druid
Flight Form), the CameraController now uses 3D pitch-following movement
instead of ground physics:
- Forward/back follows the camera's 3D look direction (ascend when
looking up, descend when looking down)
- Space = ascend vertically, X (while mounted) = descend
- No gravity, no grounding, no jump coyote time
- Fall-damage checks suppressed (grounded=true)
Also wire up all remaining server movement state flags to CameraController:
- Feather Fall: cap terminal velocity at -2 m/s
- Water Walk: clamp to water surface, skip swim entry
- Flying: 3D movement with no gravity
All states synced each frame from GameHandler via isPlayerFlying(),
isFeatherFalling(), isWaterWalking(), isGravityDisabled().
SMSG_MOVE_WATER_WALK / SMSG_MOVE_LAND_WALK now correctly set/clear
WATER_WALK (0x00008000) in movementInfo.flags, ensuring the flag is
included in movement ACKs sent to the server.
In CameraController, when waterWalkActive_ is set and the player is
at or above the water surface (within 0.5 units), clamp them to the
water surface and mark as grounded — preventing water entry and allowing
them to walk across the water surface as the spell intends.
Feather Fall (SMSG_MOVE_FEATHER_FALL / SMSG_MOVE_NORMAL_FALL):
- Add FEATHER_FALL = 0x00004000 to MovementFlags enum
- Fix handlers to set/clear the flag instead of passing flag=0
- Cap downward terminal velocity at -2.0 m/s in CameraController when
feather fall is active (Slow Fall, Parachute, etc.)
All three handlers now correctly propagate server movement state flags
that were previously acknowledged without updating any local state.
serverWalkSpeed_ and serverSwimSpeed_ were stored in GameHandler but
never exposed or synced to the camera controller. The controller used
hardcoded WOW_WALK_SPEED and speed*SWIM_SPEED_FACTOR regardless of
server-sent speed changes.
Add getServerWalkSpeed()/getServerSwimSpeed() accessors, walkSpeedOverride_
and swimSpeedOverride_ fields in CameraController, and sync all three
server speeds each frame. Both swim speed sites (main and camera-collision
path) now use the override when set. This makes Slow debuffs (walk speed),
Swim Form, and Engineering fins actually affect movement speed.
SMSG_MOVE_GRAVITY_DISABLE/ENABLE now correctly set/clear the LEVITATING
movement flag instead of passing flag=0. GameHandler::isGravityDisabled()
reads the LEVITATING bit and is synced to CameraController each frame.
When gravity is disabled the physics loop bleeds off downward velocity
and skips gravity accumulation, so Levitate and similar effects actually
float the player rather than letting them fall through the world.
When SMSG_FORCE_MOVE_ROOT sets ROOT in movementInfo.flags, the
camera controller was not aware and continued to accept directional
input. This caused position desync (client moves, server sees player
as rooted).
- Add movementRooted_ flag to CameraController with setter/getter.
- Block nowForward/nowBackward/nowStrafe when movementRooted_ is set.
- Sync isPlayerRooted() from GameHandler to CameraController each
frame alongside the existing run-speed sync in application.cpp.
- Add GameHandler::isPlayerRooted() convenience accessor.