mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 00:20:16 +00:00
Fix taxi flights, mounts, and movement recovery
This commit is contained in:
parent
d910073d7a
commit
6736ec328b
13 changed files with 607 additions and 49 deletions
|
|
@ -70,6 +70,8 @@ public:
|
|||
const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; }
|
||||
uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; }
|
||||
uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; }
|
||||
uint32_t getGryphonDisplayId() const { return gryphonDisplayId_; }
|
||||
uint32_t getWyvernDisplayId() const { return wyvernDisplayId_; }
|
||||
|
||||
private:
|
||||
void update(float deltaTime);
|
||||
|
|
@ -154,6 +156,10 @@ private:
|
|||
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
||||
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
|
||||
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
||||
uint32_t gryphonDisplayId_ = 0;
|
||||
uint32_t wyvernDisplayId_ = 0;
|
||||
bool lastTaxiFlight_ = false;
|
||||
float taxiStreamCooldown_ = 0.0f;
|
||||
|
||||
// Online gameobject model spawning
|
||||
struct GameObjectInstanceInfo {
|
||||
|
|
|
|||
|
|
@ -503,6 +503,8 @@ public:
|
|||
uint32_t mapId = 0;
|
||||
float x = 0, y = 0, z = 0;
|
||||
std::string name;
|
||||
uint32_t mountDisplayIdAlliance = 0;
|
||||
uint32_t mountDisplayIdHorde = 0;
|
||||
};
|
||||
struct TaxiPathEdge {
|
||||
uint32_t pathId = 0;
|
||||
|
|
@ -668,6 +670,7 @@ private:
|
|||
|
||||
// ---- Teleport handler ----
|
||||
void handleTeleportAck(network::Packet& packet);
|
||||
void handleNewWorld(network::Packet& packet);
|
||||
|
||||
// ---- Speed change handler ----
|
||||
void handleForceRunSpeedChange(network::Packet& packet);
|
||||
|
|
@ -858,6 +861,7 @@ private:
|
|||
std::string pendingInviterName;
|
||||
|
||||
uint64_t activeCharacterGuid_ = 0;
|
||||
Race playerRace_ = Race::HUMAN;
|
||||
|
||||
// ---- Phase 5: Loot ----
|
||||
bool lootWindowOpen = false;
|
||||
|
|
@ -904,10 +908,25 @@ private:
|
|||
ShowTaxiNodesData currentTaxiData_;
|
||||
uint64_t taxiNpcGuid_ = 0;
|
||||
bool onTaxiFlight_ = false;
|
||||
bool taxiMountActive_ = false;
|
||||
uint32_t taxiMountDisplayId_ = 0;
|
||||
bool taxiActivatePending_ = false;
|
||||
float taxiActivateTimer_ = 0.0f;
|
||||
bool taxiClientActive_ = false;
|
||||
size_t taxiClientIndex_ = 0;
|
||||
std::vector<glm::vec3> taxiClientPath_;
|
||||
float taxiClientSpeed_ = 32.0f;
|
||||
float taxiClientSegmentProgress_ = 0.0f;
|
||||
bool taxiRecoverPending_ = false;
|
||||
uint32_t taxiRecoverMapId_ = 0;
|
||||
glm::vec3 taxiRecoverPos_{0.0f};
|
||||
uint32_t knownTaxiMask_[12] = {}; // Track previously known nodes for discovery alerts
|
||||
bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts
|
||||
std::unordered_map<uint32_t, uint32_t> taxiCostMap_; // destNodeId -> total cost in copper
|
||||
void buildTaxiCostMap();
|
||||
void applyTaxiMountForCurrentNode();
|
||||
void startClientTaxiPath(const std::vector<uint32_t>& pathNodes);
|
||||
void updateClientTaxi(float deltaTime);
|
||||
|
||||
// Vendor
|
||||
bool vendorWindowOpen = false;
|
||||
|
|
|
|||
|
|
@ -250,6 +250,9 @@ enum class Opcode : uint16_t {
|
|||
// ---- Teleport / Transfer ----
|
||||
MSG_MOVE_TELEPORT_ACK = 0x0C7,
|
||||
SMSG_TRANSFER_PENDING = 0x003F,
|
||||
SMSG_NEW_WORLD = 0x003E,
|
||||
MSG_MOVE_WORLDPORT_ACK = 0x00DC,
|
||||
SMSG_TRANSFER_ABORTED = 0x0040,
|
||||
|
||||
// ---- Speed Changes ----
|
||||
SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2,
|
||||
|
|
@ -261,6 +264,8 @@ enum class Opcode : uint16_t {
|
|||
// ---- Taxi / Flight Paths ----
|
||||
SMSG_SHOWTAXINODES = 0x01A9,
|
||||
SMSG_ACTIVATETAXIREPLY = 0x01AE,
|
||||
// Some cores send activate taxi reply on 0x029D (observed in logs)
|
||||
SMSG_ACTIVATETAXIREPLY_ALT = 0x029D,
|
||||
SMSG_NEW_TAXI_PATH = 0x01AF,
|
||||
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
|
||||
|
||||
|
|
|
|||
|
|
@ -1733,7 +1733,7 @@ public:
|
|||
/** CMSG_ACTIVATETAXIEXPRESS packet builder */
|
||||
class ActivateTaxiExpressPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t npcGuid, const std::vector<uint32_t>& pathNodes);
|
||||
static network::Packet build(uint64_t npcGuid, uint32_t totalCost, const std::vector<uint32_t>& pathNodes);
|
||||
};
|
||||
|
||||
/** CMSG_ACTIVATETAXI packet builder */
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ public:
|
|||
void setRunSpeedOverride(float speed) { runSpeedOverride_ = speed; }
|
||||
void setMounted(bool m) { mounted_ = m; }
|
||||
void setMountHeightOffset(float offset) { mountHeightOffset_ = offset; }
|
||||
void setExternalFollow(bool enabled) { externalFollow_ = enabled; }
|
||||
void setExternalMoving(bool moving) { externalMoving_ = moving; }
|
||||
void clearMovementInputs();
|
||||
|
||||
// For first-person player hiding
|
||||
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
||||
|
|
@ -113,6 +116,7 @@ private:
|
|||
float userTargetDistance = 10.0f; // What the player wants (scroll wheel)
|
||||
float currentDistance = 10.0f; // Smoothed actual distance
|
||||
float collisionDistance = 10.0f; // Max allowed by collision
|
||||
bool externalFollow_ = false;
|
||||
static constexpr float MIN_DISTANCE = 0.5f; // Minimum zoom (first-person threshold)
|
||||
static constexpr float MAX_DISTANCE = 50.0f; // Maximum zoom out
|
||||
static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases
|
||||
|
|
@ -195,6 +199,7 @@ private:
|
|||
float runSpeedOverride_ = 0.0f;
|
||||
bool mounted_ = false;
|
||||
float mountHeightOffset_ = 0.0f;
|
||||
bool externalMoving_ = false;
|
||||
|
||||
// Online mode: trust server position, don't prefer outdoors over WMO floors
|
||||
bool onlineMode = false;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ public:
|
|||
glm::vec3& getCharacterPosition() { return characterPosition; }
|
||||
uint32_t getCharacterInstanceId() const { return characterInstanceId; }
|
||||
float getCharacterYaw() const { return characterYaw; }
|
||||
void setCharacterYaw(float yawDeg) { characterYaw = yawDeg; }
|
||||
|
||||
// Emote support
|
||||
void playEmote(const std::string& emoteName);
|
||||
|
|
@ -127,6 +128,7 @@ public:
|
|||
|
||||
// Mount rendering
|
||||
void setMounted(uint32_t mountInstId, float heightOffset);
|
||||
void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; }
|
||||
void clearMount();
|
||||
bool isMounted() const { return mountInstanceId_ != 0; }
|
||||
|
||||
|
|
@ -272,6 +274,7 @@ private:
|
|||
// Mount state
|
||||
uint32_t mountInstanceId_ = 0;
|
||||
float mountHeightOffset_ = 0.0f;
|
||||
bool taxiFlight_ = false;
|
||||
|
||||
bool terrainEnabled = true;
|
||||
bool terrainLoaded = false;
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ public:
|
|||
void setLoadRadius(int radius) { loadRadius = radius; }
|
||||
void setUnloadRadius(int radius) { unloadRadius = radius; }
|
||||
void setStreamingEnabled(bool enabled) { streamingEnabled = enabled; }
|
||||
void setUpdateInterval(float seconds) { updateInterval = seconds; }
|
||||
void setWaterRenderer(WaterRenderer* renderer) { waterRenderer = renderer; }
|
||||
void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; }
|
||||
void setWMORenderer(WMORenderer* renderer) { wmoRenderer = renderer; }
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ void Application::setState(AppState newState) {
|
|||
case AppState::CHARACTER_SELECTION:
|
||||
// Show character screen
|
||||
break;
|
||||
case AppState::IN_GAME:
|
||||
case AppState::IN_GAME: {
|
||||
// Wire up movement opcodes from camera controller
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
auto* cc = renderer->getCameraController();
|
||||
|
|
@ -323,6 +323,7 @@ void Application::setState(AppState newState) {
|
|||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AppState::DISCONNECTED:
|
||||
// Back to auth
|
||||
break;
|
||||
|
|
@ -379,7 +380,7 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
break;
|
||||
|
||||
case AppState::IN_GAME:
|
||||
case AppState::IN_GAME: {
|
||||
if (gameHandler) {
|
||||
gameHandler->update(deltaTime);
|
||||
}
|
||||
|
|
@ -399,16 +400,60 @@ void Application::update(float deltaTime) {
|
|||
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
||||
}
|
||||
|
||||
// Sync character render position → canonical WoW coords each frame
|
||||
if (renderer && gameHandler) {
|
||||
glm::vec3 renderPos = renderer->getCharacterPosition();
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
bool onTaxi = gameHandler && gameHandler->isOnTaxiFlight();
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->setExternalFollow(onTaxi);
|
||||
renderer->getCameraController()->setExternalMoving(onTaxi);
|
||||
if (lastTaxiFlight_ && !onTaxi) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
}
|
||||
}
|
||||
if (renderer) {
|
||||
renderer->setTaxiFlight(onTaxi);
|
||||
}
|
||||
if (renderer && renderer->getTerrainManager()) {
|
||||
renderer->getTerrainManager()->setStreamingEnabled(true);
|
||||
// Freeze new tile streaming during taxi to avoid hangs; still process ready tiles.
|
||||
if (onTaxi) {
|
||||
renderer->getTerrainManager()->setUpdateInterval(9999.0f);
|
||||
taxiStreamCooldown_ = 2.5f;
|
||||
} else {
|
||||
// Ramp streaming back in after taxi to avoid end-of-flight hitches.
|
||||
if (lastTaxiFlight_) {
|
||||
taxiStreamCooldown_ = 2.5f;
|
||||
}
|
||||
if (taxiStreamCooldown_ > 0.0f) {
|
||||
taxiStreamCooldown_ -= deltaTime;
|
||||
renderer->getTerrainManager()->setUpdateInterval(1.0f);
|
||||
} else {
|
||||
renderer->getTerrainManager()->setUpdateInterval(0.1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastTaxiFlight_ = onTaxi;
|
||||
|
||||
// Sync orientation: camera yaw (degrees) → WoW orientation (radians)
|
||||
float yawDeg = renderer->getCharacterYaw();
|
||||
float wowOrientation = glm::radians(yawDeg - 90.0f);
|
||||
gameHandler->setOrientation(wowOrientation);
|
||||
// Sync character render position ↔ canonical WoW coords each frame
|
||||
if (renderer && gameHandler) {
|
||||
if (onTaxi) {
|
||||
auto playerEntity = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid());
|
||||
if (playerEntity) {
|
||||
glm::vec3 canonical(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
renderer->getCharacterPosition() = renderPos;
|
||||
// Lock yaw to server/taxi orientation so mouse cannot steer during taxi.
|
||||
float yawDeg = glm::degrees(playerEntity->getOrientation()) + 90.0f;
|
||||
renderer->setCharacterYaw(yawDeg);
|
||||
}
|
||||
} else {
|
||||
glm::vec3 renderPos = renderer->getCharacterPosition();
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
|
||||
// Sync orientation: camera yaw (degrees) → WoW orientation (radians)
|
||||
float yawDeg = renderer->getCharacterYaw();
|
||||
float wowOrientation = glm::radians(yawDeg - 90.0f);
|
||||
gameHandler->setOrientation(wowOrientation);
|
||||
}
|
||||
}
|
||||
|
||||
// Send movement heartbeat every 500ms (keeps server position in sync)
|
||||
|
|
@ -422,6 +467,7 @@ void Application::update(float deltaTime) {
|
|||
movementHeartbeatTimer = 0.0f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AppState::DISCONNECTED:
|
||||
// Handle disconnection
|
||||
|
|
@ -1661,6 +1707,46 @@ void Application::buildCreatureDisplayLookups() {
|
|||
LOG_INFO("Loaded ", modelIdToPath_.size(), " model→path mappings");
|
||||
}
|
||||
|
||||
// Resolve gryphon/wyvern display IDs by exact model path so taxi mounts have textures.
|
||||
auto toLower = [](std::string s) {
|
||||
for (char& c : s) c = static_cast<char>(::tolower(c));
|
||||
return s;
|
||||
};
|
||||
auto normalizePath = [&](const std::string& p) {
|
||||
std::string s = p;
|
||||
for (char& c : s) if (c == '/') c = '\\';
|
||||
return toLower(s);
|
||||
};
|
||||
auto resolveDisplayIdForExactPath = [&](const std::string& exactPath) -> uint32_t {
|
||||
const std::string target = normalizePath(exactPath);
|
||||
uint32_t modelId = 0;
|
||||
for (const auto& [mid, path] : modelIdToPath_) {
|
||||
if (normalizePath(path) == target) {
|
||||
modelId = mid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (modelId == 0) return 0;
|
||||
uint32_t bestDisplayId = 0;
|
||||
int bestScore = -1;
|
||||
for (const auto& [dispId, data] : displayDataMap_) {
|
||||
if (data.modelId != modelId) continue;
|
||||
int score = 0;
|
||||
if (!data.skin1.empty()) score += 3;
|
||||
if (!data.skin2.empty()) score += 2;
|
||||
if (!data.skin3.empty()) score += 1;
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestDisplayId = dispId;
|
||||
}
|
||||
}
|
||||
return bestDisplayId;
|
||||
};
|
||||
|
||||
gryphonDisplayId_ = resolveDisplayIdForExactPath("Creature\\Gryphon\\Gryphon.m2");
|
||||
wyvernDisplayId_ = resolveDisplayIdForExactPath("Creature\\Wyvern\\Wyvern.m2");
|
||||
LOG_INFO("Taxi mount displayIds: gryphon=", gryphonDisplayId_, " wyvern=", wyvernDisplayId_);
|
||||
|
||||
// CharHairGeosets.dbc: maps (race, sex, hairStyleId) → skinSectionId for hair mesh
|
||||
// Col 0: ID, Col 1: RaceID, Col 2: SexID, Col 3: VariationID, Col 4: GeosetID, Col 5: Showscalp
|
||||
if (auto chg = assetManager->loadDBC("CharHairGeosets.dbc"); chg && chg->isLoaded()) {
|
||||
|
|
@ -1705,8 +1791,19 @@ void Application::buildCreatureDisplayLookups() {
|
|||
}
|
||||
|
||||
std::string Application::getModelPathForDisplayId(uint32_t displayId) const {
|
||||
if (displayId == 30412) return "Creature\\Gryphon\\Gryphon.m2";
|
||||
if (displayId == 30413) return "Creature\\Wyvern\\Wyvern.m2";
|
||||
auto itData = displayDataMap_.find(displayId);
|
||||
if (itData == displayDataMap_.end()) return "";
|
||||
if (itData == displayDataMap_.end()) {
|
||||
// Some sources (e.g., taxi nodes) may provide a modelId directly.
|
||||
auto itPath = modelIdToPath_.find(displayId);
|
||||
if (itPath != modelIdToPath_.end()) {
|
||||
return itPath->second;
|
||||
}
|
||||
if (displayId == 30412) return "Creature\\Gryphon\\Gryphon.m2";
|
||||
if (displayId == 30413) return "Creature\\Wyvern\\Wyvern.m2";
|
||||
return "";
|
||||
}
|
||||
|
||||
auto itPath = modelIdToPath_.find(itData->second.modelId);
|
||||
if (itPath == modelIdToPath_.end()) return "";
|
||||
|
|
@ -2507,6 +2604,7 @@ void Application::processPendingMount() {
|
|||
if (lastSlash != std::string::npos) {
|
||||
modelDir = m2Path.substr(0, lastSlash + 1);
|
||||
}
|
||||
int replaced = 0;
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
const auto& tex = md->textures[ti];
|
||||
std::string texPath;
|
||||
|
|
@ -2521,9 +2619,19 @@ void Application::processPendingMount() {
|
|||
GLuint skinTex = charRenderer->loadTexture(texPath);
|
||||
if (skinTex != 0) {
|
||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), skinTex);
|
||||
replaced++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Some mounts (gryphon/wyvern) use empty model textures; force skin1 onto slot 0.
|
||||
if (replaced == 0 && !dispData.skin1.empty() && !md->textures.empty()) {
|
||||
std::string texPath = modelDir + dispData.skin1 + ".blp";
|
||||
GLuint skinTex = charRenderer->loadTexture(texPath);
|
||||
if (skinTex != 0) {
|
||||
charRenderer->setModelTexture(modelId, 0, skinTex);
|
||||
LOG_INFO("Forced mount skin1 texture on slot 0: ", texPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ GameHandler::GameHandler() {
|
|||
|
||||
// Default spells always available
|
||||
knownSpells.push_back(6603); // Attack
|
||||
knownSpells.push_back(8690); // Hearthstone
|
||||
|
||||
// Default action bar layout
|
||||
actionBar[0].type = ActionBarSlot::SPELL;
|
||||
actionBar[0].id = 6603; // Attack in slot 1
|
||||
actionBar[11].type = ActionBarSlot::ITEM;
|
||||
actionBar[11].id = 6948; // Hearthstone item in slot 12
|
||||
actionBar[11].type = ActionBarSlot::SPELL;
|
||||
actionBar[11].id = 8690; // Hearthstone in slot 12
|
||||
}
|
||||
|
||||
GameHandler::~GameHandler() {
|
||||
|
|
@ -96,6 +97,11 @@ bool GameHandler::connect(const std::string& host,
|
|||
}
|
||||
|
||||
void GameHandler::disconnect() {
|
||||
if (onTaxiFlight_) {
|
||||
taxiRecoverPending_ = true;
|
||||
} else {
|
||||
taxiRecoverPending_ = false;
|
||||
}
|
||||
if (socket) {
|
||||
socket->disconnect();
|
||||
socket.reset();
|
||||
|
|
@ -178,16 +184,84 @@ void GameHandler::update(float deltaTime) {
|
|||
|
||||
// Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared
|
||||
if (onTaxiFlight_) {
|
||||
updateClientTaxi(deltaTime);
|
||||
if (!taxiMountActive_ && !taxiClientActive_ && taxiClientPath_.empty()) {
|
||||
onTaxiFlight_ = false;
|
||||
LOG_INFO("Cleared stale taxi state in update");
|
||||
}
|
||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
if (playerEntity && playerEntity->getType() == ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<Unit>(playerEntity);
|
||||
if ((unit->getUnitFlags() & 0x00000100) == 0) {
|
||||
onTaxiFlight_ = false;
|
||||
if (taxiMountActive_ && mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
currentMountDisplayId_ = 0;
|
||||
taxiClientActive_ = false;
|
||||
taxiClientPath_.clear();
|
||||
taxiRecoverPending_ = false;
|
||||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
if (socket) {
|
||||
sendMovement(Opcode::CMSG_MOVE_STOP);
|
||||
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
LOG_INFO("Taxi flight landed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: if taxi flight ended but mount is still active, force dismount.
|
||||
if (!onTaxiFlight_ && taxiMountActive_) {
|
||||
if (mountCallback_) mountCallback_(0);
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
currentMountDisplayId_ = 0;
|
||||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
if (socket) {
|
||||
sendMovement(Opcode::CMSG_MOVE_STOP);
|
||||
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
LOG_INFO("Taxi dismount cleanup");
|
||||
}
|
||||
|
||||
if (taxiRecoverPending_ && state == WorldState::IN_WORLD) {
|
||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
if (playerEntity) {
|
||||
playerEntity->setPosition(taxiRecoverPos_.x, taxiRecoverPos_.y,
|
||||
taxiRecoverPos_.z, movementInfo.orientation);
|
||||
movementInfo.x = taxiRecoverPos_.x;
|
||||
movementInfo.y = taxiRecoverPos_.y;
|
||||
movementInfo.z = taxiRecoverPos_.z;
|
||||
if (socket) {
|
||||
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
taxiRecoverPending_ = false;
|
||||
LOG_INFO("Taxi recovery applied");
|
||||
}
|
||||
}
|
||||
|
||||
if (taxiActivatePending_) {
|
||||
taxiActivateTimer_ += deltaTime;
|
||||
if (!onTaxiFlight_ && taxiActivateTimer_ > 5.0f) {
|
||||
taxiActivatePending_ = false;
|
||||
taxiActivateTimer_ = 0.0f;
|
||||
if (taxiMountActive_ && mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
taxiClientActive_ = false;
|
||||
taxiClientPath_.clear();
|
||||
onTaxiFlight_ = false;
|
||||
LOG_WARNING("Taxi activation timed out");
|
||||
}
|
||||
}
|
||||
|
||||
// Leave combat if auto-attack target is too far away (leash range)
|
||||
if (autoAttacking && autoAttackTarget != 0) {
|
||||
auto targetEntity = entityManager.getEntity(autoAttackTarget);
|
||||
|
|
@ -744,12 +818,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_NEW_WORLD:
|
||||
handleNewWorld(packet);
|
||||
break;
|
||||
case Opcode::SMSG_TRANSFER_ABORTED: {
|
||||
uint32_t mapId = packet.readUInt32();
|
||||
uint8_t reason = (packet.getReadPos() < packet.getSize()) ? packet.readUInt8() : 0;
|
||||
LOG_WARNING("SMSG_TRANSFER_ABORTED: mapId=", mapId, " reason=", (int)reason);
|
||||
addSystemChatMessage("Transfer aborted.");
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Taxi / Flight Paths ----
|
||||
case Opcode::SMSG_SHOWTAXINODES:
|
||||
handleShowTaxiNodes(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ACTIVATETAXIREPLY:
|
||||
case Opcode::SMSG_ACTIVATETAXIREPLY_ALT:
|
||||
handleActivateTaxiReply(packet);
|
||||
break;
|
||||
case Opcode::SMSG_NEW_TAXI_PATH:
|
||||
|
|
@ -1086,6 +1171,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
|||
LOG_INFO("Level ", (int)character.level, " ",
|
||||
getRaceName(character.race), " ",
|
||||
getClassName(character.characterClass));
|
||||
playerRace_ = character.race;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1188,6 +1274,20 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
if (worldEntryCallback_) {
|
||||
worldEntryCallback_(data.mapId, data.x, data.y, data.z);
|
||||
}
|
||||
|
||||
// If we disconnected mid-taxi, attempt to recover to destination after login.
|
||||
if (taxiRecoverPending_ && taxiRecoverMapId_ == data.mapId) {
|
||||
float dx = movementInfo.x - taxiRecoverPos_.x;
|
||||
float dy = movementInfo.y - taxiRecoverPos_.y;
|
||||
float dz = movementInfo.z - taxiRecoverPos_.z;
|
||||
float dist = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (dist > 5.0f) {
|
||||
// Keep pending until player entity exists; update() will apply.
|
||||
LOG_INFO("Taxi recovery pending: dist=", dist);
|
||||
} else {
|
||||
taxiRecoverPending_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAccountDataTimes(network::Packet& packet) {
|
||||
|
|
@ -1265,7 +1365,15 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
}
|
||||
|
||||
// Block movement during taxi flight
|
||||
if (onTaxiFlight_) return;
|
||||
if (onTaxiFlight_) {
|
||||
// If taxi visuals are already gone, clear taxi state to avoid stuck movement.
|
||||
if (!taxiMountActive_ && !taxiClientActive_ && taxiClientPath_.empty()) {
|
||||
onTaxiFlight_ = false;
|
||||
LOG_INFO("Cleared stale taxi state in sendMovement");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (resurrectPending_) return;
|
||||
|
||||
// Use real millisecond timestamp (server validates for anti-cheat)
|
||||
|
|
@ -1497,6 +1605,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
default: break;
|
||||
}
|
||||
}
|
||||
if (block.guid == playerGuid) {
|
||||
constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100;
|
||||
if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_) {
|
||||
onTaxiFlight_ = true;
|
||||
applyTaxiMountForCurrentNode();
|
||||
}
|
||||
}
|
||||
if (block.guid == playerGuid &&
|
||||
(unit->getDynamicFlags() & UNIT_DYNFLAG_DEAD) != 0) {
|
||||
playerDead_ = true;
|
||||
|
|
@ -1776,6 +1891,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
|
||||
entity->setPosition(pos.x, pos.y, pos.z, block.orientation);
|
||||
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
|
||||
if (block.guid == playerGuid) {
|
||||
movementInfo.x = pos.x;
|
||||
movementInfo.y = pos.y;
|
||||
movementInfo.z = pos.z;
|
||||
movementInfo.orientation = block.orientation;
|
||||
}
|
||||
|
||||
// Fire transport move callback if this is a known transport
|
||||
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
|
||||
|
|
@ -3746,6 +3867,12 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
|||
|
||||
if (casting) return; // Already casting
|
||||
|
||||
// Hearthstone is item-bound; use the item rather than direct spell cast.
|
||||
if (spellId == 8690) {
|
||||
useItemById(6948);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid;
|
||||
auto packet = CastSpellPacket::build(spellId, target, ++castCount);
|
||||
socket->send(packet);
|
||||
|
|
@ -3787,10 +3914,13 @@ void GameHandler::handleInitialSpells(network::Packet& packet) {
|
|||
|
||||
knownSpells = data.spellIds;
|
||||
|
||||
// Ensure Attack (6603) is always present
|
||||
// Ensure Attack (6603) and Hearthstone (8690) are always present
|
||||
if (std::find(knownSpells.begin(), knownSpells.end(), 6603u) == knownSpells.end()) {
|
||||
knownSpells.insert(knownSpells.begin(), 6603u);
|
||||
}
|
||||
if (std::find(knownSpells.begin(), knownSpells.end(), 8690u) == knownSpells.end()) {
|
||||
knownSpells.push_back(8690u);
|
||||
}
|
||||
|
||||
// Set initial cooldowns
|
||||
for (const auto& cd : data.cooldowns) {
|
||||
|
|
@ -3799,11 +3929,11 @@ void GameHandler::handleInitialSpells(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
// Load saved action bar or use defaults (Attack slot 1, Hearthstone item slot 12)
|
||||
// Load saved action bar or use defaults (Attack slot 1, Hearthstone slot 12)
|
||||
actionBar[0].type = ActionBarSlot::SPELL;
|
||||
actionBar[0].id = 6603; // Attack
|
||||
actionBar[11].type = ActionBarSlot::ITEM;
|
||||
actionBar[11].id = 6948; // Hearthstone item
|
||||
actionBar[11].type = ActionBarSlot::SPELL;
|
||||
actionBar[11].id = 8690; // Hearthstone
|
||||
loadCharacterConfig();
|
||||
|
||||
LOG_INFO("Learned ", knownSpells.size(), " spells");
|
||||
|
|
@ -4541,6 +4671,54 @@ void GameHandler::handleTeleportAck(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleNewWorld(network::Packet& packet) {
|
||||
// SMSG_NEW_WORLD: uint32 mapId, float x, y, z, orientation
|
||||
if (packet.getSize() - packet.getReadPos() < 20) {
|
||||
LOG_WARNING("SMSG_NEW_WORLD too short");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t mapId = packet.readUInt32();
|
||||
float serverX = packet.readFloat();
|
||||
float serverY = packet.readFloat();
|
||||
float serverZ = packet.readFloat();
|
||||
float orientation = packet.readFloat();
|
||||
|
||||
LOG_INFO("SMSG_NEW_WORLD: mapId=", mapId,
|
||||
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
||||
" orient=", orientation);
|
||||
|
||||
currentMapId_ = mapId;
|
||||
|
||||
// Update player position
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
||||
movementInfo.x = canonical.x;
|
||||
movementInfo.y = canonical.y;
|
||||
movementInfo.z = canonical.z;
|
||||
movementInfo.orientation = orientation;
|
||||
movementInfo.flags = 0;
|
||||
|
||||
// Clear world state for the new map
|
||||
entityManager.clear();
|
||||
hostileAttackers_.clear();
|
||||
stopAutoAttack();
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
|
||||
// Send MSG_MOVE_WORLDPORT_ACK to tell the server we're ready
|
||||
if (socket) {
|
||||
network::Packet ack(static_cast<uint16_t>(Opcode::MSG_MOVE_WORLDPORT_ACK));
|
||||
socket->send(ack);
|
||||
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK");
|
||||
}
|
||||
|
||||
// Reload terrain at new position
|
||||
if (worldEntryCallback_) {
|
||||
worldEntryCallback_(mapId, serverX, serverY, serverZ);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Taxi / Flight Path Handlers
|
||||
// ============================================================
|
||||
|
|
@ -4555,6 +4733,7 @@ void GameHandler::loadTaxiDbc() {
|
|||
// Load TaxiNodes.dbc: 0=ID, 1=mapId, 2=x, 3=y, 4=z, 5=name(enUS locale)
|
||||
auto nodesDbc = am->loadDBC("TaxiNodes.dbc");
|
||||
if (nodesDbc && nodesDbc->isLoaded()) {
|
||||
uint32_t fieldCount = nodesDbc->getFieldCount();
|
||||
for (uint32_t i = 0; i < nodesDbc->getRecordCount(); i++) {
|
||||
TaxiNode node;
|
||||
node.id = nodesDbc->getUInt32(i, 0);
|
||||
|
|
@ -4563,9 +4742,25 @@ void GameHandler::loadTaxiDbc() {
|
|||
node.y = nodesDbc->getFloat(i, 3);
|
||||
node.z = nodesDbc->getFloat(i, 4);
|
||||
node.name = nodesDbc->getString(i, 5);
|
||||
// TaxiNodes.dbc (3.3.5a): last two fields are mount display IDs (Alliance, Horde)
|
||||
if (fieldCount >= 24) {
|
||||
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, 22);
|
||||
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, 23);
|
||||
if (node.mountDisplayIdAlliance == 0 && node.mountDisplayIdHorde == 0 && fieldCount >= 22) {
|
||||
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, 20);
|
||||
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, 21);
|
||||
}
|
||||
}
|
||||
if (node.id > 0) {
|
||||
taxiNodes_[node.id] = std::move(node);
|
||||
}
|
||||
if (node.id == 195) {
|
||||
std::string fields;
|
||||
for (uint32_t f = 0; f < fieldCount; f++) {
|
||||
fields += std::to_string(f) + ":" + std::to_string(nodesDbc->getUInt32(i, f)) + " ";
|
||||
}
|
||||
LOG_INFO("TaxiNodes[195] fields: ", fields);
|
||||
}
|
||||
}
|
||||
LOG_INFO("Loaded ", taxiNodes_.size(), " taxi nodes from TaxiNodes.dbc");
|
||||
} else {
|
||||
|
|
@ -4626,9 +4821,146 @@ void GameHandler::handleShowTaxiNodes(network::Packet& packet) {
|
|||
taxiWindowOpen_ = true;
|
||||
gossipWindowOpen = false;
|
||||
buildTaxiCostMap();
|
||||
auto it = taxiNodes_.find(data.nearestNode);
|
||||
if (it != taxiNodes_.end()) {
|
||||
LOG_INFO("Taxi node ", data.nearestNode, " mounts: A=", it->second.mountDisplayIdAlliance,
|
||||
" H=", it->second.mountDisplayIdHorde);
|
||||
}
|
||||
LOG_INFO("Taxi window opened, nearest node=", data.nearestNode);
|
||||
}
|
||||
|
||||
void GameHandler::applyTaxiMountForCurrentNode() {
|
||||
if (taxiMountActive_ || !mountCallback_) return;
|
||||
auto it = taxiNodes_.find(currentTaxiData_.nearestNode);
|
||||
if (it == taxiNodes_.end()) return;
|
||||
|
||||
bool isAlliance = true;
|
||||
switch (playerRace_) {
|
||||
case Race::ORC:
|
||||
case Race::UNDEAD:
|
||||
case Race::TAUREN:
|
||||
case Race::TROLL:
|
||||
case Race::GOBLIN:
|
||||
case Race::BLOOD_ELF:
|
||||
isAlliance = false;
|
||||
break;
|
||||
default:
|
||||
isAlliance = true;
|
||||
break;
|
||||
}
|
||||
uint32_t mountId = isAlliance ? it->second.mountDisplayIdAlliance
|
||||
: it->second.mountDisplayIdHorde;
|
||||
if (mountId == 0) {
|
||||
mountId = isAlliance ? it->second.mountDisplayIdHorde
|
||||
: it->second.mountDisplayIdAlliance;
|
||||
}
|
||||
if (mountId == 0) {
|
||||
auto& app = core::Application::getInstance();
|
||||
uint32_t gryphonId = app.getGryphonDisplayId();
|
||||
uint32_t wyvernId = app.getWyvernDisplayId();
|
||||
if (isAlliance && gryphonId != 0) mountId = gryphonId;
|
||||
if (!isAlliance && wyvernId != 0) mountId = wyvernId;
|
||||
if (mountId == 0) {
|
||||
mountId = (isAlliance ? wyvernId : gryphonId);
|
||||
}
|
||||
}
|
||||
if (mountId == 0) {
|
||||
// Fallback: any non-zero mount display from the node.
|
||||
if (it->second.mountDisplayIdAlliance != 0) mountId = it->second.mountDisplayIdAlliance;
|
||||
else if (it->second.mountDisplayIdHorde != 0) mountId = it->second.mountDisplayIdHorde;
|
||||
}
|
||||
if (mountId == 0 || mountId == 541) {
|
||||
mountId = isAlliance ? 30412u : 30413u;
|
||||
}
|
||||
if (mountId != 0) {
|
||||
taxiMountDisplayId_ = mountId;
|
||||
taxiMountActive_ = true;
|
||||
LOG_INFO("Taxi mount apply: displayId=", mountId);
|
||||
mountCallback_(mountId);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::startClientTaxiPath(const std::vector<uint32_t>& pathNodes) {
|
||||
taxiClientPath_.clear();
|
||||
taxiClientIndex_ = 0;
|
||||
taxiClientActive_ = false;
|
||||
taxiClientSegmentProgress_ = 0.0f;
|
||||
|
||||
for (uint32_t nodeId : pathNodes) {
|
||||
auto it = taxiNodes_.find(nodeId);
|
||||
if (it == taxiNodes_.end()) continue;
|
||||
glm::vec3 serverPos(it->second.x, it->second.y, it->second.z);
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(serverPos);
|
||||
taxiClientPath_.push_back(canonical);
|
||||
}
|
||||
if (taxiClientPath_.size() < 2) return;
|
||||
|
||||
taxiClientActive_ = true;
|
||||
}
|
||||
|
||||
void GameHandler::updateClientTaxi(float deltaTime) {
|
||||
if (!taxiClientActive_ || taxiClientPath_.size() < 2) return;
|
||||
if (!entityManager.hasEntity(playerGuid)) return;
|
||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
if (!playerEntity) return;
|
||||
|
||||
if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) {
|
||||
taxiClientActive_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 start = taxiClientPath_[taxiClientIndex_];
|
||||
glm::vec3 end = taxiClientPath_[taxiClientIndex_ + 1];
|
||||
glm::vec3 dir = end - start;
|
||||
float segmentLen = glm::length(dir);
|
||||
if (segmentLen < 0.01f) {
|
||||
taxiClientIndex_++;
|
||||
taxiClientSegmentProgress_ = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
taxiClientSegmentProgress_ += taxiClientSpeed_ * deltaTime;
|
||||
float t = taxiClientSegmentProgress_ / segmentLen;
|
||||
if (t >= 1.0f) {
|
||||
taxiClientIndex_++;
|
||||
taxiClientSegmentProgress_ = 0.0f;
|
||||
if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) {
|
||||
taxiClientActive_ = false;
|
||||
onTaxiFlight_ = false;
|
||||
if (taxiMountActive_ && mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
taxiClientPath_.clear();
|
||||
taxiRecoverPending_ = false;
|
||||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
if (socket) {
|
||||
sendMovement(Opcode::CMSG_MOVE_STOP);
|
||||
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
LOG_INFO("Taxi flight landed (client path)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 dirNorm = dir / segmentLen;
|
||||
glm::vec3 nextPos = start + dirNorm * (t * segmentLen);
|
||||
|
||||
// Add a flight arc to avoid terrain collisions.
|
||||
float arcHeight = std::clamp(segmentLen * 0.15f, 20.0f, 120.0f);
|
||||
float arc = 4.0f * t * (1.0f - t);
|
||||
nextPos.z = glm::mix(start.z, end.z, t) + arcHeight * arc;
|
||||
|
||||
float orientation = std::atan2(dir.y, dir.x) - 1.57079632679f;
|
||||
playerEntity->setPosition(nextPos.x, nextPos.y, nextPos.z, orientation);
|
||||
movementInfo.x = nextPos.x;
|
||||
movementInfo.y = nextPos.y;
|
||||
movementInfo.z = nextPos.z;
|
||||
movementInfo.orientation = orientation;
|
||||
}
|
||||
|
||||
void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
|
||||
ActivateTaxiReplyData data;
|
||||
if (!ActivateTaxiReplyParser::parse(packet, data)) {
|
||||
|
|
@ -4639,10 +4971,23 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
|
|||
if (data.result == 0) {
|
||||
onTaxiFlight_ = true;
|
||||
taxiWindowOpen_ = false;
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
taxiActivatePending_ = false;
|
||||
taxiActivateTimer_ = 0.0f;
|
||||
applyTaxiMountForCurrentNode();
|
||||
LOG_INFO("Taxi flight started!");
|
||||
} else {
|
||||
LOG_WARNING("Taxi activation failed, result=", data.result);
|
||||
addSystemChatMessage("Cannot take that flight path.");
|
||||
taxiActivatePending_ = false;
|
||||
taxiActivateTimer_ = 0.0f;
|
||||
if (taxiMountActive_ && mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
onTaxiFlight_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4690,6 +5035,16 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
|
|||
uint32_t startNode = currentTaxiData_.nearestNode;
|
||||
if (startNode == 0 || destNodeId == 0 || startNode == destNodeId) return;
|
||||
|
||||
// If already mounted, dismount before starting a taxi flight.
|
||||
if (isMounted()) {
|
||||
LOG_INFO("Taxi activate: dismounting current mount");
|
||||
if (mountCallback_) mountCallback_(0);
|
||||
currentMountDisplayId_ = 0;
|
||||
dismount();
|
||||
}
|
||||
|
||||
addSystemChatMessage("Taxi: requesting flight...");
|
||||
|
||||
// BFS to find path from startNode to destNodeId
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> adj;
|
||||
for (const auto& edge : taxiPathEdges_) {
|
||||
|
|
@ -4740,12 +5095,34 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
|
|||
LOG_INFO("Taxi path nodes: ", pathStr);
|
||||
}
|
||||
|
||||
auto pkt = ActivateTaxiExpressPacket::build(taxiNpcGuid_, path);
|
||||
socket->send(pkt);
|
||||
uint32_t totalCost = getTaxiCostTo(destNodeId);
|
||||
LOG_INFO("Taxi activate: start=", startNode, " dest=", destNodeId, " cost=", totalCost);
|
||||
|
||||
// Fallback: some servers expect basic CMSG_ACTIVATETAXI.
|
||||
// Some servers only accept basic CMSG_ACTIVATETAXI.
|
||||
auto basicPkt = ActivateTaxiPacket::build(taxiNpcGuid_, startNode, destNodeId);
|
||||
socket->send(basicPkt);
|
||||
|
||||
// Others accept express with a full node path + cost.
|
||||
auto pkt = ActivateTaxiExpressPacket::build(taxiNpcGuid_, totalCost, path);
|
||||
socket->send(pkt);
|
||||
|
||||
// Optimistically start taxi visuals; server will correct if it denies.
|
||||
taxiActivatePending_ = true;
|
||||
taxiActivateTimer_ = 0.0f;
|
||||
if (!onTaxiFlight_) {
|
||||
onTaxiFlight_ = true;
|
||||
applyTaxiMountForCurrentNode();
|
||||
}
|
||||
startClientTaxiPath(path);
|
||||
|
||||
// Save recovery target in case of disconnect during taxi.
|
||||
auto destIt = taxiNodes_.find(destNodeId);
|
||||
if (destIt != taxiNodes_.end()) {
|
||||
taxiRecoverMapId_ = destIt->second.mapId;
|
||||
taxiRecoverPos_ = core::coords::serverToCanonical(
|
||||
glm::vec3(destIt->second.x, destIt->second.y, destIt->second.z));
|
||||
taxiRecoverPending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -2667,25 +2667,29 @@ bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data
|
|||
}
|
||||
|
||||
bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyData& data) {
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining >= 4) {
|
||||
data.result = packet.readUInt32();
|
||||
} else if (remaining >= 1) {
|
||||
data.result = packet.readUInt8();
|
||||
} else {
|
||||
LOG_ERROR("ActivateTaxiReplyParser: packet too short");
|
||||
return false;
|
||||
}
|
||||
data.result = packet.readUInt32();
|
||||
LOG_INFO("ActivateTaxiReply: result=", data.result);
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, const std::vector<uint32_t>& pathNodes) {
|
||||
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t totalCost, const std::vector<uint32_t>& pathNodes) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ACTIVATETAXIEXPRESS));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(0); // totalCost (server recalculates)
|
||||
packet.writeUInt32(totalCost);
|
||||
packet.writeUInt32(static_cast<uint32_t>(pathNodes.size()));
|
||||
for (uint32_t nodeId : pathNodes) {
|
||||
packet.writeUInt32(nodeId);
|
||||
}
|
||||
LOG_INFO("ActivateTaxiExpress: npc=0x", std::hex, npcGuid, std::dec,
|
||||
" nodes=", pathNodes.size());
|
||||
" cost=", totalCost, " nodes=", pathNodes.size());
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -266,27 +266,30 @@ void CameraController::update(float deltaTime) {
|
|||
if (thirdPerson && followTarget) {
|
||||
// Move the follow target (character position) instead of the camera
|
||||
glm::vec3 targetPos = *followTarget;
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->setCollisionFocus(targetPos, COLLISION_FOCUS_RADIUS_THIRD_PERSON);
|
||||
}
|
||||
if (m2Renderer) {
|
||||
m2Renderer->setCollisionFocus(targetPos, COLLISION_FOCUS_RADIUS_THIRD_PERSON);
|
||||
if (!externalFollow_) {
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->setCollisionFocus(targetPos, COLLISION_FOCUS_RADIUS_THIRD_PERSON);
|
||||
}
|
||||
if (m2Renderer) {
|
||||
m2Renderer->setCollisionFocus(targetPos, COLLISION_FOCUS_RADIUS_THIRD_PERSON);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for water at current position — simple submersion test.
|
||||
// If the player's feet are meaningfully below the water surface, swim.
|
||||
std::optional<float> waterH;
|
||||
if (waterRenderer) {
|
||||
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
|
||||
}
|
||||
bool inWater = waterH && (targetPos.z < (*waterH - 0.3f));
|
||||
// Keep swimming through water-data gaps (chunk boundaries).
|
||||
if (!inWater && swimming && !waterH) {
|
||||
inWater = true;
|
||||
}
|
||||
if (!externalFollow_) {
|
||||
// Check for water at current position — simple submersion test.
|
||||
// If the player's feet are meaningfully below the water surface, swim.
|
||||
std::optional<float> waterH;
|
||||
if (waterRenderer) {
|
||||
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
|
||||
}
|
||||
bool inWater = waterH && (targetPos.z < (*waterH - 0.3f));
|
||||
// Keep swimming through water-data gaps (chunk boundaries).
|
||||
if (!inWater && swimming && !waterH) {
|
||||
inWater = true;
|
||||
}
|
||||
|
||||
|
||||
if (inWater) {
|
||||
if (inWater) {
|
||||
swimming = true;
|
||||
// Swim movement follows look pitch (forward/back), while strafe stays
|
||||
// lateral for stable control.
|
||||
|
|
@ -406,7 +409,7 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
|
||||
grounded = false;
|
||||
} else {
|
||||
} else {
|
||||
// Exiting water — give a small upward boost to help climb onto shore.
|
||||
swimming = false;
|
||||
|
||||
|
|
@ -433,6 +436,12 @@ void CameraController::update(float deltaTime) {
|
|||
// Apply gravity
|
||||
verticalVelocity += gravity * deltaTime;
|
||||
targetPos.z += verticalVelocity * deltaTime;
|
||||
}
|
||||
} else {
|
||||
// External follow (e.g., taxi): trust server position without grounding.
|
||||
swimming = false;
|
||||
grounded = true;
|
||||
verticalVelocity = 0.0f;
|
||||
}
|
||||
|
||||
// Sweep collisions in small steps to reduce tunneling through thin walls/floors.
|
||||
|
|
@ -1286,9 +1295,18 @@ bool CameraController::isMoving() const {
|
|||
if (!enabled || !camera) {
|
||||
return false;
|
||||
}
|
||||
if (externalMoving_) return true;
|
||||
return moveForwardActive || moveBackwardActive || strafeLeftActive || strafeRightActive || autoRunning;
|
||||
}
|
||||
|
||||
void CameraController::clearMovementInputs() {
|
||||
moveForwardActive = false;
|
||||
moveBackwardActive = false;
|
||||
strafeLeftActive = false;
|
||||
strafeRightActive = false;
|
||||
autoRunning = false;
|
||||
}
|
||||
|
||||
bool CameraController::isSprinting() const {
|
||||
return enabled && camera && runPace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -694,7 +694,8 @@ void Renderer::updateCharacterAnimation() {
|
|||
if (moving && haveMountState && curMountDur > 1.0f) {
|
||||
float norm = std::fmod(curMountTime, curMountDur) / curMountDur;
|
||||
// One bounce per stride cycle
|
||||
mountBob = std::sin(norm * 2.0f * 3.14159f) * 0.12f;
|
||||
float bobSpeed = taxiFlight_ ? 2.0f : 1.0f;
|
||||
mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1190,7 +1191,7 @@ void Renderer::update(float deltaTime) {
|
|||
cameraController && cameraController->isThirdPerson() &&
|
||||
cameraController->isGrounded() && !cameraController->isSwimming();
|
||||
|
||||
if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0) {
|
||||
if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0 && !taxiFlight_) {
|
||||
// Mount footsteps: use mount's animation for timing
|
||||
uint32_t animId = 0;
|
||||
float animTimeMs = 0.0f, animDurationMs = 0.0f;
|
||||
|
|
|
|||
|
|
@ -3624,8 +3624,15 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
bool isSelected = (selectedNodeId == nodeId);
|
||||
if (ImGui::Selectable(node.name.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
if (ImGui::Selectable(node.name.c_str(), isSelected,
|
||||
ImGuiSelectableFlags_SpanAllColumns |
|
||||
ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
selectedNodeId = nodeId;
|
||||
LOG_INFO("Taxi UI: Selected dest=", nodeId);
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
LOG_INFO("Taxi UI: Double-click activate dest=", nodeId);
|
||||
gameHandler.activateTaxi(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
|
|
@ -3656,6 +3663,10 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (selectedNodeId != 0 && ImGui::Button("Fly Selected", ImVec2(-1, 0))) {
|
||||
LOG_INFO("Taxi UI: Fly Selected dest=", selectedNodeId);
|
||||
gameHandler.activateTaxi(selectedNodeId);
|
||||
}
|
||||
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
||||
gameHandler.closeTaxi();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue