mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Merge pull request #49 from ldmonster/chore/ui-callbacks-refactor
[chore] refactor(core): decompose Application::setupUICallbacks() into 7 domain handlers
This commit is contained in:
commit
910ba50c26
18 changed files with 2294 additions and 1765 deletions
|
|
@ -490,6 +490,13 @@ set(WOWEE_SOURCES
|
||||||
src/core/entity_spawner.cpp
|
src/core/entity_spawner.cpp
|
||||||
src/core/appearance_composer.cpp
|
src/core/appearance_composer.cpp
|
||||||
src/core/world_loader.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/window.cpp
|
||||||
src/core/input.cpp
|
src/core/input.cpp
|
||||||
src/core/logger.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 {
|
namespace core {
|
||||||
|
|
||||||
|
// Handler forward declarations
|
||||||
|
class NPCInteractionCallbackHandler;
|
||||||
|
class AudioCallbackHandler;
|
||||||
|
class EntitySpawnCallbackHandler;
|
||||||
|
class AnimationCallbackHandler;
|
||||||
|
class TransportCallbackHandler;
|
||||||
|
class WorldEntryCallbackHandler;
|
||||||
|
class UIScreenCallbackHandler;
|
||||||
|
|
||||||
enum class AppState {
|
enum class AppState {
|
||||||
AUTHENTICATION,
|
AUTHENTICATION,
|
||||||
REALM_SELECTION,
|
REALM_SELECTION,
|
||||||
|
|
@ -134,9 +143,17 @@ private:
|
||||||
std::unique_ptr<WorldLoader> worldLoader_;
|
std::unique_ptr<WorldLoader> worldLoader_;
|
||||||
std::unique_ptr<audio::AudioCoordinator> audioCoordinator_;
|
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;
|
AppState state = AppState::AUTHENTICATION;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
std::string pendingCreatedCharacterName_; // Auto-select after character creation
|
|
||||||
bool playerCharacterSpawned = false;
|
bool playerCharacterSpawned = false;
|
||||||
bool npcsSpawned = false;
|
bool npcsSpawned = false;
|
||||||
bool spawnSnapToGround = true;
|
bool spawnSnapToGround = true;
|
||||||
|
|
@ -154,27 +171,11 @@ private:
|
||||||
static inline const std::string emptyString_;
|
static inline const std::string emptyString_;
|
||||||
static inline const std::vector<std::string> emptyStringVec_;
|
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 facingSendCooldown_ = 0.0f; // Rate-limits MSG_MOVE_SET_FACING
|
||||||
float lastSentCanonicalYaw_ = 1000.0f; // Sentinel — triggers first send
|
float lastSentCanonicalYaw_ = 1000.0f; // Sentinel — triggers first send
|
||||||
float taxiStreamCooldown_ = 0.0f;
|
float taxiStreamCooldown_ = 0.0f;
|
||||||
bool idleYawned_ = false;
|
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;
|
bool wasAutoAttacking_ = false;
|
||||||
|
|
||||||
// Quest marker billboard sprites (above NPCs)
|
// 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
133
src/core/audio_callback_handler.cpp
Normal file
133
src/core/audio_callback_handler.cpp
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
#include "core/audio_callback_handler.hpp"
|
||||||
|
#include "core/coordinates.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include "rendering/renderer.hpp"
|
||||||
|
#include "pipeline/asset_manager.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/world_loader.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
|
#include "core/world_entry_callback_handler.hpp"
|
||||||
#include "rendering/animation/animation_ids.hpp"
|
#include "rendering/animation/animation_ids.hpp"
|
||||||
#include "core/entity_spawner.hpp"
|
#include "core/entity_spawner.hpp"
|
||||||
#include "core/appearance_composer.hpp"
|
#include "core/appearance_composer.hpp"
|
||||||
|
|
@ -173,9 +174,11 @@ void WorldLoader::processPendingEntry() {
|
||||||
auto entry = *pendingWorldEntry_;
|
auto entry = *pendingWorldEntry_;
|
||||||
pendingWorldEntry_.reset();
|
pendingWorldEntry_.reset();
|
||||||
LOG_WARNING("Processing deferred world entry: map ", entry.mapId);
|
LOG_WARNING("Processing deferred world entry: map ", entry.mapId);
|
||||||
app_.worldEntryMovementGraceTimer_ = 2.0f;
|
if (app_.worldEntryCallbacks_) {
|
||||||
app_.taxiLandingClampTimer_ = 0.0f;
|
app_.worldEntryCallbacks_->setWorldEntryMovementGraceTimer(2.0f);
|
||||||
app_.lastTaxiFlight_ = false;
|
app_.worldEntryCallbacks_->setTaxiLandingClampTimer(0.0f);
|
||||||
|
app_.worldEntryCallbacks_->setLastTaxiFlight(false);
|
||||||
|
}
|
||||||
// Clear camera movement inputs before loading terrain
|
// Clear camera movement inputs before loading terrain
|
||||||
if (renderer_ && renderer_->getCameraController()) {
|
if (renderer_ && renderer_->getCameraController()) {
|
||||||
renderer_->getCameraController()->clearMovementInputs();
|
renderer_->getCameraController()->clearMovementInputs();
|
||||||
|
|
@ -1075,9 +1078,11 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
auto entry = *pendingWorldEntry_;
|
auto entry = *pendingWorldEntry_;
|
||||||
pendingWorldEntry_.reset();
|
pendingWorldEntry_.reset();
|
||||||
LOG_WARNING("Processing deferred world entry: map ", entry.mapId);
|
LOG_WARNING("Processing deferred world entry: map ", entry.mapId);
|
||||||
app_.worldEntryMovementGraceTimer_ = 2.0f;
|
if (app_.worldEntryCallbacks_) {
|
||||||
app_.taxiLandingClampTimer_ = 0.0f;
|
app_.worldEntryCallbacks_->setWorldEntryMovementGraceTimer(2.0f);
|
||||||
app_.lastTaxiFlight_ = false;
|
app_.worldEntryCallbacks_->setTaxiLandingClampTimer(0.0f);
|
||||||
|
app_.worldEntryCallbacks_->setLastTaxiFlight(false);
|
||||||
|
}
|
||||||
// Recursive call — sets loadedMapId_ and IN_GAME state for the final map.
|
// Recursive call — sets loadedMapId_ and IN_GAME state for the final map.
|
||||||
loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z);
|
loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z);
|
||||||
return; // The recursive call handles setState(IN_GAME).
|
return; // The recursive call handles setState(IN_GAME).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue