mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Add mount system and crash mouse-release handler
Render mount M2 model under player with seated animation, apply creature skin textures, server-driven speed via SMSG_FORCE_RUN_SPEED_CHANGE, and /dismount command. X11 XUngrabPointer on crash/hang to always release mouse.
This commit is contained in:
parent
4e749080ec
commit
6ba143ddc0
13 changed files with 363 additions and 3 deletions
|
|
@ -272,6 +272,9 @@ if (FFMPEG_LIBRARY_DIRS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Platform-specific libraries
|
# Platform-specific libraries
|
||||||
|
if(UNIX AND NOT APPLE)
|
||||||
|
target_link_libraries(wowee PRIVATE X11)
|
||||||
|
endif()
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(wowee PRIVATE ws2_32)
|
target_link_libraries(wowee PRIVATE ws2_32)
|
||||||
# SDL2main provides WinMain entry point on Windows
|
# SDL2main provides WinMain entry point on Windows
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,10 @@ private:
|
||||||
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
||||||
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
|
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
|
||||||
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
||||||
|
|
||||||
|
// Mount model tracking
|
||||||
|
uint32_t mountInstanceId_ = 0;
|
||||||
|
uint32_t mountModelId_ = 0;
|
||||||
bool creatureLookupsBuilt_ = false;
|
bool creatureLookupsBuilt_ = false;
|
||||||
|
|
||||||
// Deferred creature spawn queue (throttles spawning to avoid hangs)
|
// Deferred creature spawn queue (throttles spawning to avoid hangs)
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,10 @@ public:
|
||||||
uint32_t getDisplayId() const { return displayId; }
|
uint32_t getDisplayId() const { return displayId; }
|
||||||
void setDisplayId(uint32_t id) { displayId = id; }
|
void setDisplayId(uint32_t id) { displayId = id; }
|
||||||
|
|
||||||
|
// Mount display ID (UNIT_FIELD_MOUNTDISPLAYID, index 69)
|
||||||
|
uint32_t getMountDisplayId() const { return mountDisplayId; }
|
||||||
|
void setMountDisplayId(uint32_t id) { mountDisplayId = id; }
|
||||||
|
|
||||||
// Unit flags (UNIT_FIELD_FLAGS, index 59)
|
// Unit flags (UNIT_FIELD_FLAGS, index 59)
|
||||||
uint32_t getUnitFlags() const { return unitFlags; }
|
uint32_t getUnitFlags() const { return unitFlags; }
|
||||||
void setUnitFlags(uint32_t f) { unitFlags = f; }
|
void setUnitFlags(uint32_t f) { unitFlags = f; }
|
||||||
|
|
@ -216,6 +220,7 @@ protected:
|
||||||
uint32_t level = 1;
|
uint32_t level = 1;
|
||||||
uint32_t entry = 0;
|
uint32_t entry = 0;
|
||||||
uint32_t displayId = 0;
|
uint32_t displayId = 0;
|
||||||
|
uint32_t mountDisplayId = 0;
|
||||||
uint32_t unitFlags = 0;
|
uint32_t unitFlags = 0;
|
||||||
uint32_t npcFlags = 0;
|
uint32_t npcFlags = 0;
|
||||||
uint32_t factionTemplate = 0;
|
uint32_t factionTemplate = 0;
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,13 @@ public:
|
||||||
}
|
}
|
||||||
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
|
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
|
||||||
|
|
||||||
|
// Mount state
|
||||||
|
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount
|
||||||
|
void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); }
|
||||||
|
bool isMounted() const { return currentMountDisplayId_ != 0; }
|
||||||
|
float getServerRunSpeed() const { return serverRunSpeed_; }
|
||||||
|
void dismount();
|
||||||
|
|
||||||
// Taxi / Flight Paths
|
// Taxi / Flight Paths
|
||||||
bool isTaxiWindowOpen() const { return taxiWindowOpen_; }
|
bool isTaxiWindowOpen() const { return taxiWindowOpen_; }
|
||||||
void closeTaxi();
|
void closeTaxi();
|
||||||
|
|
@ -630,6 +637,9 @@ private:
|
||||||
// ---- Teleport handler ----
|
// ---- Teleport handler ----
|
||||||
void handleTeleportAck(network::Packet& packet);
|
void handleTeleportAck(network::Packet& packet);
|
||||||
|
|
||||||
|
// ---- Speed change handler ----
|
||||||
|
void handleForceRunSpeedChange(network::Packet& packet);
|
||||||
|
|
||||||
// ---- Taxi handlers ----
|
// ---- Taxi handlers ----
|
||||||
void handleShowTaxiNodes(network::Packet& packet);
|
void handleShowTaxiNodes(network::Packet& packet);
|
||||||
void handleActivateTaxiReply(network::Packet& packet);
|
void handleActivateTaxiReply(network::Packet& packet);
|
||||||
|
|
@ -844,6 +854,8 @@ private:
|
||||||
ShowTaxiNodesData currentTaxiData_;
|
ShowTaxiNodesData currentTaxiData_;
|
||||||
uint64_t taxiNpcGuid_ = 0;
|
uint64_t taxiNpcGuid_ = 0;
|
||||||
bool onTaxiFlight_ = false;
|
bool onTaxiFlight_ = false;
|
||||||
|
uint32_t knownTaxiMask_[12] = {}; // Track previously known nodes for discovery alerts
|
||||||
|
bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
bool vendorWindowOpen = false;
|
bool vendorWindowOpen = false;
|
||||||
|
|
@ -877,6 +889,9 @@ private:
|
||||||
NpcRespawnCallback npcRespawnCallback_;
|
NpcRespawnCallback npcRespawnCallback_;
|
||||||
MeleeSwingCallback meleeSwingCallback_;
|
MeleeSwingCallback meleeSwingCallback_;
|
||||||
NpcSwingCallback npcSwingCallback_;
|
NpcSwingCallback npcSwingCallback_;
|
||||||
|
MountCallback mountCallback_;
|
||||||
|
uint32_t currentMountDisplayId_ = 0;
|
||||||
|
float serverRunSpeed_ = 7.0f;
|
||||||
bool playerDead_ = false;
|
bool playerDead_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -241,9 +241,16 @@ enum class Opcode : uint16_t {
|
||||||
MSG_MOVE_TELEPORT_ACK = 0x0C7,
|
MSG_MOVE_TELEPORT_ACK = 0x0C7,
|
||||||
SMSG_TRANSFER_PENDING = 0x003F,
|
SMSG_TRANSFER_PENDING = 0x003F,
|
||||||
|
|
||||||
|
// ---- Speed Changes ----
|
||||||
|
SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2,
|
||||||
|
|
||||||
|
// ---- Mount ----
|
||||||
|
CMSG_CANCEL_MOUNT_AURA = 0x0375,
|
||||||
|
|
||||||
// ---- Taxi / Flight Paths ----
|
// ---- Taxi / Flight Paths ----
|
||||||
SMSG_SHOWTAXINODES = 0x01A9,
|
SMSG_SHOWTAXINODES = 0x01A9,
|
||||||
SMSG_ACTIVATETAXIREPLY = 0x01AE,
|
SMSG_ACTIVATETAXIREPLY = 0x01AE,
|
||||||
|
SMSG_NEW_TAXI_PATH = 0x01AF,
|
||||||
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
|
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ public:
|
||||||
using MovementCallback = std::function<void(uint32_t opcode)>;
|
using MovementCallback = std::function<void(uint32_t opcode)>;
|
||||||
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
||||||
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
||||||
|
void setRunSpeedOverride(float speed) { runSpeedOverride_ = speed; }
|
||||||
|
void setMounted(bool m) { mounted_ = m; }
|
||||||
|
|
||||||
// For first-person player hiding
|
// For first-person player hiding
|
||||||
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
||||||
|
|
@ -188,6 +190,10 @@ private:
|
||||||
static constexpr float WOW_GRAVITY = -19.29f;
|
static constexpr float WOW_GRAVITY = -19.29f;
|
||||||
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
|
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
|
||||||
|
|
||||||
|
// Server-driven run speed override (0 = use default WOW_RUN_SPEED)
|
||||||
|
float runSpeedOverride_ = 0.0f;
|
||||||
|
bool mounted_ = false;
|
||||||
|
|
||||||
// Online mode: trust server position, don't prefer outdoors over WMO floors
|
// Online mode: trust server position, don't prefer outdoors over WMO floors
|
||||||
bool onlineMode = false;
|
bool onlineMode = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,11 @@ public:
|
||||||
void triggerMeleeSwing();
|
void triggerMeleeSwing();
|
||||||
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
|
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
|
||||||
|
|
||||||
|
// Mount rendering
|
||||||
|
void setMounted(uint32_t mountInstId, float heightOffset);
|
||||||
|
void clearMount();
|
||||||
|
bool isMounted() const { return mountInstanceId_ != 0; }
|
||||||
|
|
||||||
// Selection circle for targeted entity
|
// Selection circle for targeted entity
|
||||||
void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color);
|
void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color);
|
||||||
void clearSelectionCircle();
|
void clearSelectionCircle();
|
||||||
|
|
@ -214,7 +219,7 @@ private:
|
||||||
float characterYaw = 0.0f;
|
float characterYaw = 0.0f;
|
||||||
|
|
||||||
// Character animation state
|
// Character animation state
|
||||||
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING };
|
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT };
|
||||||
CharAnimState charAnimState = CharAnimState::IDLE;
|
CharAnimState charAnimState = CharAnimState::IDLE;
|
||||||
void updateCharacterAnimation();
|
void updateCharacterAnimation();
|
||||||
bool isFootstepAnimationState() const;
|
bool isFootstepAnimationState() const;
|
||||||
|
|
@ -259,6 +264,10 @@ private:
|
||||||
uint32_t meleeAnimId = 0;
|
uint32_t meleeAnimId = 0;
|
||||||
uint32_t equippedWeaponInvType_ = 0;
|
uint32_t equippedWeaponInvType_ = 0;
|
||||||
|
|
||||||
|
// Mount state
|
||||||
|
uint32_t mountInstanceId_ = 0;
|
||||||
|
float mountHeightOffset_ = 0.0f;
|
||||||
|
|
||||||
bool terrainEnabled = true;
|
bool terrainEnabled = true;
|
||||||
bool terrainLoaded = false;
|
bool terrainLoaded = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,11 @@ void Application::update(float deltaTime) {
|
||||||
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync server run speed to camera controller
|
||||||
|
if (renderer && gameHandler && renderer->getCameraController()) {
|
||||||
|
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
||||||
|
}
|
||||||
|
|
||||||
// Sync character render position → canonical WoW coords each frame
|
// Sync character render position → canonical WoW coords each frame
|
||||||
if (renderer && gameHandler) {
|
if (renderer && gameHandler) {
|
||||||
glm::vec3 renderPos = renderer->getCharacterPosition();
|
glm::vec3 renderPos = renderer->getCharacterPosition();
|
||||||
|
|
@ -554,6 +559,150 @@ void Application::setupUICallbacks() {
|
||||||
despawnOnlineCreature(guid);
|
despawnOnlineCreature(guid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mount callback (online mode) - load/destroy mount model
|
||||||
|
gameHandler->setMountCallback([this](uint32_t mountDisplayId) {
|
||||||
|
if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return;
|
||||||
|
auto* charRenderer = renderer->getCharacterRenderer();
|
||||||
|
|
||||||
|
if (mountDisplayId == 0) {
|
||||||
|
// Dismount: remove mount instance and model
|
||||||
|
if (mountInstanceId_ != 0) {
|
||||||
|
charRenderer->removeInstance(mountInstanceId_);
|
||||||
|
mountInstanceId_ = 0;
|
||||||
|
}
|
||||||
|
mountModelId_ = 0;
|
||||||
|
renderer->clearMount();
|
||||||
|
LOG_INFO("Dismounted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount: load mount model
|
||||||
|
std::string m2Path = getModelPathForDisplayId(mountDisplayId);
|
||||||
|
if (m2Path.empty()) {
|
||||||
|
LOG_WARNING("No model path for mount displayId ", mountDisplayId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check model cache
|
||||||
|
uint32_t modelId = 0;
|
||||||
|
bool modelCached = false;
|
||||||
|
auto cacheIt = displayIdModelCache_.find(mountDisplayId);
|
||||||
|
if (cacheIt != displayIdModelCache_.end()) {
|
||||||
|
modelId = cacheIt->second;
|
||||||
|
modelCached = true;
|
||||||
|
} else {
|
||||||
|
modelId = nextCreatureModelId_++;
|
||||||
|
|
||||||
|
auto m2Data = assetManager->readFile(m2Path);
|
||||||
|
if (m2Data.empty()) {
|
||||||
|
LOG_WARNING("Failed to read mount M2: ", m2Path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||||
|
if (model.vertices.empty()) {
|
||||||
|
LOG_WARNING("Failed to parse mount M2: ", m2Path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load skin file
|
||||||
|
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||||
|
auto skinData = assetManager->readFile(skinPath);
|
||||||
|
if (!skinData.empty()) {
|
||||||
|
pipeline::M2Loader::loadSkin(skinData, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load external .anim files
|
||||||
|
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||||
|
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||||
|
if (!(model.sequences[si].flags & 0x20)) {
|
||||||
|
char animFileName[256];
|
||||||
|
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||||
|
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
||||||
|
auto animData = assetManager->readFile(animFileName);
|
||||||
|
if (!animData.empty()) {
|
||||||
|
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!charRenderer->loadModel(model, modelId)) {
|
||||||
|
LOG_WARNING("Failed to load mount model: ", m2Path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayIdModelCache_[mountDisplayId] = modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply creature skin textures from CreatureDisplayInfo.dbc
|
||||||
|
if (!modelCached) {
|
||||||
|
auto itDisplayData = displayDataMap_.find(mountDisplayId);
|
||||||
|
if (itDisplayData != displayDataMap_.end()) {
|
||||||
|
const auto& dispData = itDisplayData->second;
|
||||||
|
const auto* modelData = charRenderer->getModelData(modelId);
|
||||||
|
if (modelData) {
|
||||||
|
std::string modelDir;
|
||||||
|
size_t lastSlash = m2Path.find_last_of("\\/");
|
||||||
|
if (lastSlash != std::string::npos) {
|
||||||
|
modelDir = m2Path.substr(0, lastSlash + 1);
|
||||||
|
}
|
||||||
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
|
const auto& tex = modelData->textures[ti];
|
||||||
|
std::string texPath;
|
||||||
|
if (tex.type == 11 && !dispData.skin1.empty()) {
|
||||||
|
texPath = modelDir + dispData.skin1 + ".blp";
|
||||||
|
} else if (tex.type == 12 && !dispData.skin2.empty()) {
|
||||||
|
texPath = modelDir + dispData.skin2 + ".blp";
|
||||||
|
} else if (tex.type == 13 && !dispData.skin3.empty()) {
|
||||||
|
texPath = modelDir + dispData.skin3 + ".blp";
|
||||||
|
}
|
||||||
|
if (!texPath.empty()) {
|
||||||
|
GLuint skinTex = charRenderer->loadTexture(texPath);
|
||||||
|
if (skinTex != 0) {
|
||||||
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), skinTex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mountModelId_ = modelId;
|
||||||
|
|
||||||
|
// Create mount instance at player position
|
||||||
|
glm::vec3 mountPos = renderer->getCharacterPosition();
|
||||||
|
float yawRad = glm::radians(renderer->getCharacterYaw());
|
||||||
|
uint32_t instanceId = charRenderer->createInstance(modelId, mountPos,
|
||||||
|
glm::vec3(0.0f, 0.0f, yawRad), 1.0f);
|
||||||
|
|
||||||
|
if (instanceId == 0) {
|
||||||
|
LOG_WARNING("Failed to create mount instance");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mountInstanceId_ = instanceId;
|
||||||
|
|
||||||
|
// Compute height offset — place player above mount's back
|
||||||
|
const auto* modelData = charRenderer->getModelData(modelId);
|
||||||
|
float heightOffset = 1.2f; // Default fallback
|
||||||
|
if (modelData) {
|
||||||
|
// No coord swizzle in character renderer, so Z is up in model space too.
|
||||||
|
// Use the top of the bounding box as the saddle height.
|
||||||
|
float topZ = modelData->boundMax.z;
|
||||||
|
if (topZ > 0.1f) {
|
||||||
|
heightOffset = topZ * 0.85f;
|
||||||
|
}
|
||||||
|
LOG_INFO("Mount bounds: min=(", modelData->boundMin.x, ",", modelData->boundMin.y, ",", modelData->boundMin.z,
|
||||||
|
") max=(", modelData->boundMax.x, ",", modelData->boundMax.y, ",", modelData->boundMax.z,
|
||||||
|
") radius=", modelData->boundRadius, " → heightOffset=", heightOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer->setMounted(instanceId, heightOffset);
|
||||||
|
charRenderer->playAnimation(instanceId, 0, true); // Idle animation
|
||||||
|
|
||||||
|
LOG_INFO("Mounted: displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset);
|
||||||
|
});
|
||||||
|
|
||||||
// Creature move callback (online mode) - update creature positions
|
// Creature move callback (online mode) - update creature positions
|
||||||
gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
||||||
auto it = creatureInstances_.find(guid);
|
auto it = creatureInstances_.find(guid);
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
handleMonsterMove(packet);
|
handleMonsterMove(packet);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ---- Speed Changes ----
|
||||||
|
case Opcode::SMSG_FORCE_RUN_SPEED_CHANGE:
|
||||||
|
handleForceRunSpeedChange(packet);
|
||||||
|
break;
|
||||||
|
|
||||||
// ---- Phase 2: Combat ----
|
// ---- Phase 2: Combat ----
|
||||||
case Opcode::SMSG_ATTACKSTART:
|
case Opcode::SMSG_ATTACKSTART:
|
||||||
handleAttackStart(packet);
|
handleAttackStart(packet);
|
||||||
|
|
@ -648,6 +653,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_ACTIVATETAXIREPLY:
|
case Opcode::SMSG_ACTIVATETAXIREPLY:
|
||||||
handleActivateTaxiReply(packet);
|
handleActivateTaxiReply(packet);
|
||||||
break;
|
break;
|
||||||
|
case Opcode::SMSG_NEW_TAXI_PATH:
|
||||||
|
// Empty packet - server signals a new flight path was learned
|
||||||
|
// The actual node details come in the next SMSG_SHOWTAXINODES
|
||||||
|
addSystemChatMessage("New flight path discovered!");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
||||||
|
|
@ -1287,6 +1297,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||||
case 54: unit->setLevel(val); break;
|
case 54: unit->setLevel(val); break;
|
||||||
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||||
|
case 69: // UNIT_FIELD_MOUNTDISPLAYID
|
||||||
|
if (block.guid == playerGuid) {
|
||||||
|
uint32_t old = currentMountDisplayId_;
|
||||||
|
currentMountDisplayId_ = val;
|
||||||
|
if (val != old && mountCallback_) mountCallback_(val);
|
||||||
|
}
|
||||||
|
unit->setMountDisplayId(val);
|
||||||
|
break;
|
||||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
@ -1398,6 +1416,15 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
unit->setFactionTemplate(val);
|
unit->setFactionTemplate(val);
|
||||||
unit->setHostile(isHostileFaction(val));
|
unit->setHostile(isHostileFaction(val));
|
||||||
break;
|
break;
|
||||||
|
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||||
|
case 69: // UNIT_FIELD_MOUNTDISPLAYID
|
||||||
|
if (block.guid == playerGuid) {
|
||||||
|
uint32_t old = currentMountDisplayId_;
|
||||||
|
currentMountDisplayId_ = val;
|
||||||
|
if (val != old && mountCallback_) mountCallback_(val);
|
||||||
|
}
|
||||||
|
unit->setMountDisplayId(val);
|
||||||
|
break;
|
||||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
@ -2964,6 +2991,27 @@ void GameHandler::handleAttackStop(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::dismount() {
|
||||||
|
if (!isMounted() || !socket) return;
|
||||||
|
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_CANCEL_MOUNT_AURA));
|
||||||
|
socket->send(pkt);
|
||||||
|
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
|
||||||
|
// Packed GUID
|
||||||
|
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
// uint32 counter (ack counter, we ignore)
|
||||||
|
packet.readUInt32();
|
||||||
|
// float newSpeed
|
||||||
|
float newSpeed = packet.readFloat();
|
||||||
|
|
||||||
|
if (guid == playerGuid) {
|
||||||
|
serverRunSpeed_ = newSpeed;
|
||||||
|
LOG_INFO("Server run speed changed to ", newSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||||
MonsterMoveData data;
|
MonsterMoveData data;
|
||||||
if (!MonsterMoveParser::parse(packet, data)) {
|
if (!MonsterMoveParser::parse(packet, data)) {
|
||||||
|
|
@ -3945,6 +3993,29 @@ void GameHandler::handleShowTaxiNodes(network::Packet& packet) {
|
||||||
|
|
||||||
loadTaxiDbc();
|
loadTaxiDbc();
|
||||||
|
|
||||||
|
// Detect newly discovered flight paths by comparing with stored mask
|
||||||
|
if (taxiMaskInitialized_) {
|
||||||
|
for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) {
|
||||||
|
uint32_t newBits = data.nodeMask[i] & ~knownTaxiMask_[i];
|
||||||
|
if (newBits == 0) continue;
|
||||||
|
for (uint32_t bit = 0; bit < 32; ++bit) {
|
||||||
|
if (newBits & (1u << bit)) {
|
||||||
|
uint32_t nodeId = i * 32 + bit;
|
||||||
|
auto it = taxiNodes_.find(nodeId);
|
||||||
|
if (it != taxiNodes_.end()) {
|
||||||
|
addSystemChatMessage("Discovered flight path: " + it->second.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stored mask
|
||||||
|
for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) {
|
||||||
|
knownTaxiMask_[i] = data.nodeMask[i];
|
||||||
|
}
|
||||||
|
taxiMaskInitialized_ = true;
|
||||||
|
|
||||||
currentTaxiData_ = data;
|
currentTaxiData_ = data;
|
||||||
taxiNpcGuid_ = data.npcGuid;
|
taxiNpcGuid_ = data.npcGuid;
|
||||||
taxiWindowOpen_ = true;
|
taxiWindowOpen_ = true;
|
||||||
|
|
|
||||||
27
src/main.cpp
27
src/main.cpp
|
|
@ -1,8 +1,33 @@
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
#include <csignal>
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
|
||||||
|
static void releaseMouseGrab() {
|
||||||
|
// Bypass SDL — talk to X11 directly (signal-safe enough for our purposes)
|
||||||
|
Display* dpy = XOpenDisplay(nullptr);
|
||||||
|
if (dpy) {
|
||||||
|
XUngrabPointer(dpy, CurrentTime);
|
||||||
|
XUngrabKeyboard(dpy, CurrentTime);
|
||||||
|
XFlush(dpy);
|
||||||
|
XCloseDisplay(dpy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void crashHandler(int sig) {
|
||||||
|
releaseMouseGrab();
|
||||||
|
std::signal(sig, SIG_DFL);
|
||||||
|
std::raise(sig);
|
||||||
|
}
|
||||||
|
|
||||||
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
||||||
|
std::signal(SIGSEGV, crashHandler);
|
||||||
|
std::signal(SIGABRT, crashHandler);
|
||||||
|
std::signal(SIGFPE, crashHandler);
|
||||||
|
std::signal(SIGTERM, crashHandler);
|
||||||
|
std::signal(SIGINT, crashHandler);
|
||||||
try {
|
try {
|
||||||
wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::DEBUG);
|
wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::DEBUG);
|
||||||
LOG_INFO("=== Wowee Native Client ===");
|
LOG_INFO("=== Wowee Native Client ===");
|
||||||
|
|
@ -22,10 +47,12 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (const std::exception& e) {
|
catch (const std::exception& e) {
|
||||||
|
releaseMouseGrab();
|
||||||
LOG_FATAL("Unhandled exception: ", e.what());
|
LOG_FATAL("Unhandled exception: ", e.what());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
catch (...) {
|
catch (...) {
|
||||||
|
releaseMouseGrab();
|
||||||
LOG_FATAL("Unknown exception occurred");
|
LOG_FATAL("Unknown exception occurred");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ void CameraController::update(float deltaTime) {
|
||||||
} else if (ctrlDown) {
|
} else if (ctrlDown) {
|
||||||
speed = WOW_WALK_SPEED;
|
speed = WOW_WALK_SPEED;
|
||||||
} else {
|
} else {
|
||||||
speed = WOW_RUN_SPEED;
|
speed = (runSpeedOverride_ > 0.0f) ? runSpeedOverride_ : WOW_RUN_SPEED;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Exploration mode (original behavior)
|
// Exploration mode (original behavior)
|
||||||
|
|
@ -225,10 +225,12 @@ void CameraController::update(float deltaTime) {
|
||||||
glm::vec3 right(-std::sin(moveYawRad), std::cos(moveYawRad), 0.0f);
|
glm::vec3 right(-std::sin(moveYawRad), std::cos(moveYawRad), 0.0f);
|
||||||
|
|
||||||
// Toggle sit/crouch with X key (edge-triggered) — only when UI doesn't want keyboard
|
// Toggle sit/crouch with X key (edge-triggered) — only when UI doesn't want keyboard
|
||||||
|
// Blocked while mounted
|
||||||
bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X);
|
bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X);
|
||||||
if (xDown && !xKeyWasDown) {
|
if (xDown && !xKeyWasDown && !mounted_) {
|
||||||
sitting = !sitting;
|
sitting = !sitting;
|
||||||
}
|
}
|
||||||
|
if (mounted_) sitting = false;
|
||||||
xKeyWasDown = xDown;
|
xKeyWasDown = xDown;
|
||||||
|
|
||||||
// Update eye height based on crouch state (smooth transition)
|
// Update eye height based on crouch state (smooth transition)
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,20 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Renderer::setMounted(uint32_t mountInstId, float heightOffset) {
|
||||||
|
mountInstanceId_ = mountInstId;
|
||||||
|
mountHeightOffset_ = heightOffset;
|
||||||
|
charAnimState = CharAnimState::MOUNT;
|
||||||
|
if (cameraController) cameraController->setMounted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::clearMount() {
|
||||||
|
mountInstanceId_ = 0;
|
||||||
|
mountHeightOffset_ = 0.0f;
|
||||||
|
charAnimState = CharAnimState::IDLE;
|
||||||
|
if (cameraController) cameraController->setMounted(false);
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t Renderer::resolveMeleeAnimId() {
|
uint32_t Renderer::resolveMeleeAnimId() {
|
||||||
if (!characterRenderer || characterInstanceId == 0) {
|
if (!characterRenderer || characterInstanceId == 0) {
|
||||||
meleeAnimId = 0;
|
meleeAnimId = 0;
|
||||||
|
|
@ -473,6 +487,7 @@ void Renderer::updateCharacterAnimation() {
|
||||||
constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle)
|
constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle)
|
||||||
constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle)
|
constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle)
|
||||||
constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim)
|
constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim)
|
||||||
|
constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount
|
||||||
|
|
||||||
CharAnimState newState = charAnimState;
|
CharAnimState newState = charAnimState;
|
||||||
|
|
||||||
|
|
@ -489,6 +504,42 @@ void Renderer::updateCharacterAnimation() {
|
||||||
bool swim = cameraController->isSwimming();
|
bool swim = cameraController->isSwimming();
|
||||||
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
||||||
|
|
||||||
|
// When mounted, force MOUNT state and skip normal transitions
|
||||||
|
if (isMounted()) {
|
||||||
|
newState = CharAnimState::MOUNT;
|
||||||
|
charAnimState = newState;
|
||||||
|
|
||||||
|
// Play seated animation on player
|
||||||
|
uint32_t currentAnimId = 0;
|
||||||
|
float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f;
|
||||||
|
bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs);
|
||||||
|
if (!haveState || currentAnimId != ANIM_MOUNT) {
|
||||||
|
characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync mount instance position and rotation
|
||||||
|
if (mountInstanceId_ > 0) {
|
||||||
|
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
||||||
|
float yawRad = glm::radians(characterYaw);
|
||||||
|
characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(0.0f, 0.0f, yawRad));
|
||||||
|
|
||||||
|
// Drive mount model animation: idle when still, run when moving
|
||||||
|
uint32_t mountAnimId = moving ? ANIM_RUN : ANIM_STAND;
|
||||||
|
uint32_t curMountAnim = 0;
|
||||||
|
float curMountTime = 0, curMountDur = 0;
|
||||||
|
bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur);
|
||||||
|
if (!haveMountState || curMountAnim != mountAnimId) {
|
||||||
|
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset player Z above mount
|
||||||
|
glm::vec3 playerPos = characterPosition;
|
||||||
|
playerPos.z += mountHeightOffset_;
|
||||||
|
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!forceMelee) switch (charAnimState) {
|
if (!forceMelee) switch (charAnimState) {
|
||||||
case CharAnimState::IDLE:
|
case CharAnimState::IDLE:
|
||||||
if (swim) {
|
if (swim) {
|
||||||
|
|
@ -629,6 +680,9 @@ void Renderer::updateCharacterAnimation() {
|
||||||
newState = CharAnimState::IDLE;
|
newState = CharAnimState::IDLE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CharAnimState::MOUNT:
|
||||||
|
break; // Handled by early return above
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceMelee) {
|
if (forceMelee) {
|
||||||
|
|
@ -692,6 +746,7 @@ void Renderer::updateCharacterAnimation() {
|
||||||
}
|
}
|
||||||
loop = false;
|
loop = false;
|
||||||
break;
|
break;
|
||||||
|
case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t currentAnimId = 0;
|
uint32_t currentAnimId = 0;
|
||||||
|
|
|
||||||
|
|
@ -1138,6 +1138,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /dismount command
|
||||||
|
if (cmdLower == "dismount") {
|
||||||
|
gameHandler.dismount();
|
||||||
|
chatInputBuffer[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// /sit command
|
// /sit command
|
||||||
if (cmdLower == "sit") {
|
if (cmdLower == "sit") {
|
||||||
gameHandler.setStandState(1); // 1 = sit
|
gameHandler.setStandState(1); // 1 = sit
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue