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
This commit is contained in:
Paul 2026-04-05 16:48:17 +03:00
parent a23c2172a8
commit 6dcc06697b
18 changed files with 2293 additions and 1765 deletions

View file

@ -0,0 +1,275 @@
#include "core/transport_callback_handler.hpp"
#include "core/entity_spawner.hpp"
#include "core/world_loader.hpp"
#include "core/coordinates.hpp"
#include "core/logger.hpp"
#include "rendering/renderer.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/camera_controller.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/m2_renderer.hpp"
#include "game/game_handler.hpp"
#include "game/transport_manager.hpp"
#include <set>
namespace wowee { namespace core {
TransportCallbackHandler::TransportCallbackHandler(
EntitySpawner& entitySpawner,
rendering::Renderer& renderer,
game::GameHandler& gameHandler,
WorldLoader* worldLoader)
: entitySpawner_(entitySpawner)
, renderer_(renderer)
, gameHandler_(gameHandler)
, worldLoader_(worldLoader)
{
}
void TransportCallbackHandler::setupCallbacks() {
// Mount callback (online mode) - defer heavy model load to next frame
gameHandler_.setMountCallback([this](uint32_t mountDisplayId) {
if (mountDisplayId == 0) {
// Dismount is instant (no loading needed)
if (renderer_.getCharacterRenderer() && entitySpawner_.getMountInstanceId() != 0) {
renderer_.getCharacterRenderer()->removeInstance(entitySpawner_.getMountInstanceId());
entitySpawner_.clearMountState();
}
entitySpawner_.setMountDisplayId(0);
renderer_.clearMount();
LOG_INFO("Dismounted");
return;
}
// Queue the mount for processing in the next update() frame
entitySpawner_.setMountDisplayId(mountDisplayId);
});
// Taxi precache callback - preload terrain tiles along flight path
gameHandler_.setTaxiPrecacheCallback([this](const std::vector<glm::vec3>& path) {
if (!renderer_.getTerrainManager()) return;
std::set<std::pair<int, int>> uniqueTiles;
// Sample waypoints along path and gather tiles.
// Denser sampling + neighbor coverage reduces in-flight stream spikes.
const size_t stride = 2;
for (size_t i = 0; i < path.size(); i += stride) {
const auto& waypoint = path[i];
glm::vec3 renderPos = core::coords::canonicalToRender(waypoint);
int tileX = static_cast<int>(32 - (renderPos.x / 533.33333f));
int tileY = static_cast<int>(32 - (renderPos.y / 533.33333f));
if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
int nx = tileX + dx;
int ny = tileY + dy;
if (nx >= 0 && nx <= 63 && ny >= 0 && ny <= 63) {
uniqueTiles.insert({nx, ny});
}
}
}
}
}
// Ensure final destination tile is included.
if (!path.empty()) {
glm::vec3 renderPos = core::coords::canonicalToRender(path.back());
int tileX = static_cast<int>(32 - (renderPos.x / 533.33333f));
int tileY = static_cast<int>(32 - (renderPos.y / 533.33333f));
if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
int nx = tileX + dx;
int ny = tileY + dy;
if (nx >= 0 && nx <= 63 && ny >= 0 && ny <= 63) {
uniqueTiles.insert({nx, ny});
}
}
}
}
}
std::vector<std::pair<int, int>> tilesToLoad(uniqueTiles.begin(), uniqueTiles.end());
if (tilesToLoad.size() > 512) {
tilesToLoad.resize(512);
}
LOG_INFO("Precaching ", tilesToLoad.size(), " tiles for taxi route");
renderer_.getTerrainManager()->precacheTiles(tilesToLoad);
});
// Taxi orientation callback - update mount rotation during flight
gameHandler_.setTaxiOrientationCallback([this](float yaw, float pitch, float roll) {
if (renderer_.getCameraController()) {
// Taxi callback now provides render-space yaw directly.
float yawDegrees = glm::degrees(yaw);
renderer_.getCameraController()->setFacingYaw(yawDegrees);
renderer_.setCharacterYaw(yawDegrees);
// Set mount pitch and roll for realistic flight animation
renderer_.setMountPitchRoll(pitch, roll);
}
});
// Taxi flight start callback - keep non-blocking to avoid hitching at takeoff.
gameHandler_.setTaxiFlightStartCallback([this]() {
if (renderer_.getTerrainManager() && renderer_.getM2Renderer()) {
LOG_INFO("Taxi flight start: incremental terrain/M2 streaming active");
uint32_t m2Count = renderer_.getM2Renderer()->getModelCount();
uint32_t instCount = renderer_.getM2Renderer()->getInstanceCount();
LOG_INFO("Current M2 VRAM state: ", m2Count, " models (", instCount, " instances)");
}
});
// Transport spawn callback (online mode) - register transports with TransportManager
gameHandler_.setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
// Get the GameObject instance now so late queue processing can rely on stable IDs.
auto& goInstances2 = entitySpawner_.getGameObjectInstances();
auto it = goInstances2.find(guid);
if (it == goInstances2.end()) {
LOG_WARNING("Transport spawn callback: GameObject instance not found for GUID 0x", std::hex, guid, std::dec);
return;
}
auto pendingIt = entitySpawner_.hasTransportRegistrationPending(guid);
if (pendingIt) {
entitySpawner_.updateTransportRegistration(guid, displayId, x, y, z, orientation);
} else {
entitySpawner_.queueTransportRegistration(guid, entry, displayId, x, y, z, orientation);
}
});
// Transport move callback (online mode) - update transport gameobject positions
gameHandler_.setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float orientation) {
LOG_DEBUG("Transport move callback: GUID=0x", std::hex, guid, std::dec,
" pos=(", x, ", ", y, ", ", z, ") orientation=", orientation);
auto* transportManager = gameHandler_.getTransportManager();
if (!transportManager) {
LOG_WARNING("Transport move callback: TransportManager is null!");
return;
}
if (entitySpawner_.hasTransportRegistrationPending(guid)) {
entitySpawner_.setTransportPendingMove(guid, x, y, z, orientation);
LOG_DEBUG("Queued transport move for pending registration GUID=0x", std::hex, guid, std::dec);
return;
}
// Check if transport exists - if not, treat this as a late spawn (reconnection/server restart)
if (!transportManager->getTransport(guid)) {
LOG_DEBUG("Received position update for unregistered transport 0x", std::hex, guid, std::dec,
" - auto-spawning from position update");
// Get transport info from entity manager
auto entity = gameHandler_.getEntityManager().getEntity(guid);
if (entity && entity->getType() == game::ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<game::GameObject>(entity);
uint32_t entry = go->getEntry();
uint32_t displayId = go->getDisplayId();
// Find the WMO instance for this transport (should exist from earlier GameObject spawn)
auto& goInstances3 = entitySpawner_.getGameObjectInstances();
auto it = goInstances3.find(guid);
if (it != goInstances3.end()) {
uint32_t wmoInstanceId = it->second.instanceId;
// TransportAnimation.dbc is indexed by GameObject entry
uint32_t pathId = entry;
const bool preferServerData = gameHandler_.hasServerTransportUpdate(guid);
// Coordinates are already canonical (converted in game_handler.cpp)
glm::vec3 canonicalSpawnPos(x, y, z);
// Check if we have a real usable path, otherwise remap/infer/fall back to stationary.
const bool shipOrZeppelinDisplay =
(displayId == 3015 || displayId == 3031 || displayId == 7546 ||
displayId == 7446 || displayId == 1587 || displayId == 2454 ||
displayId == 807 || displayId == 808);
bool hasUsablePath = transportManager->hasPathForEntry(entry);
if (shipOrZeppelinDisplay) {
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
}
if (preferServerData) {
// Strict server-authoritative mode: no inferred/remapped fallback routes.
if (!hasUsablePath) {
std::vector<glm::vec3> path = { canonicalSpawnPos };
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
LOG_INFO("Auto-spawned transport in strict server-first mode (stationary fallback): entry=", entry,
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
} else {
LOG_INFO("Auto-spawned transport in server-first mode with entry DBC path: entry=", entry,
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
}
} else if (!hasUsablePath) {
bool allowZOnly = (displayId == 455 || displayId == 462);
uint32_t inferredPath = transportManager->inferDbcPathForSpawn(
canonicalSpawnPos, 1200.0f, allowZOnly);
if (inferredPath != 0) {
pathId = inferredPath;
LOG_INFO("Auto-spawned transport with inferred path: entry=", entry,
" inferredPath=", pathId, " displayId=", displayId,
" wmoInstance=", wmoInstanceId);
} else {
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
if (remappedPath != 0) {
pathId = remappedPath;
LOG_INFO("Auto-spawned transport with remapped fallback path: entry=", entry,
" remappedPath=", pathId, " displayId=", displayId,
" wmoInstance=", wmoInstanceId);
} else {
std::vector<glm::vec3> path = { canonicalSpawnPos };
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
LOG_INFO("Auto-spawned transport with stationary path: entry=", entry,
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
}
}
} else {
LOG_INFO("Auto-spawned transport with real path: entry=", entry,
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
}
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
// Keep type in sync with the spawned instance; needed for M2 lift boarding/motion.
if (!it->second.isWmo) {
if (auto* tr = transportManager->getTransport(guid)) {
tr->isM2 = true;
}
}
} else {
entitySpawner_.setTransportPendingMove(guid, x, y, z, orientation);
LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,
" - WMO instance not found yet (queued move for replay)");
return;
}
} else {
entitySpawner_.setTransportPendingMove(guid, x, y, z, orientation);
LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,
" - entity not found in EntityManager (queued move for replay)");
return;
}
}
// Update TransportManager's internal state (position, rotation, transform matrices)
// This also updates the WMO renderer automatically
// Coordinates are already canonical (converted in game_handler.cpp when entity was created)
glm::vec3 canonicalPos(x, y, z);
transportManager->updateServerTransport(guid, canonicalPos, orientation);
// Move player with transport if riding it
if (gameHandler_.isOnTransport() && gameHandler_.getPlayerTransportGuid() == guid) {
auto* cc = renderer_.getCameraController();
if (cc) {
glm::vec3* ft = cc->getFollowTargetMutable();
if (ft) {
// Get player world position from TransportManager (handles transform properly)
glm::vec3 offset = gameHandler_.getPlayerTransportOffset();
glm::vec3 worldPos = transportManager->getPlayerWorldPosition(guid, offset);
*ft = worldPos;
}
}
}
});
}
}} // namespace wowee::core