refactor(core): decompose Application::setupUICallbacks() into 7 domain handlers
Extract ~1,700 lines / 60+ inline [this]-capturing lambdas from the monolithic
Application::setupUICallbacks() into 7 focused callback handler classes following
the ToastManager/ChatPanel::setupCallbacks() pattern already in the codebase.
New handlers (include/core/ + src/core/):
- NPCInteractionCallbackHandler NPC greeting/farewell/vendor/aggro voice
- AudioCallbackHandler Music, positional sound, level-up, achievement, LFG
- EntitySpawnCallbackHandler Creature/player/GO spawn, despawn, move, state
- AnimationCallbackHandler Death, respawn, combat, emotes, charge, sprint, vehicle
- TransportCallbackHandler Mount, taxi, transport spawn/move
- WorldEntryCallbackHandler World entry, unstuck, hearthstone, bind point
- UIScreenCallbackHandler Auth, realm selection, char selection/creation/deletion
application.cpp: 4,462 → 2,791 lines (−1,671)
setupUICallbacks: ~1,700 → ~50 lines (thin orchestrator)
Deduplication:
resolveSoundEntryPath() — was 3× copy-paste of SoundEntries.dbc lookup
resolveNpcVoiceType() — was 4× copy-paste of display-ID→voice detection
precacheNearbyTiles() — was 3× copy-paste of 17×17 tile loop
4 helper lambdas — promoted to private methods on WorldEntryCallbackHandler
State migration out of Application:
charge* (6 vars) → AnimationCallbackHandler
hearth*/worldEntry*/taxi* → WorldEntryCallbackHandler
pendingCreatedCharacterName_ → UIScreenCallbackHandler
Bug fixes:
- Duplicate `namespace core {` in application.hpp caused wowee::std pollution
- AppState forward decl in ui_screen_callback_handler.hpp was at wrong scope
- world_loader.cpp accessed moved member vars directly via friend; now uses handler API
2026-04-05 16:48:17 +03:00
|
|
|
#include "core/entity_spawn_callback_handler.hpp"
|
|
|
|
|
#include "core/entity_spawner.hpp"
|
|
|
|
|
#include "core/coordinates.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include "rendering/renderer.hpp"
|
|
|
|
|
#include "rendering/character_renderer.hpp"
|
|
|
|
|
#include "rendering/m2_renderer.hpp"
|
|
|
|
|
#include "rendering/wmo_renderer.hpp"
|
|
|
|
|
#include "rendering/animation/animation_ids.hpp"
|
|
|
|
|
#include "game/game_handler.hpp"
|
|
|
|
|
|
|
|
|
|
namespace wowee { namespace core {
|
|
|
|
|
|
|
|
|
|
EntitySpawnCallbackHandler::EntitySpawnCallbackHandler(
|
|
|
|
|
EntitySpawner& entitySpawner,
|
|
|
|
|
rendering::Renderer& renderer,
|
|
|
|
|
game::GameHandler& gameHandler,
|
|
|
|
|
std::function<bool(uint64_t)> isLocalPlayerGuid)
|
|
|
|
|
: entitySpawner_(entitySpawner)
|
|
|
|
|
, renderer_(renderer)
|
|
|
|
|
, gameHandler_(gameHandler)
|
|
|
|
|
, isLocalPlayerGuid_(std::move(isLocalPlayerGuid))
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EntitySpawnCallbackHandler::setupCallbacks() {
|
|
|
|
|
// Creature spawn callback (online mode) - spawn creature models
|
|
|
|
|
gameHandler_.setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale) {
|
|
|
|
|
// Queue spawns to avoid hanging when many creatures appear at once.
|
|
|
|
|
// Deduplicate so repeated updates don't flood pending queue.
|
|
|
|
|
if (entitySpawner_.isCreatureSpawned(guid)) return;
|
|
|
|
|
if (entitySpawner_.isCreaturePending(guid)) return;
|
|
|
|
|
entitySpawner_.queueCreatureSpawn(guid, displayId, x, y, z, orientation, scale);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Player spawn callback (online mode) - spawn player models with correct textures
|
|
|
|
|
gameHandler_.setPlayerSpawnCallback([this](uint64_t guid,
|
|
|
|
|
uint32_t /*displayId*/,
|
|
|
|
|
uint8_t raceId,
|
|
|
|
|
uint8_t genderId,
|
|
|
|
|
uint32_t appearanceBytes,
|
|
|
|
|
uint8_t facialFeatures,
|
|
|
|
|
float x, float y, float z, float orientation) {
|
2026-04-05 20:12:17 -07:00
|
|
|
LOG_DEBUG("playerSpawnCallback: guid=0x", std::hex, guid, std::dec,
|
refactor(core): decompose Application::setupUICallbacks() into 7 domain handlers
Extract ~1,700 lines / 60+ inline [this]-capturing lambdas from the monolithic
Application::setupUICallbacks() into 7 focused callback handler classes following
the ToastManager/ChatPanel::setupCallbacks() pattern already in the codebase.
New handlers (include/core/ + src/core/):
- NPCInteractionCallbackHandler NPC greeting/farewell/vendor/aggro voice
- AudioCallbackHandler Music, positional sound, level-up, achievement, LFG
- EntitySpawnCallbackHandler Creature/player/GO spawn, despawn, move, state
- AnimationCallbackHandler Death, respawn, combat, emotes, charge, sprint, vehicle
- TransportCallbackHandler Mount, taxi, transport spawn/move
- WorldEntryCallbackHandler World entry, unstuck, hearthstone, bind point
- UIScreenCallbackHandler Auth, realm selection, char selection/creation/deletion
application.cpp: 4,462 → 2,791 lines (−1,671)
setupUICallbacks: ~1,700 → ~50 lines (thin orchestrator)
Deduplication:
resolveSoundEntryPath() — was 3× copy-paste of SoundEntries.dbc lookup
resolveNpcVoiceType() — was 4× copy-paste of display-ID→voice detection
precacheNearbyTiles() — was 3× copy-paste of 17×17 tile loop
4 helper lambdas — promoted to private methods on WorldEntryCallbackHandler
State migration out of Application:
charge* (6 vars) → AnimationCallbackHandler
hearth*/worldEntry*/taxi* → WorldEntryCallbackHandler
pendingCreatedCharacterName_ → UIScreenCallbackHandler
Bug fixes:
- Duplicate `namespace core {` in application.hpp caused wowee::std pollution
- AppState forward decl in ui_screen_callback_handler.hpp was at wrong scope
- world_loader.cpp accessed moved member vars directly via friend; now uses handler API
2026-04-05 16:48:17 +03:00
|
|
|
" race=", static_cast<int>(raceId), " gender=", static_cast<int>(genderId),
|
|
|
|
|
" pos=(", x, ",", y, ",", z, ")");
|
|
|
|
|
// Skip local player — already spawned as the main character
|
|
|
|
|
if (isLocalPlayerGuid_(guid)) return;
|
|
|
|
|
if (entitySpawner_.isPlayerSpawned(guid)) return;
|
|
|
|
|
if (entitySpawner_.isPlayerPending(guid)) return;
|
|
|
|
|
entitySpawner_.queuePlayerSpawn(guid, raceId, genderId, appearanceBytes, facialFeatures, x, y, z, orientation);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Online player equipment callback - apply armor geosets/skin overlays per player instance.
|
|
|
|
|
gameHandler_.setPlayerEquipmentCallback([this](uint64_t guid,
|
|
|
|
|
const std::array<uint32_t, 19>& displayInfoIds,
|
|
|
|
|
const std::array<uint8_t, 19>& inventoryTypes) {
|
|
|
|
|
// Queue equipment compositing instead of doing it immediately —
|
|
|
|
|
// compositeWithRegions is expensive (file I/O + CPU blit + GPU upload)
|
|
|
|
|
// and causes frame stutters if multiple players update at once.
|
|
|
|
|
entitySpawner_.queuePlayerEquipment(guid, displayInfoIds, inventoryTypes);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Creature despawn callback (online mode) - remove creature models
|
|
|
|
|
gameHandler_.setCreatureDespawnCallback([this](uint64_t guid) {
|
|
|
|
|
entitySpawner_.despawnCreature(guid);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
gameHandler_.setPlayerDespawnCallback([this](uint64_t guid) {
|
|
|
|
|
entitySpawner_.despawnPlayer(guid);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.)
|
|
|
|
|
gameHandler_.setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale) {
|
|
|
|
|
entitySpawner_.queueGameObjectSpawn(guid, entry, displayId, x, y, z, orientation, scale);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// GameObject despawn callback (online mode) - remove static models
|
|
|
|
|
gameHandler_.setGameObjectDespawnCallback([this](uint64_t guid) {
|
|
|
|
|
entitySpawner_.despawnGameObject(guid);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// GameObject custom animation callback (e.g. chest opening)
|
|
|
|
|
gameHandler_.setGameObjectCustomAnimCallback([this](uint64_t guid, uint32_t animId) {
|
|
|
|
|
auto& goInstances = entitySpawner_.getGameObjectInstances();
|
|
|
|
|
auto it = goInstances.find(guid);
|
|
|
|
|
if (it == goInstances.end()) return;
|
|
|
|
|
auto& info = it->second;
|
|
|
|
|
if (!info.isWmo) {
|
|
|
|
|
if (auto* m2r = renderer_.getM2Renderer()) {
|
|
|
|
|
// Play the custom animation as a one-shot if model supports it
|
|
|
|
|
if (m2r->hasAnimation(info.instanceId, animId))
|
|
|
|
|
m2r->setInstanceAnimation(info.instanceId, animId, false);
|
|
|
|
|
else
|
|
|
|
|
m2r->setInstanceAnimationFrozen(info.instanceId, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// GameObject state change callback — animate doors/chests opening/closing/destroying
|
|
|
|
|
gameHandler_.setGameObjectStateCallback([this](uint64_t guid, uint8_t goState) {
|
|
|
|
|
auto& goInstances = entitySpawner_.getGameObjectInstances();
|
|
|
|
|
auto it = goInstances.find(guid);
|
|
|
|
|
if (it == goInstances.end()) return;
|
|
|
|
|
auto& info = it->second;
|
|
|
|
|
if (info.isWmo) return; // WMOs don't have M2 animation sequences
|
|
|
|
|
auto* m2r = renderer_.getM2Renderer();
|
|
|
|
|
if (!m2r) return;
|
|
|
|
|
uint32_t instId = info.instanceId;
|
|
|
|
|
// GO states: 0=READY(closed), 1=OPEN, 2=DESTROYED/ACTIVE
|
|
|
|
|
if (goState == 1) {
|
|
|
|
|
// Opening: play OPEN(148) one-shot, fall back to unfreezing
|
|
|
|
|
if (m2r->hasAnimation(instId, 148))
|
|
|
|
|
m2r->setInstanceAnimation(instId, 148, false);
|
|
|
|
|
else
|
|
|
|
|
m2r->setInstanceAnimationFrozen(instId, false);
|
|
|
|
|
} else if (goState == 2) {
|
|
|
|
|
// Destroyed: play DESTROY(149) one-shot
|
|
|
|
|
if (m2r->hasAnimation(instId, 149))
|
|
|
|
|
m2r->setInstanceAnimation(instId, 149, false);
|
|
|
|
|
} else {
|
|
|
|
|
// Closed: play CLOSE(146) one-shot, else freeze
|
|
|
|
|
if (m2r->hasAnimation(instId, 146))
|
|
|
|
|
m2r->setInstanceAnimation(instId, 146, false);
|
|
|
|
|
else
|
|
|
|
|
m2r->setInstanceAnimationFrozen(instId, true);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Creature move callback (online mode) - update creature positions
|
|
|
|
|
gameHandler_.setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
|
|
|
|
if (!renderer_.getCharacterRenderer()) return;
|
|
|
|
|
uint32_t instanceId = 0;
|
|
|
|
|
bool isPlayer = false;
|
|
|
|
|
instanceId = entitySpawner_.getPlayerInstanceId(guid);
|
|
|
|
|
if (instanceId != 0) { isPlayer = true; }
|
|
|
|
|
else {
|
|
|
|
|
instanceId = entitySpawner_.getCreatureInstanceId(guid);
|
|
|
|
|
}
|
|
|
|
|
if (instanceId != 0) {
|
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
|
|
|
|
float durationSec = static_cast<float>(durationMs) / 1000.0f;
|
|
|
|
|
renderer_.getCharacterRenderer()->moveInstanceTo(instanceId, renderPos, durationSec);
|
|
|
|
|
// Play Run animation (anim 5) for the duration of the spline move.
|
|
|
|
|
// WoW M2 animation IDs: 4=Walk, 5=Run.
|
|
|
|
|
// Don't override Death animation (1). The per-frame sync loop will return to
|
|
|
|
|
// Stand when movement stops.
|
|
|
|
|
if (durationMs > 0) {
|
|
|
|
|
// Player animation is managed by the local renderer state machine —
|
|
|
|
|
// don't reset it here or every server movement packet restarts the
|
|
|
|
|
// run cycle from frame 0, causing visible stutter.
|
|
|
|
|
if (!isPlayer) {
|
|
|
|
|
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
|
|
|
|
auto* cr = renderer_.getCharacterRenderer();
|
|
|
|
|
bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur);
|
|
|
|
|
// Only start Run if not already running and not in Death animation.
|
|
|
|
|
if (!gotState || (curAnimId != rendering::anim::DEATH && curAnimId != rendering::anim::RUN)) {
|
|
|
|
|
cr->playAnimation(instanceId, rendering::anim::RUN, /*loop=*/true);
|
|
|
|
|
}
|
|
|
|
|
entitySpawner_.getCreatureWasMoving()[guid] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
gameHandler_.setGameObjectMoveCallback([this](uint64_t guid, float x, float y, float z, float orientation) {
|
|
|
|
|
auto& goInstMap = entitySpawner_.getGameObjectInstances();
|
|
|
|
|
auto it = goInstMap.find(guid);
|
|
|
|
|
if (it == goInstMap.end()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
|
|
|
|
auto& info = it->second;
|
|
|
|
|
if (info.isWmo) {
|
|
|
|
|
if (auto* wr = renderer_.getWMORenderer()) {
|
|
|
|
|
glm::mat4 transform(1.0f);
|
|
|
|
|
transform = glm::translate(transform, renderPos);
|
|
|
|
|
transform = glm::rotate(transform, orientation, glm::vec3(0, 0, 1));
|
|
|
|
|
wr->setInstanceTransform(info.instanceId, transform);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (auto* mr = renderer_.getM2Renderer()) {
|
|
|
|
|
glm::mat4 transform(1.0f);
|
|
|
|
|
transform = glm::translate(transform, renderPos);
|
|
|
|
|
mr->setInstanceTransform(info.instanceId, transform);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}} // namespace wowee::core
|