mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-14 08:23:52 +00:00
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:
parent
a23c2172a8
commit
6dcc06697b
18 changed files with 2293 additions and 1765 deletions
|
|
@ -490,6 +490,13 @@ set(WOWEE_SOURCES
|
|||
src/core/entity_spawner.cpp
|
||||
src/core/appearance_composer.cpp
|
||||
src/core/world_loader.cpp
|
||||
src/core/npc_interaction_callback_handler.cpp
|
||||
src/core/audio_callback_handler.cpp
|
||||
src/core/entity_spawn_callback_handler.cpp
|
||||
src/core/animation_callback_handler.cpp
|
||||
src/core/transport_callback_handler.cpp
|
||||
src/core/world_entry_callback_handler.cpp
|
||||
src/core/ui_screen_callback_handler.cpp
|
||||
src/core/window.cpp
|
||||
src/core/input.cpp
|
||||
src/core/logger.cpp
|
||||
|
|
|
|||
50
include/core/animation_callback_handler.hpp
Normal file
50
include/core/animation_callback_handler.hpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace rendering { class Renderer; }
|
||||
namespace game { class GameHandler; }
|
||||
namespace core { class EntitySpawner; }
|
||||
|
||||
namespace core {
|
||||
|
||||
/// Handles animation callbacks: death, respawn, swing, hit reaction, spell cast, emote,
|
||||
/// stun, stealth, health, ghost, stand state, loot, sprint, vehicle, charge.
|
||||
/// Owns charge rush state (interpolated in update).
|
||||
class AnimationCallbackHandler {
|
||||
public:
|
||||
AnimationCallbackHandler(EntitySpawner& entitySpawner,
|
||||
rendering::Renderer& renderer,
|
||||
game::GameHandler& gameHandler);
|
||||
|
||||
void setupCallbacks();
|
||||
|
||||
/// Called each frame from Application::update() to drive charge interpolation.
|
||||
/// Returns true if charge is active (player is externally driven).
|
||||
bool updateCharge(float deltaTime);
|
||||
|
||||
// Charge state queries (used by Application::update for externallyDrivenMotion)
|
||||
bool isCharging() const { return chargeActive_; }
|
||||
|
||||
// Reset charge state (logout/disconnect)
|
||||
void resetChargeState();
|
||||
|
||||
private:
|
||||
EntitySpawner& entitySpawner_;
|
||||
rendering::Renderer& renderer_;
|
||||
game::GameHandler& gameHandler_;
|
||||
|
||||
// Charge rush state (moved from Application)
|
||||
bool chargeActive_ = false;
|
||||
float chargeTimer_ = 0.0f;
|
||||
float chargeDuration_ = 0.0f;
|
||||
glm::vec3 chargeStartPos_{0.0f}; // Render coordinates
|
||||
glm::vec3 chargeEndPos_{0.0f}; // Render coordinates
|
||||
uint64_t chargeTargetGuid_ = 0;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
|
|
@ -34,6 +34,15 @@ namespace addons { class AddonManager; }
|
|||
|
||||
namespace core {
|
||||
|
||||
// Handler forward declarations
|
||||
class NPCInteractionCallbackHandler;
|
||||
class AudioCallbackHandler;
|
||||
class EntitySpawnCallbackHandler;
|
||||
class AnimationCallbackHandler;
|
||||
class TransportCallbackHandler;
|
||||
class WorldEntryCallbackHandler;
|
||||
class UIScreenCallbackHandler;
|
||||
|
||||
enum class AppState {
|
||||
AUTHENTICATION,
|
||||
REALM_SELECTION,
|
||||
|
|
@ -134,9 +143,17 @@ private:
|
|||
std::unique_ptr<WorldLoader> worldLoader_;
|
||||
std::unique_ptr<audio::AudioCoordinator> audioCoordinator_;
|
||||
|
||||
// Callback handlers (extracted from setupUICallbacks)
|
||||
std::unique_ptr<NPCInteractionCallbackHandler> npcInteractionCallbacks_;
|
||||
std::unique_ptr<AudioCallbackHandler> audioCallbacks_;
|
||||
std::unique_ptr<EntitySpawnCallbackHandler> entitySpawnCallbacks_;
|
||||
std::unique_ptr<AnimationCallbackHandler> animationCallbacks_;
|
||||
std::unique_ptr<TransportCallbackHandler> transportCallbacks_;
|
||||
std::unique_ptr<WorldEntryCallbackHandler> worldEntryCallbacks_;
|
||||
std::unique_ptr<UIScreenCallbackHandler> uiScreenCallbacks_;
|
||||
|
||||
AppState state = AppState::AUTHENTICATION;
|
||||
bool running = false;
|
||||
std::string pendingCreatedCharacterName_; // Auto-select after character creation
|
||||
bool playerCharacterSpawned = false;
|
||||
bool npcsSpawned = false;
|
||||
bool spawnSnapToGround = true;
|
||||
|
|
@ -154,27 +171,11 @@ private:
|
|||
static inline const std::string emptyString_;
|
||||
static inline const std::vector<std::string> emptyStringVec_;
|
||||
|
||||
bool lastTaxiFlight_ = false;
|
||||
float taxiLandingClampTimer_ = 0.0f;
|
||||
float worldEntryMovementGraceTimer_ = 0.0f;
|
||||
|
||||
// Hearth teleport: freeze player until terrain loads at destination
|
||||
bool hearthTeleportPending_ = false;
|
||||
glm::vec3 hearthTeleportPos_{0.0f}; // render coords
|
||||
float hearthTeleportTimer_ = 0.0f; // timeout safety
|
||||
float facingSendCooldown_ = 0.0f; // Rate-limits MSG_MOVE_SET_FACING
|
||||
float lastSentCanonicalYaw_ = 1000.0f; // Sentinel — triggers first send
|
||||
float taxiStreamCooldown_ = 0.0f;
|
||||
bool idleYawned_ = false;
|
||||
|
||||
// Charge rush state
|
||||
bool chargeActive_ = false;
|
||||
float chargeTimer_ = 0.0f;
|
||||
float chargeDuration_ = 0.0f;
|
||||
glm::vec3 chargeStartPos_{0.0f}; // Render coordinates
|
||||
glm::vec3 chargeEndPos_{0.0f}; // Render coordinates
|
||||
uint64_t chargeTargetGuid_ = 0;
|
||||
|
||||
bool wasAutoAttacking_ = false;
|
||||
|
||||
// Quest marker billboard sprites (above NPCs)
|
||||
|
|
|
|||
40
include/core/audio_callback_handler.hpp
Normal file
40
include/core/audio_callback_handler.hpp
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace rendering { class Renderer; }
|
||||
namespace game { class GameHandler; }
|
||||
namespace audio { class AudioCoordinator; }
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace ui { class UIManager; }
|
||||
|
||||
namespace core {
|
||||
|
||||
/// Handles audio-related callbacks: music, sound effects, level-up, achievement, LFG.
|
||||
class AudioCallbackHandler {
|
||||
public:
|
||||
AudioCallbackHandler(pipeline::AssetManager& assetManager,
|
||||
audio::AudioCoordinator* audioCoordinator,
|
||||
rendering::Renderer* renderer,
|
||||
ui::UIManager* uiManager,
|
||||
game::GameHandler& gameHandler);
|
||||
|
||||
void setupCallbacks();
|
||||
|
||||
private:
|
||||
/// Resolve SoundEntries.dbc → file path for a given soundId (eliminates 3x copy-paste)
|
||||
std::optional<std::string> resolveSoundEntryPath(uint32_t soundId) const;
|
||||
|
||||
pipeline::AssetManager& assetManager_;
|
||||
audio::AudioCoordinator* audioCoordinator_;
|
||||
rendering::Renderer* renderer_;
|
||||
ui::UIManager* uiManager_;
|
||||
game::GameHandler& gameHandler_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
34
include/core/entity_spawn_callback_handler.hpp
Normal file
34
include/core/entity_spawn_callback_handler.hpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace rendering { class Renderer; }
|
||||
namespace game { class GameHandler; }
|
||||
namespace core { class EntitySpawner; }
|
||||
|
||||
namespace core {
|
||||
|
||||
/// Handles entity spawn/despawn callbacks: creatures, players, game objects.
|
||||
class EntitySpawnCallbackHandler {
|
||||
public:
|
||||
/// @param isLocalPlayerGuid Returns true if the given GUID is the local player (to skip self-spawn)
|
||||
EntitySpawnCallbackHandler(EntitySpawner& entitySpawner,
|
||||
rendering::Renderer& renderer,
|
||||
game::GameHandler& gameHandler,
|
||||
std::function<bool(uint64_t)> isLocalPlayerGuid);
|
||||
|
||||
void setupCallbacks();
|
||||
|
||||
private:
|
||||
EntitySpawner& entitySpawner_;
|
||||
rendering::Renderer& renderer_;
|
||||
game::GameHandler& gameHandler_;
|
||||
std::function<bool(uint64_t)> isLocalPlayerGuid_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
37
include/core/npc_interaction_callback_handler.hpp
Normal file
37
include/core/npc_interaction_callback_handler.hpp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <glm/glm.hpp>
|
||||
#include "audio/npc_voice_manager.hpp"
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace rendering { class Renderer; }
|
||||
namespace game { class GameHandler; }
|
||||
namespace audio { class AudioCoordinator; }
|
||||
namespace core { class EntitySpawner; }
|
||||
|
||||
namespace core {
|
||||
|
||||
/// Handles NPC interaction callbacks: greeting, farewell, vendor, aggro voice lines.
|
||||
class NPCInteractionCallbackHandler {
|
||||
public:
|
||||
NPCInteractionCallbackHandler(EntitySpawner& entitySpawner,
|
||||
rendering::Renderer* renderer,
|
||||
game::GameHandler& gameHandler,
|
||||
audio::AudioCoordinator* audioCoordinator);
|
||||
|
||||
void setupCallbacks();
|
||||
|
||||
private:
|
||||
/// Resolve NPC voice type from GUID (eliminates 4x copy-paste of display-ID lookup)
|
||||
audio::VoiceType resolveNpcVoiceType(uint64_t guid) const;
|
||||
|
||||
EntitySpawner& entitySpawner_;
|
||||
rendering::Renderer* renderer_;
|
||||
game::GameHandler& gameHandler_;
|
||||
audio::AudioCoordinator* audioCoordinator_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
33
include/core/transport_callback_handler.hpp
Normal file
33
include/core/transport_callback_handler.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace rendering { class Renderer; }
|
||||
namespace game { class GameHandler; }
|
||||
namespace core { class EntitySpawner; class WorldLoader; }
|
||||
|
||||
namespace core {
|
||||
|
||||
/// Handles transport-related callbacks: transport spawn/move, taxi, mount.
|
||||
class TransportCallbackHandler {
|
||||
public:
|
||||
TransportCallbackHandler(EntitySpawner& entitySpawner,
|
||||
rendering::Renderer& renderer,
|
||||
game::GameHandler& gameHandler,
|
||||
WorldLoader* worldLoader);
|
||||
|
||||
void setupCallbacks();
|
||||
|
||||
private:
|
||||
EntitySpawner& entitySpawner_;
|
||||
rendering::Renderer& renderer_;
|
||||
game::GameHandler& gameHandler_;
|
||||
WorldLoader* worldLoader_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
46
include/core/ui_screen_callback_handler.hpp
Normal file
46
include/core/ui_screen_callback_handler.hpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace ui { class UIManager; }
|
||||
namespace game { class GameHandler; class ExpansionRegistry; }
|
||||
namespace auth { class AuthHandler; }
|
||||
namespace pipeline { class AssetManager; }
|
||||
|
||||
namespace core {
|
||||
|
||||
// Forward-declared in application.hpp
|
||||
enum class AppState;
|
||||
|
||||
/// Handles authentication, realm selection, character selection/creation UI callbacks.
|
||||
/// Owns pendingCreatedCharacterName_.
|
||||
class UIScreenCallbackHandler {
|
||||
public:
|
||||
using SetStateFn = std::function<void(AppState)>;
|
||||
|
||||
UIScreenCallbackHandler(ui::UIManager& uiManager,
|
||||
game::GameHandler& gameHandler,
|
||||
auth::AuthHandler& authHandler,
|
||||
game::ExpansionRegistry* expansionRegistry,
|
||||
pipeline::AssetManager* assetManager,
|
||||
SetStateFn setState);
|
||||
|
||||
void setupCallbacks();
|
||||
|
||||
private:
|
||||
ui::UIManager& uiManager_;
|
||||
game::GameHandler& gameHandler_;
|
||||
auth::AuthHandler& authHandler_;
|
||||
game::ExpansionRegistry* expansionRegistry_;
|
||||
pipeline::AssetManager* assetManager_;
|
||||
SetStateFn setState_;
|
||||
|
||||
std::string pendingCreatedCharacterName_; // Auto-select after character creation
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
78
include/core/world_entry_callback_handler.hpp
Normal file
78
include/core/world_entry_callback_handler.hpp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace rendering { class Renderer; }
|
||||
namespace game { class GameHandler; }
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace audio { class AudioCoordinator; }
|
||||
namespace core { class EntitySpawner; class WorldLoader; }
|
||||
|
||||
namespace core {
|
||||
|
||||
/// Handles world entry, unstuck, hearthstone, and bind point callbacks.
|
||||
/// Owns hearth-teleport state and worldEntryMovementGraceTimer.
|
||||
class WorldEntryCallbackHandler {
|
||||
public:
|
||||
WorldEntryCallbackHandler(rendering::Renderer& renderer,
|
||||
game::GameHandler& gameHandler,
|
||||
WorldLoader* worldLoader,
|
||||
EntitySpawner* entitySpawner,
|
||||
audio::AudioCoordinator* audioCoordinator,
|
||||
pipeline::AssetManager* assetManager);
|
||||
|
||||
void setupCallbacks();
|
||||
|
||||
/// Called each frame from Application::update() to manage hearth-teleport freeze/thaw.
|
||||
void update(float deltaTime);
|
||||
|
||||
// State queries (used by Application::update)
|
||||
float getWorldEntryMovementGraceTimer() const { return worldEntryMovementGraceTimer_; }
|
||||
void setWorldEntryMovementGraceTimer(float t) { worldEntryMovementGraceTimer_ = t; }
|
||||
bool isHearthTeleportPending() const { return hearthTeleportPending_; }
|
||||
|
||||
// Reset state (logout/disconnect)
|
||||
void resetState();
|
||||
|
||||
// Taxi state (managed by Application::update, but tracked here for clarity)
|
||||
bool getLastTaxiFlight() const { return lastTaxiFlight_; }
|
||||
void setLastTaxiFlight(bool v) { lastTaxiFlight_ = v; }
|
||||
float getTaxiLandingClampTimer() const { return taxiLandingClampTimer_; }
|
||||
void setTaxiLandingClampTimer(float t) { taxiLandingClampTimer_ = t; }
|
||||
|
||||
private:
|
||||
/// Sample best floor height at (x, y) from terrain, WMO, and M2 (eliminates 3x duplication)
|
||||
std::optional<float> sampleBestFloorAt(float x, float y, float probeZ) const;
|
||||
|
||||
/// Clear stuck movement state on player
|
||||
void clearStuckMovement();
|
||||
|
||||
/// Sync teleported render position to server
|
||||
void syncTeleportedPositionToServer(const glm::vec3& renderPos);
|
||||
|
||||
/// Force server-side teleport via GM command
|
||||
void forceServerTeleportCommand(const glm::vec3& renderPos);
|
||||
|
||||
rendering::Renderer& renderer_;
|
||||
game::GameHandler& gameHandler_;
|
||||
WorldLoader* worldLoader_;
|
||||
EntitySpawner* entitySpawner_;
|
||||
audio::AudioCoordinator* audioCoordinator_;
|
||||
pipeline::AssetManager* assetManager_;
|
||||
|
||||
// Hearth teleport: freeze player until terrain loads at destination (moved from Application)
|
||||
bool hearthTeleportPending_ = false;
|
||||
glm::vec3 hearthTeleportPos_{0.0f}; // render coords
|
||||
float hearthTeleportTimer_ = 0.0f; // timeout safety
|
||||
|
||||
float worldEntryMovementGraceTimer_ = 0.0f;
|
||||
bool lastTaxiFlight_ = false;
|
||||
float taxiLandingClampTimer_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
548
src/core/animation_callback_handler.cpp
Normal file
548
src/core/animation_callback_handler.cpp
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
#include "core/animation_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/camera_controller.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "game/world_packets.hpp"
|
||||
#include "audio/audio_engine.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee { namespace core {
|
||||
|
||||
AnimationCallbackHandler::AnimationCallbackHandler(
|
||||
EntitySpawner& entitySpawner,
|
||||
rendering::Renderer& renderer,
|
||||
game::GameHandler& gameHandler)
|
||||
: entitySpawner_(entitySpawner)
|
||||
, renderer_(renderer)
|
||||
, gameHandler_(gameHandler)
|
||||
{
|
||||
}
|
||||
|
||||
void AnimationCallbackHandler::resetChargeState() {
|
||||
chargeActive_ = false;
|
||||
chargeTimer_ = 0.0f;
|
||||
chargeDuration_ = 0.0f;
|
||||
chargeTargetGuid_ = 0;
|
||||
}
|
||||
|
||||
bool AnimationCallbackHandler::updateCharge(float deltaTime) {
|
||||
if (!chargeActive_) return false;
|
||||
|
||||
// Warrior Charge: lerp position from start to end using smoothstep
|
||||
chargeTimer_ += deltaTime;
|
||||
float t = std::min(chargeTimer_ / chargeDuration_, 1.0f);
|
||||
// smoothstep for natural acceleration/deceleration
|
||||
float s = t * t * (3.0f - 2.0f * t);
|
||||
glm::vec3 renderPos = chargeStartPos_ + (chargeEndPos_ - chargeStartPos_) * s;
|
||||
renderer_.getCharacterPosition() = renderPos;
|
||||
|
||||
// Keep facing toward target and emit charge effect
|
||||
glm::vec3 dir = chargeEndPos_ - chargeStartPos_;
|
||||
float dirLenSq = glm::dot(dir, dir);
|
||||
if (dirLenSq > 1e-4f) {
|
||||
dir *= glm::inversesqrt(dirLenSq);
|
||||
float yawDeg = glm::degrees(std::atan2(dir.x, dir.y));
|
||||
renderer_.setCharacterYaw(yawDeg);
|
||||
renderer_.emitChargeEffect(renderPos, dir);
|
||||
}
|
||||
|
||||
// Sync to game handler
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
|
||||
gameHandler_.setPosition(canonical.x, canonical.y, canonical.z);
|
||||
|
||||
// Update camera follow target
|
||||
if (renderer_.getCameraController()) {
|
||||
glm::vec3* followTarget = renderer_.getCameraController()->getFollowTargetMutable();
|
||||
if (followTarget) {
|
||||
*followTarget = renderPos;
|
||||
}
|
||||
}
|
||||
|
||||
// Charge complete
|
||||
if (t >= 1.0f) {
|
||||
chargeActive_ = false;
|
||||
renderer_.setCharging(false);
|
||||
renderer_.stopChargeEffect();
|
||||
renderer_.getCameraController()->setExternalFollow(false);
|
||||
renderer_.getCameraController()->setExternalMoving(false);
|
||||
|
||||
// Snap to melee range of target's CURRENT position (it may have moved)
|
||||
if (chargeTargetGuid_ != 0) {
|
||||
auto targetEntity = gameHandler_.getEntityManager().getEntity(chargeTargetGuid_);
|
||||
if (targetEntity) {
|
||||
glm::vec3 targetCanonical(targetEntity->getX(), targetEntity->getY(), targetEntity->getZ());
|
||||
glm::vec3 targetRender = core::coords::canonicalToRender(targetCanonical);
|
||||
glm::vec3 toTarget = targetRender - renderPos;
|
||||
float dSq = glm::dot(toTarget, toTarget);
|
||||
if (dSq > 2.25f) {
|
||||
// Place us 1.5 units from target (well within 8-unit melee range)
|
||||
glm::vec3 snapPos = targetRender - toTarget * (1.5f * glm::inversesqrt(dSq));
|
||||
renderer_.getCharacterPosition() = snapPos;
|
||||
glm::vec3 snapCanonical = core::coords::renderToCanonical(snapPos);
|
||||
gameHandler_.setPosition(snapCanonical.x, snapCanonical.y, snapCanonical.z);
|
||||
if (renderer_.getCameraController()) {
|
||||
glm::vec3* ft = renderer_.getCameraController()->getFollowTargetMutable();
|
||||
if (ft) *ft = snapPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
gameHandler_.startAutoAttack(chargeTargetGuid_);
|
||||
renderer_.triggerMeleeSwing();
|
||||
}
|
||||
|
||||
// Send movement heartbeat so server knows our new position
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
|
||||
return true; // charge is active
|
||||
}
|
||||
|
||||
void AnimationCallbackHandler::setupCallbacks() {
|
||||
// Sprint aura callback — use SPRINT(143) animation when sprint-type buff is active
|
||||
gameHandler_.setSprintAuraCallback([this](bool active) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (ac) ac->setSprintAuraActive(active);
|
||||
});
|
||||
|
||||
// Vehicle state callback — hide player character when inside a vehicle
|
||||
gameHandler_.setVehicleStateCallback([this](bool entered, uint32_t /*vehicleId*/) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
uint32_t instId = renderer_.getCharacterInstanceId();
|
||||
if (!cr || instId == 0) return;
|
||||
cr->setInstanceVisible(instId, !entered);
|
||||
});
|
||||
|
||||
// Charge callback — warrior rushes toward target
|
||||
gameHandler_.setChargeCallback([this](uint64_t targetGuid, float tx, float ty, float tz) {
|
||||
if (!renderer_.getCameraController()) return;
|
||||
|
||||
// Get current player position in render coords
|
||||
glm::vec3 startRender = renderer_.getCharacterPosition();
|
||||
// Convert target from canonical to render
|
||||
glm::vec3 targetRender = core::coords::canonicalToRender(glm::vec3(tx, ty, tz));
|
||||
|
||||
// Compute direction and stop 2.0 units short (melee reach)
|
||||
glm::vec3 dir = targetRender - startRender;
|
||||
float distSq = glm::dot(dir, dir);
|
||||
if (distSq < 9.0f) return; // Too close, nothing to do
|
||||
float invDist = glm::inversesqrt(distSq);
|
||||
glm::vec3 dirNorm = dir * invDist;
|
||||
glm::vec3 endRender = targetRender - dirNorm * 2.0f;
|
||||
|
||||
// Face toward target BEFORE starting charge
|
||||
float yawRad = std::atan2(dirNorm.x, dirNorm.y);
|
||||
float yawDeg = glm::degrees(yawRad);
|
||||
renderer_.setCharacterYaw(yawDeg);
|
||||
// Sync canonical orientation to server so it knows we turned
|
||||
float canonicalYaw = core::coords::normalizeAngleRad(glm::radians(180.0f - yawDeg));
|
||||
gameHandler_.setOrientation(canonicalYaw);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_SET_FACING);
|
||||
|
||||
// Set charge state
|
||||
chargeActive_ = true;
|
||||
chargeTimer_ = 0.0f;
|
||||
chargeDuration_ = std::max(std::sqrt(distSq) / 25.0f, 0.3f); // ~25 units/sec
|
||||
chargeStartPos_ = startRender;
|
||||
chargeEndPos_ = endRender;
|
||||
chargeTargetGuid_ = targetGuid;
|
||||
|
||||
// Disable player input, play charge animation
|
||||
renderer_.getCameraController()->setExternalFollow(true);
|
||||
renderer_.getCameraController()->clearMovementInputs();
|
||||
renderer_.setCharging(true);
|
||||
|
||||
// Start charge visual effect (red haze + dust)
|
||||
glm::vec3 chargeDir = glm::normalize(endRender - startRender);
|
||||
renderer_.startChargeEffect(startRender, chargeDir);
|
||||
|
||||
// Play charge whoosh sound (try multiple paths)
|
||||
auto& audio = audio::AudioEngine::instance();
|
||||
if (!audio.playSound2D("Sound\\Spells\\Charge.wav", 0.8f)) {
|
||||
if (!audio.playSound2D("Sound\\Spells\\charge.wav", 0.8f)) {
|
||||
if (!audio.playSound2D("Sound\\Spells\\SpellCharge.wav", 0.8f)) {
|
||||
// Fallback: weapon whoosh
|
||||
audio.playSound2D("Sound\\Item\\Weapons\\WeaponSwings\\mWooshLarge1.wav", 0.9f);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// NPC/player death callback (online mode) - play death animation
|
||||
gameHandler_.setNpcDeathCallback([this](uint64_t guid) {
|
||||
entitySpawner_.markCreatureDead(guid);
|
||||
if (!renderer_.getCharacterRenderer()) return;
|
||||
uint32_t instanceId = entitySpawner_.getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_.getPlayerInstanceId(guid);
|
||||
if (instanceId != 0) {
|
||||
renderer_.getCharacterRenderer()->playAnimation(instanceId, rendering::anim::DEATH, false);
|
||||
}
|
||||
});
|
||||
|
||||
// NPC/player respawn callback (online mode) - play rise animation then idle
|
||||
gameHandler_.setNpcRespawnCallback([this](uint64_t guid) {
|
||||
entitySpawner_.unmarkCreatureDead(guid);
|
||||
if (!renderer_.getCharacterRenderer()) return;
|
||||
uint32_t instanceId = entitySpawner_.getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_.getPlayerInstanceId(guid);
|
||||
if (instanceId != 0) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
// Play RISE one-shot (auto-returns to STAND when finished), fall back to STAND
|
||||
if (cr->hasAnimation(instanceId, rendering::anim::RISE))
|
||||
cr->playAnimation(instanceId, rendering::anim::RISE, false);
|
||||
else
|
||||
cr->playAnimation(instanceId, rendering::anim::STAND, true);
|
||||
}
|
||||
});
|
||||
|
||||
// NPC/player swing callback (online mode) - play attack animation
|
||||
// Probes the model for the best available attack animation:
|
||||
// ATTACK_1H(17) → ATTACK_2H(18) → ATTACK_2H_LOOSE(19) → ATTACK_UNARMED(16)
|
||||
gameHandler_.setNpcSwingCallback([this](uint64_t guid) {
|
||||
if (!renderer_.getCharacterRenderer()) return;
|
||||
uint32_t instanceId = entitySpawner_.getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_.getPlayerInstanceId(guid);
|
||||
if (instanceId != 0) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
static const uint32_t attackAnims[] = {
|
||||
rendering::anim::ATTACK_1H,
|
||||
rendering::anim::ATTACK_2H,
|
||||
rendering::anim::ATTACK_2H_LOOSE,
|
||||
rendering::anim::ATTACK_UNARMED
|
||||
};
|
||||
bool played = false;
|
||||
for (uint32_t anim : attackAnims) {
|
||||
if (cr->hasAnimation(instanceId, anim)) {
|
||||
cr->playAnimation(instanceId, anim, false);
|
||||
played = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!played) cr->playAnimation(instanceId, rendering::anim::ATTACK_UNARMED, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Hit reaction callback — plays one-shot dodge/block/wound animation on the victim
|
||||
gameHandler_.setHitReactionCallback([this](uint64_t victimGuid, game::GameHandler::HitReaction reaction) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
|
||||
// Determine animation based on reaction type
|
||||
uint32_t animId = rendering::anim::COMBAT_WOUND;
|
||||
switch (reaction) {
|
||||
case game::GameHandler::HitReaction::DODGE: animId = rendering::anim::DODGE; break;
|
||||
case game::GameHandler::HitReaction::PARRY: break; // Parry already handled by existing system
|
||||
case game::GameHandler::HitReaction::BLOCK: animId = rendering::anim::BLOCK; break;
|
||||
case game::GameHandler::HitReaction::SHIELD_BLOCK: animId = rendering::anim::SHIELD_BLOCK; break;
|
||||
case game::GameHandler::HitReaction::CRIT_WOUND: animId = rendering::anim::COMBAT_CRITICAL; break;
|
||||
case game::GameHandler::HitReaction::WOUND: animId = rendering::anim::COMBAT_WOUND; break;
|
||||
}
|
||||
|
||||
// For local player: use AnimationController state
|
||||
bool isLocalPlayer = (victimGuid == gameHandler_.getPlayerGuid());
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (ac) {
|
||||
uint32_t charInstId = renderer_.getCharacterInstanceId();
|
||||
if (charInstId && cr->hasAnimation(charInstId, animId))
|
||||
ac->triggerHitReaction(animId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For NPCs/other players: direct playAnimation
|
||||
uint32_t instanceId = entitySpawner_.getCreatureInstanceId(victimGuid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_.getPlayerInstanceId(victimGuid);
|
||||
if (instanceId != 0 && cr->hasAnimation(instanceId, animId))
|
||||
cr->playAnimation(instanceId, animId, false);
|
||||
});
|
||||
|
||||
// Stun state callback — enters/exits STUNNED animation on local player
|
||||
gameHandler_.setStunStateCallback([this](bool stunned) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (ac) ac->setStunned(stunned);
|
||||
});
|
||||
|
||||
// Stealth state callback — switches to stealth animation variants
|
||||
gameHandler_.setStealthStateCallback([this](bool stealthed) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (ac) ac->setStealthed(stealthed);
|
||||
});
|
||||
|
||||
// Player health callback — switches to wounded idle when HP < 20%
|
||||
gameHandler_.setPlayerHealthCallback([this](uint32_t health, uint32_t maxHealth) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (!ac) return;
|
||||
bool lowHp = (maxHealth > 0) && (health > 0) && (health * 5 <= maxHealth);
|
||||
ac->setLowHealth(lowHp);
|
||||
});
|
||||
|
||||
// Unit animation hint callback — plays jump (38=JumpMid) animation on other players/NPCs.
|
||||
// Swim/walking state is now authoritative from the move-flags callback below.
|
||||
// animId=38 (JumpMid): airborne jump animation; land detection is via per-frame sync.
|
||||
gameHandler_.setUnitAnimHintCallback([this](uint64_t guid, uint32_t animId) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
uint32_t instanceId = entitySpawner_.getPlayerInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_.getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) return;
|
||||
// Don't override Death animation
|
||||
uint32_t curAnim = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
if (cr->getAnimationState(instanceId, curAnim, curT, curDur) && curAnim == rendering::anim::DEATH) return;
|
||||
cr->playAnimation(instanceId, animId, /*loop=*/true);
|
||||
});
|
||||
|
||||
// Unit move-flags callback — updates swimming and walking state from every MSG_MOVE_* packet.
|
||||
// This is more reliable than opcode-based hints for cold joins and heartbeats:
|
||||
// a player already swimming when we join will have SWIMMING set on the first heartbeat.
|
||||
// Walking(4) vs Running(5) is also driven here from the WALKING flag.
|
||||
gameHandler_.setUnitMoveFlagsCallback([this](uint64_t guid, uint32_t moveFlags) {
|
||||
const bool isSwimming = (moveFlags & static_cast<uint32_t>(game::MovementFlags::SWIMMING)) != 0;
|
||||
const bool isWalking = (moveFlags & static_cast<uint32_t>(game::MovementFlags::WALKING)) != 0;
|
||||
const bool isFlying = (moveFlags & static_cast<uint32_t>(game::MovementFlags::FLYING)) != 0;
|
||||
auto& swimState = entitySpawner_.getCreatureSwimmingState();
|
||||
auto& walkState = entitySpawner_.getCreatureWalkingState();
|
||||
auto& flyState = entitySpawner_.getCreatureFlyingState();
|
||||
if (isSwimming) swimState[guid] = true;
|
||||
else swimState.erase(guid);
|
||||
if (isWalking) walkState[guid] = true;
|
||||
else walkState.erase(guid);
|
||||
if (isFlying) flyState[guid] = true;
|
||||
else flyState.erase(guid);
|
||||
});
|
||||
|
||||
// Emote animation callback — play server-driven emote animations on NPCs and other players.
|
||||
// When emoteAnim is 0, the NPC's emote state was cleared → revert to STAND.
|
||||
// Non-zero values from UNIT_NPC_EMOTESTATE updates are persistent (played looping).
|
||||
gameHandler_.setEmoteAnimCallback([this](uint64_t guid, uint32_t emoteAnim) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
// Look up creature instance first, then online players
|
||||
uint32_t emoteInstanceId = entitySpawner_.getCreatureInstanceId(guid);
|
||||
if (emoteInstanceId != 0) {
|
||||
if (emoteAnim == 0) {
|
||||
// Emote state cleared → return to idle
|
||||
cr->playAnimation(emoteInstanceId, rendering::anim::STAND, true);
|
||||
} else {
|
||||
cr->playAnimation(emoteInstanceId, emoteAnim, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emoteInstanceId = entitySpawner_.getPlayerInstanceId(guid);
|
||||
if (emoteInstanceId != 0) {
|
||||
cr->playAnimation(emoteInstanceId, emoteAnim, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Spell cast animation callback — play cast animation on caster (player or NPC/other player)
|
||||
// WoW-accurate 3-phase spell animation sequence:
|
||||
// Phase 1: SPELL_PRECAST (31) — one-shot wind-up
|
||||
// Phase 2: READY_SPELL_DIRECTED/OMNI (51/52) — looping hold while cast bar fills
|
||||
// Phase 3: SPELL_CAST_DIRECTED/OMNI/AREA (53/54/33) — one-shot release at completion
|
||||
// Channels use CHANNEL_CAST_DIRECTED/OMNI (124/125) or SPELL_CHANNEL_DIRECTED_OMNI (201).
|
||||
// castType comes from the spell packet's targetGuid:
|
||||
// DIRECTED — spell targets a specific unit (Frostbolt, Heal)
|
||||
// OMNI — self-cast / no explicit target (Arcane Explosion, buffs)
|
||||
// AREA — ground-targeted AoE (Blizzard, Rain of Fire)
|
||||
gameHandler_.setSpellCastAnimCallback([this](uint64_t guid, bool start, bool isChannel,
|
||||
game::SpellCastType castType) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
|
||||
// Determine if this is the local player
|
||||
bool isLocalPlayer = false;
|
||||
uint32_t instanceId = 0;
|
||||
{
|
||||
uint32_t charInstId = renderer_.getCharacterInstanceId();
|
||||
if (charInstId != 0 && guid == gameHandler_.getPlayerGuid()) {
|
||||
instanceId = charInstId;
|
||||
isLocalPlayer = true;
|
||||
}
|
||||
}
|
||||
if (instanceId == 0) instanceId = entitySpawner_.getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_.getPlayerInstanceId(guid);
|
||||
if (instanceId == 0) return;
|
||||
|
||||
const bool isDirected = (castType == game::SpellCastType::DIRECTED);
|
||||
const bool isArea = (castType == game::SpellCastType::AREA);
|
||||
|
||||
if (start) {
|
||||
// Detect fishing spells (channeled) — use FISHING_LOOP instead of generic cast
|
||||
auto isFishingSpell = [](uint32_t spellId) {
|
||||
return spellId == 7620 || spellId == 7731 || spellId == 7732 ||
|
||||
spellId == 18248 || spellId == 33095 || spellId == 51294;
|
||||
};
|
||||
uint32_t currentSpell = isLocalPlayer ? gameHandler_.getCurrentCastSpellId() : 0;
|
||||
bool isFishing = isChannel && isFishingSpell(currentSpell);
|
||||
|
||||
if (isFishing && cr->hasAnimation(instanceId, rendering::anim::FISHING_LOOP)) {
|
||||
// Fishing: use FISHING_LOOP (looping idle) for the channel duration
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (ac) ac->startSpellCast(0, rendering::anim::FISHING_LOOP, true, 0);
|
||||
} else {
|
||||
cr->playAnimation(instanceId, rendering::anim::FISHING_LOOP, true);
|
||||
}
|
||||
} else {
|
||||
// Helper: pick first animation the model supports from a list
|
||||
auto pickFirst = [&](std::initializer_list<uint32_t> ids) -> uint32_t {
|
||||
for (uint32_t id : ids)
|
||||
if (cr->hasAnimation(instanceId, id)) return id;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Phase 1: Precast wind-up (one-shot, non-channels only)
|
||||
uint32_t precastAnim = 0;
|
||||
if (!isChannel) {
|
||||
precastAnim = pickFirst({rendering::anim::SPELL_PRECAST});
|
||||
}
|
||||
|
||||
// Phase 2: Cast hold (looping while cast bar fills / channel active)
|
||||
uint32_t castAnim = 0;
|
||||
if (isChannel) {
|
||||
// Channel hold: prefer DIRECTED/OMNI based on spell target classification
|
||||
if (isDirected) {
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CHANNEL_DIRECTED_OMNI,
|
||||
rendering::anim::READY_SPELL_DIRECTED,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else {
|
||||
// OMNI or AREA channels (Blizzard channel, Tranquility, etc.)
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CHANNEL_DIRECTED_OMNI,
|
||||
rendering::anim::READY_SPELL_OMNI,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Regular cast hold: READY_SPELL_DIRECTED/OMNI while cast bar fills
|
||||
if (isDirected) {
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::READY_SPELL_DIRECTED,
|
||||
rendering::anim::READY_SPELL_OMNI,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else {
|
||||
// OMNI (self-buff) or AREA (AoE targeting)
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::READY_SPELL_OMNI,
|
||||
rendering::anim::READY_SPELL_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
}
|
||||
}
|
||||
if (castAnim == 0) castAnim = rendering::anim::SPELL;
|
||||
|
||||
// Phase 3: Finalization release (one-shot after cast completes)
|
||||
// Animation chosen by spell target type: AREA → SPELL_CAST_AREA,
|
||||
// DIRECTED → SPELL_CAST_DIRECTED, OMNI → SPELL_CAST_OMNI
|
||||
uint32_t finalizeAnim = 0;
|
||||
if (isLocalPlayer && !isChannel) {
|
||||
if (isArea) {
|
||||
// Ground-targeted AoE: SPELL_CAST_AREA → SPELL_CAST_OMNI
|
||||
finalizeAnim = pickFirst({
|
||||
rendering::anim::SPELL_CAST_AREA,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else if (isDirected) {
|
||||
// Single-target: SPELL_CAST_DIRECTED → SPELL_CAST_OMNI
|
||||
finalizeAnim = pickFirst({
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else {
|
||||
// OMNI (self-buff, Arcane Explosion): SPELL_CAST_OMNI → SPELL_CAST_AREA
|
||||
finalizeAnim = pickFirst({
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST_AREA,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (ac) ac->startSpellCast(precastAnim, castAnim, true, finalizeAnim);
|
||||
} else {
|
||||
cr->playAnimation(instanceId, castAnim, true);
|
||||
}
|
||||
} // end !isFishing
|
||||
} else {
|
||||
// Cast/channel ended — plays finalization anim completely then returns to idle
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (ac) ac->stopSpellCast();
|
||||
} else if (isChannel) {
|
||||
cr->playAnimation(instanceId, rendering::anim::STAND, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ghost state callback — make player semi-transparent when in spirit form
|
||||
gameHandler_.setGhostStateCallback([this](bool isGhost) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
uint32_t charInstId = renderer_.getCharacterInstanceId();
|
||||
if (charInstId == 0) return;
|
||||
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
|
||||
});
|
||||
|
||||
// Stand state animation callback — route through AnimationController state machine
|
||||
// for proper sit/sleep/kneel transition animations (down → loop → up)
|
||||
gameHandler_.setStandStateCallback([this](uint8_t standState) {
|
||||
using AC = rendering::AnimationController;
|
||||
|
||||
// Sync camera controller sitting flag: block movement while sitting/kneeling
|
||||
if (auto* cc = renderer_.getCameraController()) {
|
||||
cc->setSitting(standState >= AC::STAND_STATE_SIT &&
|
||||
standState <= AC::STAND_STATE_KNEEL &&
|
||||
standState != AC::STAND_STATE_DEAD);
|
||||
}
|
||||
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (!ac) return;
|
||||
|
||||
// Death is special — play directly, not through sit state machine
|
||||
if (standState == AC::STAND_STATE_DEAD) {
|
||||
auto* cr = renderer_.getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
uint32_t charInstId = renderer_.getCharacterInstanceId();
|
||||
if (charInstId == 0) return;
|
||||
cr->playAnimation(charInstId, rendering::anim::DEATH, false);
|
||||
return;
|
||||
}
|
||||
|
||||
ac->setStandState(standState);
|
||||
});
|
||||
|
||||
// Loot window callback — play kneel/loot animation while looting
|
||||
gameHandler_.setLootWindowCallback([this](bool open) {
|
||||
auto* ac = renderer_.getAnimationController();
|
||||
if (!ac) return;
|
||||
if (open) ac->startLooting();
|
||||
else ac->stopLooting();
|
||||
});
|
||||
}
|
||||
|
||||
}} // namespace wowee::core
|
||||
File diff suppressed because it is too large
Load diff
132
src/core/audio_callback_handler.cpp
Normal file
132
src/core/audio_callback_handler.cpp
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#include "core/audio_callback_handler.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
#include "audio/music_manager.hpp"
|
||||
#include "audio/audio_engine.hpp"
|
||||
#include "ui/ui_manager.hpp"
|
||||
|
||||
namespace wowee { namespace core {
|
||||
|
||||
AudioCallbackHandler::AudioCallbackHandler(
|
||||
pipeline::AssetManager& assetManager,
|
||||
audio::AudioCoordinator* audioCoordinator,
|
||||
rendering::Renderer* renderer,
|
||||
ui::UIManager* uiManager,
|
||||
game::GameHandler& gameHandler)
|
||||
: assetManager_(assetManager)
|
||||
, audioCoordinator_(audioCoordinator)
|
||||
, renderer_(renderer)
|
||||
, uiManager_(uiManager)
|
||||
, gameHandler_(gameHandler)
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<std::string> AudioCallbackHandler::resolveSoundEntryPath(uint32_t soundId) const {
|
||||
auto dbc = assetManager_.loadDBC("SoundEntries.dbc");
|
||||
if (!dbc || !dbc->isLoaded()) return std::nullopt;
|
||||
|
||||
int32_t idx = dbc->findRecordById(soundId);
|
||||
if (idx < 0) return std::nullopt;
|
||||
|
||||
// SoundEntries.dbc (WotLK): field 2 = Name (label), fields 3-12 = File[0..9], field 23 = DirectoryBase
|
||||
const uint32_t row = static_cast<uint32_t>(idx);
|
||||
std::string dir = dbc->getString(row, 23);
|
||||
for (uint32_t f = 3; f <= 12; ++f) {
|
||||
std::string name = dbc->getString(row, f);
|
||||
if (name.empty()) continue;
|
||||
return dir.empty() ? name : dir + "\\" + name;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void AudioCallbackHandler::setupCallbacks() {
|
||||
// Level-up callback — play sound, cheer emote, and trigger UI ding overlay + 3D effect
|
||||
gameHandler_.setLevelUpCallback([this](uint32_t newLevel) {
|
||||
if (uiManager_) {
|
||||
uiManager_->getGameScreen().toastManager().triggerDing(newLevel);
|
||||
}
|
||||
if (renderer_) {
|
||||
renderer_->triggerLevelUpEffect(renderer_->getCharacterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
// Achievement earned callback — show toast banner
|
||||
gameHandler_.setAchievementEarnedCallback([this](uint32_t achievementId, const std::string& name) {
|
||||
if (uiManager_) {
|
||||
uiManager_->getGameScreen().toastManager().triggerAchievementToast(achievementId, name);
|
||||
}
|
||||
});
|
||||
|
||||
// Server-triggered music callback (SMSG_PLAY_MUSIC)
|
||||
// Resolves soundId → SoundEntries.dbc → MPQ path → MusicManager.
|
||||
gameHandler_.setPlayMusicCallback([this](uint32_t soundId) {
|
||||
if (!renderer_) return;
|
||||
auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr;
|
||||
if (!music) return;
|
||||
|
||||
auto path = resolveSoundEntryPath(soundId);
|
||||
if (path) {
|
||||
music->playMusic(*path, /*loop=*/false);
|
||||
}
|
||||
});
|
||||
|
||||
// SMSG_PLAY_SOUND: look up SoundEntries.dbc and play 2-D sound effect
|
||||
gameHandler_.setPlaySoundCallback([this](uint32_t soundId) {
|
||||
auto path = resolveSoundEntryPath(soundId);
|
||||
if (path) {
|
||||
audio::AudioEngine::instance().playSound2D(*path);
|
||||
}
|
||||
});
|
||||
|
||||
// SMSG_PLAY_OBJECT_SOUND / SMSG_PLAY_SPELL_IMPACT: play as 3D positional sound at source entity
|
||||
gameHandler_.setPlayPositionalSoundCallback([this](uint32_t soundId, uint64_t sourceGuid) {
|
||||
auto path = resolveSoundEntryPath(soundId);
|
||||
if (!path) return;
|
||||
|
||||
// Play as 3D sound if source entity position is available.
|
||||
// Entity stores canonical coords; listener uses render coords (camera).
|
||||
auto entity = gameHandler_.getEntityManager().getEntity(sourceGuid);
|
||||
if (entity) {
|
||||
glm::vec3 canonical{entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()};
|
||||
glm::vec3 pos = core::coords::canonicalToRender(canonical);
|
||||
audio::AudioEngine::instance().playSound3D(*path, pos);
|
||||
} else {
|
||||
audio::AudioEngine::instance().playSound2D(*path);
|
||||
}
|
||||
});
|
||||
|
||||
// Other player level-up callback — trigger 3D effect + chat notification
|
||||
gameHandler_.setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) {
|
||||
if (!renderer_) return;
|
||||
|
||||
// Trigger 3D effect at the other player's position
|
||||
auto entity = gameHandler_.getEntityManager().getEntity(guid);
|
||||
if (entity) {
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
renderer_->triggerLevelUpEffect(renderPos);
|
||||
}
|
||||
|
||||
// Show chat message if in group
|
||||
if (gameHandler_.isInGroup()) {
|
||||
std::string name = gameHandler_.getCachedPlayerName(guid);
|
||||
if (name.empty()) name = "A party member";
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = name + " has reached level " + std::to_string(newLevel) + "!";
|
||||
gameHandler_.addLocalChatMessage(msg);
|
||||
}
|
||||
});
|
||||
|
||||
// Open dungeon finder callback — server sends SMSG_OPEN_LFG_DUNGEON_FINDER
|
||||
gameHandler_.setOpenLfgCallback([this]() {
|
||||
if (uiManager_) uiManager_->getGameScreen().openDungeonFinder();
|
||||
});
|
||||
}
|
||||
|
||||
}} // namespace wowee::core
|
||||
191
src/core/entity_spawn_callback_handler.cpp
Normal file
191
src/core/entity_spawn_callback_handler.cpp
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
#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) {
|
||||
LOG_WARNING("playerSpawnCallback: guid=0x", std::hex, guid, std::dec,
|
||||
" 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
|
||||
84
src/core/npc_interaction_callback_handler.cpp
Normal file
84
src/core/npc_interaction_callback_handler.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#include "core/npc_interaction_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/animation/animation_ids.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
#include "audio/npc_voice_manager.hpp"
|
||||
|
||||
namespace wowee { namespace core {
|
||||
|
||||
NPCInteractionCallbackHandler::NPCInteractionCallbackHandler(
|
||||
EntitySpawner& entitySpawner,
|
||||
rendering::Renderer* renderer,
|
||||
game::GameHandler& gameHandler,
|
||||
audio::AudioCoordinator* audioCoordinator)
|
||||
: entitySpawner_(entitySpawner)
|
||||
, renderer_(renderer)
|
||||
, gameHandler_(gameHandler)
|
||||
, audioCoordinator_(audioCoordinator)
|
||||
{
|
||||
}
|
||||
|
||||
audio::VoiceType NPCInteractionCallbackHandler::resolveNpcVoiceType(uint64_t guid) const {
|
||||
audio::VoiceType voiceType = audio::VoiceType::GENERIC;
|
||||
auto entity = gameHandler_.getEntityManager().getEntity(guid);
|
||||
if (entity && entity->getType() == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||
uint32_t displayId = unit->getDisplayId();
|
||||
voiceType = entitySpawner_.detectVoiceTypeFromDisplayId(displayId);
|
||||
}
|
||||
return voiceType;
|
||||
}
|
||||
|
||||
void NPCInteractionCallbackHandler::setupCallbacks() {
|
||||
// NPC greeting callback - play voice line
|
||||
gameHandler_.setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||
// Play NPC_WELCOME animation on the NPC
|
||||
if (renderer_) {
|
||||
auto* cr = renderer_->getCharacterRenderer();
|
||||
if (cr) {
|
||||
uint32_t instanceId = entitySpawner_.getCreatureInstanceId(guid);
|
||||
if (instanceId != 0) cr->playAnimation(instanceId, rendering::anim::NPC_WELCOME, false);
|
||||
}
|
||||
}
|
||||
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
|
||||
// Convert canonical to render coords for 3D audio
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||
audio::VoiceType voiceType = resolveNpcVoiceType(guid);
|
||||
audioCoordinator_->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos);
|
||||
}
|
||||
});
|
||||
|
||||
// NPC farewell callback - play farewell voice line
|
||||
gameHandler_.setNpcFarewellCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||
audio::VoiceType voiceType = resolveNpcVoiceType(guid);
|
||||
audioCoordinator_->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos);
|
||||
}
|
||||
});
|
||||
|
||||
// NPC vendor callback - play vendor voice line
|
||||
gameHandler_.setNpcVendorCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||
audio::VoiceType voiceType = resolveNpcVoiceType(guid);
|
||||
audioCoordinator_->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos);
|
||||
}
|
||||
});
|
||||
|
||||
// NPC aggro callback - play combat start voice line
|
||||
gameHandler_.setNpcAggroCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||
audio::VoiceType voiceType = resolveNpcVoiceType(guid);
|
||||
audioCoordinator_->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}} // namespace wowee::core
|
||||
275
src/core/transport_callback_handler.cpp
Normal file
275
src/core/transport_callback_handler.cpp
Normal 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
|
||||
182
src/core/ui_screen_callback_handler.cpp
Normal file
182
src/core/ui_screen_callback_handler.cpp
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
#include "core/ui_screen_callback_handler.hpp"
|
||||
#include "core/application.hpp" // AppState
|
||||
#include "core/logger.hpp"
|
||||
#include "ui/ui_manager.hpp"
|
||||
#include "auth/auth_handler.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
#include "game/world_packets.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
|
||||
namespace wowee { namespace core {
|
||||
|
||||
UIScreenCallbackHandler::UIScreenCallbackHandler(
|
||||
ui::UIManager& uiManager,
|
||||
game::GameHandler& gameHandler,
|
||||
auth::AuthHandler& authHandler,
|
||||
game::ExpansionRegistry* expansionRegistry,
|
||||
pipeline::AssetManager* assetManager,
|
||||
SetStateFn setState)
|
||||
: uiManager_(uiManager)
|
||||
, gameHandler_(gameHandler)
|
||||
, authHandler_(authHandler)
|
||||
, expansionRegistry_(expansionRegistry)
|
||||
, assetManager_(assetManager)
|
||||
, setState_(std::move(setState))
|
||||
{
|
||||
}
|
||||
|
||||
void UIScreenCallbackHandler::setupCallbacks() {
|
||||
// Authentication screen callback
|
||||
uiManager_.getAuthScreen().setOnSuccess([this]() {
|
||||
LOG_INFO("Authentication successful, transitioning to realm selection");
|
||||
setState_(AppState::REALM_SELECTION);
|
||||
});
|
||||
|
||||
// Realm selection callback
|
||||
uiManager_.getRealmScreen().setOnRealmSelected([this](const std::string& realmName, const std::string& realmAddress) {
|
||||
LOG_INFO("Realm selected: ", realmName, " (", realmAddress, ")");
|
||||
|
||||
// Parse realm address (format: "hostname:port")
|
||||
std::string host = realmAddress;
|
||||
uint16_t port = 8085; // Default world server port
|
||||
|
||||
size_t colonPos = realmAddress.find(':');
|
||||
if (colonPos != std::string::npos) {
|
||||
host = realmAddress.substr(0, colonPos);
|
||||
try { port = static_cast<uint16_t>(std::stoi(realmAddress.substr(colonPos + 1))); }
|
||||
catch (...) { LOG_WARNING("Invalid port in realm address: ", realmAddress); }
|
||||
}
|
||||
|
||||
// Connect to world server
|
||||
const auto& sessionKey = authHandler_.getSessionKey();
|
||||
std::string accountName = authHandler_.getUsername();
|
||||
if (accountName.empty()) {
|
||||
LOG_WARNING("Auth username missing; falling back to TESTACCOUNT");
|
||||
accountName = "TESTACCOUNT";
|
||||
}
|
||||
|
||||
uint32_t realmId = 0;
|
||||
uint16_t realmBuild = 0;
|
||||
{
|
||||
// WotLK AUTH_SESSION includes a RealmID field; some servers reject if it's wrong/zero.
|
||||
const auto& realms = authHandler_.getRealms();
|
||||
for (const auto& r : realms) {
|
||||
if (r.name == realmName && r.address == realmAddress) {
|
||||
realmId = r.id;
|
||||
realmBuild = r.build;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Selected realmId=", realmId, " realmBuild=", realmBuild);
|
||||
}
|
||||
|
||||
uint32_t clientBuild = 12340; // default WotLK
|
||||
if (expansionRegistry_) {
|
||||
auto* profile = expansionRegistry_->getActive();
|
||||
if (profile) clientBuild = profile->worldBuild;
|
||||
}
|
||||
// Prefer realm-reported build when available (e.g. vanilla servers
|
||||
// that report build 5875 in the realm list)
|
||||
if (realmBuild != 0) {
|
||||
clientBuild = realmBuild;
|
||||
LOG_INFO("Using realm-reported build: ", clientBuild);
|
||||
}
|
||||
if (gameHandler_.connect(host, port, sessionKey, accountName, clientBuild, realmId)) {
|
||||
LOG_INFO("Connected to world server, transitioning to character selection");
|
||||
setState_(AppState::CHARACTER_SELECTION);
|
||||
} else {
|
||||
LOG_ERROR("Failed to connect to world server");
|
||||
}
|
||||
});
|
||||
|
||||
// Realm screen back button - return to login
|
||||
uiManager_.getRealmScreen().setOnBack([this]() {
|
||||
authHandler_.disconnect();
|
||||
uiManager_.getRealmScreen().reset();
|
||||
setState_(AppState::AUTHENTICATION);
|
||||
});
|
||||
|
||||
// Character selection callback
|
||||
uiManager_.getCharacterScreen().setOnCharacterSelected([this](uint64_t characterGuid) {
|
||||
LOG_INFO("Character selected: GUID=0x", std::hex, characterGuid, std::dec);
|
||||
// Always set the active character GUID
|
||||
gameHandler_.setActiveCharacterGuid(characterGuid);
|
||||
// Keep CHARACTER_SELECTION active until world entry is fully loaded.
|
||||
// This avoids exposing pre-load hitching before the loading screen/intro.
|
||||
});
|
||||
|
||||
// Character create screen callbacks
|
||||
uiManager_.getCharacterCreateScreen().setOnCreate([this](const game::CharCreateData& data) {
|
||||
pendingCreatedCharacterName_ = data.name; // Store name for auto-selection
|
||||
gameHandler_.createCharacter(data);
|
||||
});
|
||||
|
||||
uiManager_.getCharacterCreateScreen().setOnCancel([this]() {
|
||||
setState_(AppState::CHARACTER_SELECTION);
|
||||
});
|
||||
|
||||
// Character create result callback
|
||||
gameHandler_.setCharCreateCallback([this](bool success, const std::string& msg) {
|
||||
if (success) {
|
||||
// Auto-select the newly created character
|
||||
if (!pendingCreatedCharacterName_.empty()) {
|
||||
uiManager_.getCharacterScreen().selectCharacterByName(pendingCreatedCharacterName_);
|
||||
pendingCreatedCharacterName_.clear();
|
||||
}
|
||||
setState_(AppState::CHARACTER_SELECTION);
|
||||
} else {
|
||||
uiManager_.getCharacterCreateScreen().setStatus(msg, true);
|
||||
pendingCreatedCharacterName_.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// Character login failure callback
|
||||
gameHandler_.setCharLoginFailCallback([this](const std::string& reason) {
|
||||
LOG_WARNING("Character login failed: ", reason);
|
||||
setState_(AppState::CHARACTER_SELECTION);
|
||||
uiManager_.getCharacterScreen().setStatus("Login failed: " + reason, true);
|
||||
});
|
||||
|
||||
// "Create Character" button on character screen
|
||||
uiManager_.getCharacterScreen().setOnCreateCharacter([this]() {
|
||||
uiManager_.getCharacterCreateScreen().reset();
|
||||
// Apply expansion race/class constraints before showing the screen
|
||||
if (expansionRegistry_ && expansionRegistry_->getActive()) {
|
||||
auto* profile = expansionRegistry_->getActive();
|
||||
uiManager_.getCharacterCreateScreen().setExpansionConstraints(
|
||||
profile->races, profile->classes);
|
||||
}
|
||||
uiManager_.getCharacterCreateScreen().initializePreview(assetManager_);
|
||||
setState_(AppState::CHARACTER_CREATION);
|
||||
});
|
||||
|
||||
// "Back" button on character screen
|
||||
uiManager_.getCharacterScreen().setOnBack([this]() {
|
||||
// Disconnect from world server and reset UI state for fresh realm selection
|
||||
gameHandler_.disconnect();
|
||||
uiManager_.getRealmScreen().reset();
|
||||
uiManager_.getCharacterScreen().reset();
|
||||
setState_(AppState::REALM_SELECTION);
|
||||
});
|
||||
|
||||
// "Delete Character" button on character screen
|
||||
uiManager_.getCharacterScreen().setOnDeleteCharacter([this](uint64_t guid) {
|
||||
gameHandler_.deleteCharacter(guid);
|
||||
});
|
||||
|
||||
// Character delete result callback
|
||||
gameHandler_.setCharDeleteCallback([this](bool success) {
|
||||
if (success) {
|
||||
uiManager_.getCharacterScreen().setStatus("Character deleted.");
|
||||
// Refresh character list
|
||||
gameHandler_.requestCharacterList();
|
||||
} else {
|
||||
uint8_t code = gameHandler_.getLastCharDeleteResult();
|
||||
uiManager_.getCharacterScreen().setStatus(
|
||||
"Delete failed (code " + std::to_string(static_cast<int>(code)) + ").", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}} // namespace wowee::core
|
||||
456
src/core/world_entry_callback_handler.cpp
Normal file
456
src/core/world_entry_callback_handler.cpp
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
#include "core/world_entry_callback_handler.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/entity_spawner.hpp"
|
||||
#include "core/world_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
namespace wowee { namespace core {
|
||||
|
||||
WorldEntryCallbackHandler::WorldEntryCallbackHandler(
|
||||
rendering::Renderer& renderer,
|
||||
game::GameHandler& gameHandler,
|
||||
WorldLoader* worldLoader,
|
||||
EntitySpawner* entitySpawner,
|
||||
audio::AudioCoordinator* audioCoordinator,
|
||||
pipeline::AssetManager* assetManager)
|
||||
: renderer_(renderer)
|
||||
, gameHandler_(gameHandler)
|
||||
, worldLoader_(worldLoader)
|
||||
, entitySpawner_(entitySpawner)
|
||||
, audioCoordinator_(audioCoordinator)
|
||||
, assetManager_(assetManager)
|
||||
{
|
||||
}
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// Sample best floor height at (x, y) from terrain, WMO, and M2 (eliminates 3x duplication)
|
||||
std::optional<float> WorldEntryCallbackHandler::sampleBestFloorAt(float x, float y, float probeZ) const {
|
||||
std::optional<float> terrainFloor;
|
||||
std::optional<float> wmoFloor;
|
||||
std::optional<float> m2Floor;
|
||||
|
||||
if (renderer_.getTerrainManager()) {
|
||||
terrainFloor = renderer_.getTerrainManager()->getHeightAt(x, y);
|
||||
}
|
||||
if (renderer_.getWMORenderer()) {
|
||||
wmoFloor = renderer_.getWMORenderer()->getFloorHeight(x, y, probeZ);
|
||||
}
|
||||
if (renderer_.getM2Renderer()) {
|
||||
m2Floor = renderer_.getM2Renderer()->getFloorHeight(x, y, probeZ);
|
||||
}
|
||||
|
||||
std::optional<float> best;
|
||||
if (terrainFloor) best = terrainFloor;
|
||||
if (wmoFloor && (!best || *wmoFloor > *best)) best = wmoFloor;
|
||||
if (m2Floor && (!best || *m2Floor > *best)) best = m2Floor;
|
||||
return best;
|
||||
}
|
||||
|
||||
// Clear stuck movement state on player
|
||||
void WorldEntryCallbackHandler::clearStuckMovement() {
|
||||
if (renderer_.getCameraController()) {
|
||||
renderer_.getCameraController()->clearMovementInputs();
|
||||
}
|
||||
gameHandler_.forceClearTaxiAndMovementState();
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_STOP);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_STOP_STRAFE);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_STOP_TURN);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_STOP_SWIM);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
|
||||
// Sync teleported render position to server
|
||||
void WorldEntryCallbackHandler::syncTeleportedPositionToServer(const glm::vec3& renderPos) {
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
|
||||
gameHandler_.setPosition(canonical.x, canonical.y, canonical.z);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_STOP);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_STOP_STRAFE);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_STOP_TURN);
|
||||
gameHandler_.sendMovement(game::Opcode::MSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
|
||||
// Force server-side teleport via GM command
|
||||
void WorldEntryCallbackHandler::forceServerTeleportCommand(const glm::vec3& renderPos) {
|
||||
// Server-authoritative reset first, then teleport.
|
||||
gameHandler_.sendChatMessage(game::ChatType::SAY, ".revive", "");
|
||||
gameHandler_.sendChatMessage(game::ChatType::SAY, ".dismount", "");
|
||||
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
|
||||
glm::vec3 serverPos = core::coords::canonicalToServer(canonical);
|
||||
std::ostringstream cmd;
|
||||
cmd.setf(std::ios::fixed);
|
||||
cmd.precision(3);
|
||||
cmd << ".go xyz "
|
||||
<< serverPos.x << " "
|
||||
<< serverPos.y << " "
|
||||
<< serverPos.z << " "
|
||||
<< gameHandler_.getCurrentMapId() << " "
|
||||
<< gameHandler_.getMovementInfo().orientation;
|
||||
gameHandler_.sendChatMessage(game::ChatType::SAY, cmd.str(), "");
|
||||
}
|
||||
|
||||
// Precache tiles in a radius around a position (eliminates repeated tile-loop code)
|
||||
static void precacheNearbyTiles(rendering::TerrainManager* terrainMgr,
|
||||
const glm::vec3& renderPos, int radius) {
|
||||
if (!terrainMgr) return;
|
||||
auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y);
|
||||
int side = 2 * radius + 1;
|
||||
std::vector<std::pair<int,int>> tiles;
|
||||
tiles.reserve(side * side);
|
||||
for (int dy = -radius; dy <= radius; dy++)
|
||||
for (int dx = -radius; dx <= radius; dx++)
|
||||
tiles.push_back({tileX + dx, tileY + dy});
|
||||
terrainMgr->precacheTiles(tiles);
|
||||
}
|
||||
|
||||
// ── callbacks ────────────────────────────────────────────────────
|
||||
|
||||
void WorldEntryCallbackHandler::setupCallbacks() {
|
||||
// World entry callback (online mode) - load terrain when entering world
|
||||
gameHandler_.setWorldEntryCallback([this](uint32_t mapId, float x, float y, float z, bool isInitialEntry) {
|
||||
LOG_INFO("Online world entry: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")"
|
||||
" initial=", isInitialEntry);
|
||||
renderer_.resetCombatVisualState();
|
||||
|
||||
// Reconnect to the same map: terrain stays loaded but all online entities are stale.
|
||||
// Despawn them properly so the server's fresh CREATE_OBJECTs will re-populate the world.
|
||||
uint32_t currentLoadedMap = worldLoader_ ? worldLoader_->getLoadedMapId() : 0xFFFFFFFF;
|
||||
if (entitySpawner_ && mapId == currentLoadedMap && renderer_.getTerrainManager() && isInitialEntry) {
|
||||
LOG_INFO("Reconnect to same map ", mapId, ": clearing stale online entities (terrain preserved)");
|
||||
|
||||
// Pending spawn queues and failure caches — clear so previously-failed GUIDs can retry.
|
||||
// Dead creature guids will be re-populated from fresh server state.
|
||||
entitySpawner_->clearAllQueues();
|
||||
|
||||
// Properly despawn all tracked instances from the renderer
|
||||
entitySpawner_->despawnAllCreatures();
|
||||
entitySpawner_->despawnAllPlayers();
|
||||
entitySpawner_->despawnAllGameObjects();
|
||||
|
||||
// Update player position and re-queue nearby tiles (same logic as teleport)
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(x, y, z));
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
renderer_.getCharacterPosition() = renderPos;
|
||||
if (renderer_.getCameraController()) {
|
||||
auto* ft = renderer_.getCameraController()->getFollowTargetMutable();
|
||||
if (ft) *ft = renderPos;
|
||||
renderer_.getCameraController()->clearMovementInputs();
|
||||
renderer_.getCameraController()->suppressMovementFor(1.0f);
|
||||
renderer_.getCameraController()->suspendGravityFor(10.0f);
|
||||
}
|
||||
worldEntryMovementGraceTimer_ = 2.0f;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
renderer_.getTerrainManager()->processReadyTiles();
|
||||
precacheNearbyTiles(renderer_.getTerrainManager(), renderPos, 8);
|
||||
return;
|
||||
}
|
||||
|
||||
// Same-map teleport (taxi landing, GM teleport, hearthstone on same continent):
|
||||
if (mapId == currentLoadedMap && renderer_.getTerrainManager()) {
|
||||
// Check if teleport is far enough to need terrain loading (>500 render units)
|
||||
glm::vec3 oldPos = renderer_.getCharacterPosition();
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(x, y, z));
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
float teleportDistSq = glm::dot(renderPos - oldPos, renderPos - oldPos);
|
||||
bool farTeleport = (teleportDistSq > 500.0f * 500.0f);
|
||||
|
||||
if (farTeleport) {
|
||||
// Far same-map teleport (hearthstone, etc.): defer full world reload
|
||||
// to next frame to avoid blocking the packet handler for 20+ seconds.
|
||||
LOG_WARNING("Far same-map teleport (dist=", std::sqrt(teleportDistSq),
|
||||
"), deferring world reload to next frame");
|
||||
// Update position immediately so the player doesn't keep moving at old location
|
||||
renderer_.getCharacterPosition() = renderPos;
|
||||
if (renderer_.getCameraController()) {
|
||||
auto* ft = renderer_.getCameraController()->getFollowTargetMutable();
|
||||
if (ft) *ft = renderPos;
|
||||
renderer_.getCameraController()->clearMovementInputs();
|
||||
renderer_.getCameraController()->suppressMovementFor(1.0f);
|
||||
renderer_.getCameraController()->suspendGravityFor(10.0f);
|
||||
}
|
||||
if (worldLoader_) worldLoader_->setPendingEntry(mapId, x, y, z);
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Same-map teleport (map ", mapId, "), skipping full world reload");
|
||||
// canonical and renderPos already computed above for distance check
|
||||
renderer_.getCharacterPosition() = renderPos;
|
||||
if (renderer_.getCameraController()) {
|
||||
auto* ft = renderer_.getCameraController()->getFollowTargetMutable();
|
||||
if (ft) *ft = renderPos;
|
||||
}
|
||||
worldEntryMovementGraceTimer_ = 2.0f;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
// Stop any movement that was active before the teleport
|
||||
if (renderer_.getCameraController()) {
|
||||
renderer_.getCameraController()->clearMovementInputs();
|
||||
renderer_.getCameraController()->suppressMovementFor(0.5f);
|
||||
}
|
||||
// Kick off async upload for any tiles that finished background
|
||||
// parsing. Use the bounded processReadyTiles() instead of
|
||||
// processAllReadyTiles() to avoid multi-second main-thread stalls
|
||||
// when many tiles are ready (the rest will finalize over subsequent
|
||||
// frames via the normal terrain update loop).
|
||||
renderer_.getTerrainManager()->processReadyTiles();
|
||||
|
||||
// Queue all remaining tiles within the load radius (8 tiles = 17x17)
|
||||
// at the new position. precacheTiles skips already-loaded/pending tiles,
|
||||
// so this only enqueues tiles that aren't yet in the pipeline.
|
||||
// This ensures background workers immediately start loading everything
|
||||
// visible from the new position (hearthstone may land far from old location).
|
||||
precacheNearbyTiles(renderer_.getTerrainManager(), renderPos, 8);
|
||||
return;
|
||||
}
|
||||
|
||||
// If a world load is already in progress (re-entrant call from
|
||||
// gameHandler->update() processing SMSG_NEW_WORLD during warmup),
|
||||
// defer this entry. The current load will pick it up when it finishes.
|
||||
if (worldLoader_ && worldLoader_->isLoadingWorld()) {
|
||||
LOG_WARNING("World entry deferred: map ", mapId, " while loading (will process after current load)");
|
||||
worldLoader_->setPendingEntry(mapId, x, y, z);
|
||||
return;
|
||||
}
|
||||
|
||||
// Full world loads are expensive and `loadOnlineWorldTerrain()` itself
|
||||
// drives `gameHandler->update()` during warmup. Queue the load here so
|
||||
// it runs after the current packet handler returns instead of recursing
|
||||
// from `SMSG_LOGIN_VERIFY_WORLD` / `SMSG_NEW_WORLD`.
|
||||
LOG_WARNING("Queued world entry: map ", mapId, " pos=(", x, ", ", y, ", ", z, ")");
|
||||
if (worldLoader_) worldLoader_->setPendingEntry(mapId, x, y, z);
|
||||
});
|
||||
|
||||
// /unstuck — nudge player forward and snap to floor at destination.
|
||||
gameHandler_.setUnstuckCallback([this]() {
|
||||
if (!renderer_.getCameraController()) return;
|
||||
worldEntryMovementGraceTimer_ = std::max(worldEntryMovementGraceTimer_, 1.5f);
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
clearStuckMovement();
|
||||
auto* cc = renderer_.getCameraController();
|
||||
auto* ft = cc->getFollowTargetMutable();
|
||||
if (!ft) return;
|
||||
|
||||
glm::vec3 pos = *ft;
|
||||
|
||||
// Always nudge forward first to escape stuck geometry (M2 models, collision seams).
|
||||
float renderYaw = gameHandler_.getMovementInfo().orientation + glm::radians(90.0f);
|
||||
pos.x += std::cos(renderYaw) * 5.0f;
|
||||
pos.y += std::sin(renderYaw) * 5.0f;
|
||||
|
||||
// Sample floor at the DESTINATION position (after nudge).
|
||||
// Pick the highest floor so we snap up to WMO floors when fallen below.
|
||||
bool foundFloor = false;
|
||||
if (auto floor = sampleBestFloorAt(pos.x, pos.y, pos.z + 60.0f)) {
|
||||
pos.z = *floor + 0.2f;
|
||||
foundFloor = true;
|
||||
}
|
||||
|
||||
cc->teleportTo(pos);
|
||||
if (!foundFloor) {
|
||||
cc->setGrounded(false); // Let gravity pull player down to a surface
|
||||
}
|
||||
syncTeleportedPositionToServer(pos);
|
||||
forceServerTeleportCommand(pos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: nudged forward and snapped to floor");
|
||||
});
|
||||
|
||||
// /unstuckgy — stronger recovery: safe/home position, then sampled floor fallback.
|
||||
gameHandler_.setUnstuckGyCallback([this]() {
|
||||
if (!renderer_.getCameraController()) return;
|
||||
worldEntryMovementGraceTimer_ = std::max(worldEntryMovementGraceTimer_, 1.5f);
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
clearStuckMovement();
|
||||
auto* cc = renderer_.getCameraController();
|
||||
auto* ft = cc->getFollowTargetMutable();
|
||||
if (!ft) return;
|
||||
|
||||
// Try last safe position first (nearby, terrain already loaded)
|
||||
if (cc->hasLastSafePosition()) {
|
||||
glm::vec3 safePos = cc->getLastSafePosition();
|
||||
safePos.z += 5.0f;
|
||||
cc->teleportTo(safePos);
|
||||
syncTeleportedPositionToServer(safePos);
|
||||
forceServerTeleportCommand(safePos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: teleported to last safe position");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t bindMap = 0;
|
||||
glm::vec3 bindPos(0.0f);
|
||||
if (gameHandler_.getHomeBind(bindMap, bindPos) &&
|
||||
bindMap == gameHandler_.getCurrentMapId()) {
|
||||
bindPos.z += 2.0f;
|
||||
cc->teleportTo(bindPos);
|
||||
syncTeleportedPositionToServer(bindPos);
|
||||
forceServerTeleportCommand(bindPos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: teleported to home bind position");
|
||||
return;
|
||||
}
|
||||
|
||||
// No safe/bind position — try current XY with a high floor probe.
|
||||
glm::vec3 pos = *ft;
|
||||
if (auto floor = sampleBestFloorAt(pos.x, pos.y, pos.z + 120.0f)) {
|
||||
pos.z = *floor + 0.5f;
|
||||
cc->teleportTo(pos);
|
||||
syncTeleportedPositionToServer(pos);
|
||||
forceServerTeleportCommand(pos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: teleported to sampled floor");
|
||||
return;
|
||||
}
|
||||
|
||||
// Last fallback: high snap to clear deeply bad geometry.
|
||||
pos.z += 60.0f;
|
||||
cc->teleportTo(pos);
|
||||
syncTeleportedPositionToServer(pos);
|
||||
forceServerTeleportCommand(pos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: high fallback snap");
|
||||
});
|
||||
|
||||
// /unstuckhearth — teleport to hearthstone bind point (server-synced).
|
||||
// Freezes player until terrain loads at destination to prevent falling through world.
|
||||
gameHandler_.setUnstuckHearthCallback([this]() {
|
||||
if (!renderer_.getCameraController()) return;
|
||||
|
||||
uint32_t bindMap = 0;
|
||||
glm::vec3 bindPos(0.0f);
|
||||
if (!gameHandler_.getHomeBind(bindMap, bindPos)) {
|
||||
LOG_WARNING("Unstuck hearth: no bind point available");
|
||||
return;
|
||||
}
|
||||
|
||||
worldEntryMovementGraceTimer_ = 10.0f; // long grace — terrain load check will clear it
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
clearStuckMovement();
|
||||
|
||||
auto* cc = renderer_.getCameraController();
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(bindPos);
|
||||
renderPos.z += 2.0f;
|
||||
|
||||
// Freeze player in place (no gravity/movement) until terrain loads
|
||||
cc->teleportTo(renderPos);
|
||||
cc->setExternalFollow(true);
|
||||
forceServerTeleportCommand(renderPos);
|
||||
clearStuckMovement();
|
||||
|
||||
// Set pending state — update loop will unfreeze once terrain is loaded
|
||||
hearthTeleportPending_ = true;
|
||||
hearthTeleportPos_ = renderPos;
|
||||
hearthTeleportTimer_ = 15.0f; // 15s safety timeout
|
||||
LOG_INFO("Unstuck hearth: teleporting to bind point, waiting for terrain...");
|
||||
});
|
||||
|
||||
// Auto-unstuck: falling for > 5 seconds = void fall, teleport to map entry
|
||||
if (renderer_.getCameraController()) {
|
||||
renderer_.getCameraController()->setAutoUnstuckCallback([this]() {
|
||||
if (!renderer_.getCameraController()) return;
|
||||
auto* cc = renderer_.getCameraController();
|
||||
|
||||
// Last resort: teleport to map entry point (terrain guaranteed loaded here)
|
||||
glm::vec3 spawnPos = cc->getDefaultPosition();
|
||||
spawnPos.z += 5.0f;
|
||||
cc->teleportTo(spawnPos);
|
||||
forceServerTeleportCommand(spawnPos);
|
||||
LOG_INFO("Auto-unstuck: teleported to map entry point (server synced)");
|
||||
});
|
||||
}
|
||||
|
||||
// Bind point update (innkeeper) — position stored in gameHandler->getHomeBind()
|
||||
gameHandler_.setBindPointCallback([this](uint32_t mapId, float x, float y, float z) {
|
||||
LOG_INFO("Bindpoint set: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")");
|
||||
});
|
||||
|
||||
// Hearthstone preload callback: begin loading terrain at the bind point as soon as
|
||||
// the player starts casting Hearthstone. The ~10 s cast gives enough time for
|
||||
// the background streaming workers to bring tiles into the cache so the player
|
||||
// lands on solid ground instead of falling through un-loaded terrain.
|
||||
gameHandler_.setHearthstonePreloadCallback([this](uint32_t mapId, float x, float y, float z) {
|
||||
auto* terrainMgr = renderer_.getTerrainManager();
|
||||
if (!terrainMgr || !assetManager_) return;
|
||||
|
||||
// Resolve map name from the cached Map.dbc table
|
||||
std::string mapName;
|
||||
if (worldLoader_) {
|
||||
mapName = worldLoader_->getMapNameById(mapId);
|
||||
}
|
||||
if (mapName.empty()) {
|
||||
mapName = WorldLoader::mapIdToName(mapId);
|
||||
}
|
||||
if (mapName.empty()) mapName = "Azeroth";
|
||||
|
||||
uint32_t currentLoadedMap = worldLoader_ ? worldLoader_->getLoadedMapId() : 0xFFFFFFFF;
|
||||
if (mapId == currentLoadedMap) {
|
||||
// Same map: pre-enqueue tiles around the bind point so workers start
|
||||
// loading them now. Uses render-space coords (canonicalToRender).
|
||||
// Use radius 4 (9x9=81 tiles) — hearthstone cast is ~10s, enough time
|
||||
// for workers to parse most of these before the player arrives.
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
precacheNearbyTiles(terrainMgr, renderPos, 4);
|
||||
auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y);
|
||||
LOG_INFO("Hearthstone preload: enqueued 81"
|
||||
" tiles around bind point (same map) tile=[", tileX, ",", tileY, "]");
|
||||
} else {
|
||||
// Different map: warm the file cache so ADT parsing is fast when
|
||||
// loadOnlineWorldTerrain runs its blocking load loop.
|
||||
// homeBindPos_ is canonical; startWorldPreload expects server coords.
|
||||
glm::vec3 server = core::coords::canonicalToServer(glm::vec3(x, y, z));
|
||||
if (worldLoader_) {
|
||||
worldLoader_->startWorldPreload(mapId, mapName, server.x, server.y);
|
||||
}
|
||||
LOG_INFO("Hearthstone preload: started file cache warm for map '", mapName,
|
||||
"' (id=", mapId, ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── per-frame update ─────────────────────────────────────────────
|
||||
|
||||
void WorldEntryCallbackHandler::update(float deltaTime) {
|
||||
// Hearth teleport: keep player frozen until terrain loads at destination
|
||||
if (hearthTeleportPending_ && renderer_.getTerrainManager()) {
|
||||
hearthTeleportTimer_ -= deltaTime;
|
||||
auto terrainH = renderer_.getTerrainManager()->getHeightAt(
|
||||
hearthTeleportPos_.x, hearthTeleportPos_.y);
|
||||
if (terrainH || hearthTeleportTimer_ <= 0.0f) {
|
||||
// Terrain loaded (or timeout) — snap to floor and release
|
||||
if (terrainH) {
|
||||
hearthTeleportPos_.z = *terrainH + 0.5f;
|
||||
renderer_.getCameraController()->teleportTo(hearthTeleportPos_);
|
||||
}
|
||||
renderer_.getCameraController()->setExternalFollow(false);
|
||||
worldEntryMovementGraceTimer_ = 1.0f;
|
||||
hearthTeleportPending_ = false;
|
||||
LOG_INFO("Unstuck hearth: terrain loaded, player released",
|
||||
terrainH ? "" : " (timeout)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WorldEntryCallbackHandler::resetState() {
|
||||
hearthTeleportPending_ = false;
|
||||
hearthTeleportPos_ = glm::vec3(0.0f);
|
||||
hearthTeleportTimer_ = 0.0f;
|
||||
worldEntryMovementGraceTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
}
|
||||
|
||||
}} // namespace wowee::core
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "core/world_loader.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/world_entry_callback_handler.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "core/entity_spawner.hpp"
|
||||
#include "core/appearance_composer.hpp"
|
||||
|
|
@ -173,9 +174,11 @@ void WorldLoader::processPendingEntry() {
|
|||
auto entry = *pendingWorldEntry_;
|
||||
pendingWorldEntry_.reset();
|
||||
LOG_WARNING("Processing deferred world entry: map ", entry.mapId);
|
||||
app_.worldEntryMovementGraceTimer_ = 2.0f;
|
||||
app_.taxiLandingClampTimer_ = 0.0f;
|
||||
app_.lastTaxiFlight_ = false;
|
||||
if (app_.worldEntryCallbacks_) {
|
||||
app_.worldEntryCallbacks_->setWorldEntryMovementGraceTimer(2.0f);
|
||||
app_.worldEntryCallbacks_->setTaxiLandingClampTimer(0.0f);
|
||||
app_.worldEntryCallbacks_->setLastTaxiFlight(false);
|
||||
}
|
||||
// Clear camera movement inputs before loading terrain
|
||||
if (renderer_ && renderer_->getCameraController()) {
|
||||
renderer_->getCameraController()->clearMovementInputs();
|
||||
|
|
@ -1075,9 +1078,11 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
auto entry = *pendingWorldEntry_;
|
||||
pendingWorldEntry_.reset();
|
||||
LOG_WARNING("Processing deferred world entry: map ", entry.mapId);
|
||||
app_.worldEntryMovementGraceTimer_ = 2.0f;
|
||||
app_.taxiLandingClampTimer_ = 0.0f;
|
||||
app_.lastTaxiFlight_ = false;
|
||||
if (app_.worldEntryCallbacks_) {
|
||||
app_.worldEntryCallbacks_->setWorldEntryMovementGraceTimer(2.0f);
|
||||
app_.worldEntryCallbacks_->setTaxiLandingClampTimer(0.0f);
|
||||
app_.worldEntryCallbacks_->setLastTaxiFlight(false);
|
||||
}
|
||||
// Recursive call — sets loadedMapId_ and IN_GAME state for the final map.
|
||||
loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z);
|
||||
return; // The recursive call handles setState(IN_GAME).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue