mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 15:20:15 +00:00
Stabilize taxi/state sync and creature spawn handling
This commit is contained in:
parent
38cef8d9c6
commit
40b50454ce
18 changed files with 818 additions and 127 deletions
|
|
@ -162,6 +162,8 @@ private:
|
|||
uint32_t gryphonDisplayId_ = 0;
|
||||
uint32_t wyvernDisplayId_ = 0;
|
||||
bool lastTaxiFlight_ = false;
|
||||
float taxiLandingClampTimer_ = 0.0f;
|
||||
float worldEntryMovementGraceTimer_ = 0.0f;
|
||||
float taxiStreamCooldown_ = 0.0f;
|
||||
bool idleYawned_ = false;
|
||||
|
||||
|
|
@ -193,7 +195,7 @@ private:
|
|||
float x, y, z, orientation;
|
||||
};
|
||||
std::vector<PendingCreatureSpawn> pendingCreatureSpawns_;
|
||||
static constexpr int MAX_SPAWNS_PER_FRAME = 24;
|
||||
static constexpr int MAX_SPAWNS_PER_FRAME = 96;
|
||||
static constexpr uint16_t MAX_CREATURE_SPAWN_RETRIES = 300;
|
||||
std::unordered_set<uint64_t> pendingCreatureSpawnGuids_;
|
||||
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public:
|
|||
size_t getAvailableRAM() const;
|
||||
|
||||
/**
|
||||
* Get recommended cache budget (30% of available RAM)
|
||||
* Get recommended cache budget (80% of available RAM, capped at 90% of total RAM)
|
||||
*/
|
||||
size_t getRecommendedCacheBudget() const;
|
||||
|
||||
|
|
|
|||
|
|
@ -623,6 +623,8 @@ public:
|
|||
void activateTaxi(uint32_t destNodeId);
|
||||
bool isOnTaxiFlight() const { return onTaxiFlight_; }
|
||||
bool isTaxiMountActive() const { return taxiMountActive_; }
|
||||
bool isTaxiActivationPending() const { return taxiActivatePending_; }
|
||||
void forceClearTaxiAndMovementState();
|
||||
const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; }
|
||||
uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; }
|
||||
|
||||
|
|
@ -927,6 +929,8 @@ private:
|
|||
uint32_t pingSequence = 0; // Ping sequence number (increments)
|
||||
float timeSinceLastPing = 0.0f; // Time since last ping sent (seconds)
|
||||
float pingInterval = 30.0f; // Ping interval (30 seconds)
|
||||
float timeSinceLastMoveHeartbeat_ = 0.0f; // Periodic movement heartbeat to keep server position synced
|
||||
float moveHeartbeatInterval_ = 0.5f;
|
||||
uint32_t lastLatency = 0; // Last measured latency (milliseconds)
|
||||
|
||||
// Player GUID and map
|
||||
|
|
@ -1086,6 +1090,7 @@ private:
|
|||
float taxiActivateTimer_ = 0.0f;
|
||||
bool taxiClientActive_ = false;
|
||||
float taxiLandingCooldown_ = 0.0f; // Prevent re-entering taxi right after landing
|
||||
float taxiStartGrace_ = 0.0f; // Ignore transient landing/dismount checks right after takeoff
|
||||
size_t taxiClientIndex_ = 0;
|
||||
std::vector<glm::vec3> taxiClientPath_;
|
||||
float taxiClientSpeed_ = 32.0f;
|
||||
|
|
|
|||
|
|
@ -433,6 +433,9 @@ struct MovementInfo {
|
|||
*/
|
||||
class MovementPacket {
|
||||
public:
|
||||
static void writePackedGuid(network::Packet& packet, uint64_t guid);
|
||||
static void writeMovementPayload(network::Packet& packet, const MovementInfo& info);
|
||||
|
||||
/**
|
||||
* Build a movement packet
|
||||
*
|
||||
|
|
|
|||
|
|
@ -172,6 +172,15 @@ private:
|
|||
std::optional<float> cachedCamWmoFloor;
|
||||
bool hasCachedCamFloor = false;
|
||||
|
||||
// Terrain-aware camera pivot lift cache (throttled for performance).
|
||||
glm::vec3 lastPivotLiftQueryPos_ = glm::vec3(0.0f);
|
||||
float lastPivotLiftDistance_ = 0.0f;
|
||||
int pivotLiftQueryCounter_ = 0;
|
||||
float cachedPivotLift_ = 0.0f;
|
||||
static constexpr int PIVOT_LIFT_QUERY_INTERVAL = 3;
|
||||
static constexpr float PIVOT_LIFT_POS_THRESHOLD = 0.5f;
|
||||
static constexpr float PIVOT_LIFT_DIST_THRESHOLD = 0.5f;
|
||||
|
||||
// Cached floor height queries (update every 5 frames or 2 unit movement)
|
||||
glm::vec3 lastFloorQueryPos = glm::vec3(0.0f);
|
||||
std::optional<float> cachedFloorHeight;
|
||||
|
|
|
|||
4
restart-worldserver.sh
Normal file
4
restart-worldserver.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
docker restart ac-worldserver
|
||||
|
|
@ -420,6 +420,52 @@ void Application::update(float deltaTime) {
|
|||
auto cq2 = std::chrono::high_resolution_clock::now();
|
||||
creatureQTime += std::chrono::duration<float, std::milli>(cq2 - cq1).count();
|
||||
|
||||
// Self-heal missing creature visuals: if a nearby UNIT exists in
|
||||
// entity state but has no render instance, queue a spawn retry.
|
||||
if (gameHandler) {
|
||||
static float creatureResyncTimer = 0.0f;
|
||||
creatureResyncTimer += deltaTime;
|
||||
if (creatureResyncTimer >= 1.0f) {
|
||||
creatureResyncTimer = 0.0f;
|
||||
|
||||
glm::vec3 playerPos(0.0f);
|
||||
bool havePlayerPos = false;
|
||||
uint64_t playerGuid = gameHandler->getPlayerGuid();
|
||||
if (auto playerEntity = gameHandler->getEntityManager().getEntity(playerGuid)) {
|
||||
playerPos = glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ());
|
||||
havePlayerPos = true;
|
||||
}
|
||||
|
||||
const float kResyncRadiusSq = 260.0f * 260.0f;
|
||||
for (const auto& pair : gameHandler->getEntityManager().getEntities()) {
|
||||
uint64_t guid = pair.first;
|
||||
const auto& entity = pair.second;
|
||||
if (!entity || guid == playerGuid) continue;
|
||||
if (entity->getType() != game::ObjectType::UNIT) continue;
|
||||
auto unit = std::dynamic_pointer_cast<game::Unit>(entity);
|
||||
if (!unit || unit->getDisplayId() == 0) continue;
|
||||
if (creatureInstances_.count(guid) || pendingCreatureSpawnGuids_.count(guid)) continue;
|
||||
|
||||
if (havePlayerPos) {
|
||||
glm::vec3 pos(unit->getX(), unit->getY(), unit->getZ());
|
||||
glm::vec3 delta = pos - playerPos;
|
||||
float distSq = glm::dot(delta, delta);
|
||||
if (distSq > kResyncRadiusSq) continue;
|
||||
}
|
||||
|
||||
PendingCreatureSpawn retrySpawn{};
|
||||
retrySpawn.guid = guid;
|
||||
retrySpawn.displayId = unit->getDisplayId();
|
||||
retrySpawn.x = unit->getX();
|
||||
retrySpawn.y = unit->getY();
|
||||
retrySpawn.z = unit->getZ();
|
||||
retrySpawn.orientation = unit->getOrientation();
|
||||
pendingCreatureSpawns_.push_back(retrySpawn);
|
||||
pendingCreatureSpawnGuids_.insert(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto goq1 = std::chrono::high_resolution_clock::now();
|
||||
processGameObjectSpawnQueue();
|
||||
auto goq2 = std::chrono::high_resolution_clock::now();
|
||||
|
|
@ -447,16 +493,67 @@ void Application::update(float deltaTime) {
|
|||
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
||||
}
|
||||
|
||||
bool onTaxi = gameHandler && (gameHandler->isOnTaxiFlight() || gameHandler->isTaxiMountActive());
|
||||
bool onTaxi = gameHandler &&
|
||||
(gameHandler->isOnTaxiFlight() ||
|
||||
gameHandler->isTaxiMountActive() ||
|
||||
gameHandler->isTaxiActivationPending());
|
||||
if (worldEntryMovementGraceTimer_ > 0.0f) {
|
||||
worldEntryMovementGraceTimer_ -= deltaTime;
|
||||
}
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->setExternalFollow(onTaxi);
|
||||
renderer->getCameraController()->setExternalMoving(onTaxi);
|
||||
if (onTaxi) {
|
||||
// Drop any stale local movement toggles while server drives taxi motion.
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
}
|
||||
if (lastTaxiFlight_ && !onTaxi) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
// Keep clamping for a short grace window after taxi ends to avoid
|
||||
// falling through WMOs while floor/collision state settles.
|
||||
taxiLandingClampTimer_ = 1.5f;
|
||||
}
|
||||
if (!onTaxi &&
|
||||
worldEntryMovementGraceTimer_ <= 0.0f &&
|
||||
!gameHandler->isMounted() &&
|
||||
taxiLandingClampTimer_ > 0.0f) {
|
||||
taxiLandingClampTimer_ -= deltaTime;
|
||||
if (renderer && gameHandler) {
|
||||
glm::vec3 p = renderer->getCharacterPosition();
|
||||
std::optional<float> terrainFloor;
|
||||
std::optional<float> wmoFloor;
|
||||
std::optional<float> m2Floor;
|
||||
if (renderer->getTerrainManager()) {
|
||||
terrainFloor = renderer->getTerrainManager()->getHeightAt(p.x, p.y);
|
||||
}
|
||||
if (renderer->getWMORenderer()) {
|
||||
// Probe from above so we can recover when current Z is already below floor.
|
||||
wmoFloor = renderer->getWMORenderer()->getFloorHeight(p.x, p.y, p.z + 40.0f);
|
||||
}
|
||||
if (renderer->getM2Renderer()) {
|
||||
// Include M2 floors (bridges/platforms) in landing recovery.
|
||||
m2Floor = renderer->getM2Renderer()->getFloorHeight(p.x, p.y, p.z + 40.0f);
|
||||
}
|
||||
|
||||
std::optional<float> targetFloor;
|
||||
if (terrainFloor) targetFloor = terrainFloor;
|
||||
if (wmoFloor && (!targetFloor || *wmoFloor > *targetFloor)) targetFloor = wmoFloor;
|
||||
if (m2Floor && (!targetFloor || *m2Floor > *targetFloor)) targetFloor = m2Floor;
|
||||
|
||||
if (targetFloor) {
|
||||
float targetZ = *targetFloor + 0.10f;
|
||||
// Only lift upward to prevent sinking through floors/bridges.
|
||||
// Never force the player downward from a valid elevated surface.
|
||||
if (p.z < targetZ - 0.05f) {
|
||||
p.z = targetZ;
|
||||
renderer->getCharacterPosition() = p;
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(p);
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bool idleOrbit = renderer->getCameraController()->isIdleOrbit();
|
||||
if (idleOrbit && !idleYawned_ && renderer) {
|
||||
|
|
@ -494,10 +591,27 @@ void Application::update(float deltaTime) {
|
|||
|
||||
if (onTaxi) {
|
||||
auto playerEntity = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid());
|
||||
glm::vec3 canonical(0.0f);
|
||||
bool haveCanonical = false;
|
||||
if (playerEntity) {
|
||||
glm::vec3 canonical(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ());
|
||||
canonical = glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ());
|
||||
haveCanonical = true;
|
||||
} else {
|
||||
// Fallback for brief entity gaps during taxi start/updates:
|
||||
// movementInfo is still updated by client taxi simulation.
|
||||
const auto& move = gameHandler->getMovementInfo();
|
||||
canonical = glm::vec3(move.x, move.y, move.z);
|
||||
haveCanonical = true;
|
||||
}
|
||||
if (haveCanonical) {
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
renderer->getCharacterPosition() = renderPos;
|
||||
if (renderer->getCameraController()) {
|
||||
glm::vec3* followTarget = renderer->getCameraController()->getFollowTargetMutable();
|
||||
if (followTarget) {
|
||||
*followTarget = renderPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (onTransport) {
|
||||
// Transport mode: compose world position from transport transform + local offset
|
||||
|
|
@ -523,16 +637,16 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// Send movement heartbeat every 500ms (keeps server position in sync)
|
||||
// Skip periodic taxi heartbeats; taxi start sends explicit heartbeats already.
|
||||
if (gameHandler && renderer && !onTaxi) {
|
||||
// Send periodic movement heartbeats.
|
||||
// Keep them active during taxi as well to avoid occasional server-side
|
||||
// flight stalls waiting for movement sync updates.
|
||||
if (gameHandler && renderer) {
|
||||
movementHeartbeatTimer += deltaTime;
|
||||
if (movementHeartbeatTimer >= 0.5f) {
|
||||
float hbInterval = onTaxi ? 0.25f : 0.5f;
|
||||
if (movementHeartbeatTimer >= hbInterval) {
|
||||
movementHeartbeatTimer = 0.0f;
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
} else {
|
||||
movementHeartbeatTimer = 0.0f;
|
||||
}
|
||||
|
||||
auto sync2 = std::chrono::high_resolution_clock::now();
|
||||
|
|
@ -540,7 +654,7 @@ void Application::update(float deltaTime) {
|
|||
|
||||
// Log profiling every 60 frames
|
||||
if (++appProfileCounter >= 60) {
|
||||
LOG_INFO("APP UPDATE PROFILE (60 frames): gameHandler=", ghTime / 60.0f,
|
||||
LOG_DEBUG("APP UPDATE PROFILE (60 frames): gameHandler=", ghTime / 60.0f,
|
||||
"ms world=", worldTime / 60.0f, "ms spawn=", spawnTime / 60.0f,
|
||||
"ms creatureQ=", creatureQTime / 60.0f, "ms goQ=", goQTime / 60.0f,
|
||||
"ms mount=", mountTime / 60.0f, "ms npcMgr=", npcMgrTime / 60.0f,
|
||||
|
|
@ -578,7 +692,7 @@ void Application::update(float deltaTime) {
|
|||
uiTime += std::chrono::duration<float, std::milli>(u2 - u1).count();
|
||||
|
||||
if (state == AppState::IN_GAME && ++rendererProfileCounter >= 60) {
|
||||
LOG_INFO("RENDERER/UI PROFILE (60 frames): renderer=", rendererTime / 60.0f,
|
||||
LOG_DEBUG("RENDERER/UI PROFILE (60 frames): renderer=", rendererTime / 60.0f,
|
||||
"ms ui=", uiTime / 60.0f, "ms");
|
||||
rendererProfileCounter = 0;
|
||||
rendererTime = uiTime = 0.0f;
|
||||
|
|
@ -690,23 +804,129 @@ void Application::setupUICallbacks() {
|
|||
// World entry callback (online mode) - load terrain when entering world
|
||||
gameHandler->setWorldEntryCallback([this](uint32_t mapId, float x, float y, float z) {
|
||||
LOG_INFO("Online world entry: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")");
|
||||
worldEntryMovementGraceTimer_ = 2.0f;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
loadOnlineWorldTerrain(mapId, x, y, z);
|
||||
});
|
||||
|
||||
// /unstuck — snap upward 10m to escape minor WMO cracks
|
||||
gameHandler->setUnstuckCallback([this]() {
|
||||
auto sampleBestFloorAt = [this](float x, float y, float probeZ) -> std::optional<float> {
|
||||
std::optional<float> terrainFloor;
|
||||
std::optional<float> wmoFloor;
|
||||
std::optional<float> m2Floor;
|
||||
|
||||
if (renderer && renderer->getTerrainManager()) {
|
||||
terrainFloor = renderer->getTerrainManager()->getHeightAt(x, y);
|
||||
}
|
||||
if (renderer && renderer->getWMORenderer()) {
|
||||
wmoFloor = renderer->getWMORenderer()->getFloorHeight(x, y, probeZ);
|
||||
}
|
||||
if (renderer && 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;
|
||||
};
|
||||
|
||||
auto clearStuckMovement = [this]() {
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
}
|
||||
if (gameHandler) {
|
||||
gameHandler->forceClearTaxiAndMovementState();
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_STOP);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_STOP_STRAFE);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_STOP_TURN);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_STOP_SWIM);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
};
|
||||
|
||||
auto syncTeleportedPositionToServer = [this](const glm::vec3& renderPos) {
|
||||
if (!gameHandler) return;
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_STOP);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_STOP_STRAFE);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_STOP_TURN);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
};
|
||||
|
||||
auto forceServerTeleportCommand = [this](const glm::vec3& renderPos) {
|
||||
if (!gameHandler) return;
|
||||
// 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(), "");
|
||||
};
|
||||
|
||||
// /unstuck — prefer safe position or nearest floor, avoid blind +Z snaps.
|
||||
gameHandler->setUnstuckCallback([this, sampleBestFloorAt, clearStuckMovement, syncTeleportedPositionToServer, forceServerTeleportCommand]() {
|
||||
if (!renderer || !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;
|
||||
pos.z += 10.0f;
|
||||
if (cc->hasLastSafePosition()) {
|
||||
pos = cc->getLastSafePosition();
|
||||
pos.z += 1.5f;
|
||||
cc->teleportTo(pos);
|
||||
syncTeleportedPositionToServer(pos);
|
||||
forceServerTeleportCommand(pos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: teleported to last safe position");
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto floor = sampleBestFloorAt(pos.x, pos.y, pos.z + 60.0f)) {
|
||||
pos.z = *floor + 0.2f;
|
||||
} else {
|
||||
pos.z += 20.0f;
|
||||
}
|
||||
|
||||
// Nudge forward slightly to break edge-cases where unstuck lands exactly
|
||||
// on problematic collision seams.
|
||||
if (gameHandler) {
|
||||
float renderYaw = gameHandler->getMovementInfo().orientation + glm::radians(90.0f);
|
||||
pos.x += std::cos(renderYaw) * 2.0f;
|
||||
pos.y += std::sin(renderYaw) * 2.0f;
|
||||
}
|
||||
|
||||
cc->teleportTo(pos);
|
||||
syncTeleportedPositionToServer(pos);
|
||||
forceServerTeleportCommand(pos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: recovered to sampled floor");
|
||||
});
|
||||
|
||||
// /unstuckgy — snap upward 50m to clear all WMO geometry, gravity re-settles onto terrain
|
||||
gameHandler->setUnstuckGyCallback([this]() {
|
||||
// /unstuckgy — stronger recovery: safe/home position, then sampled floor fallback.
|
||||
gameHandler->setUnstuckGyCallback([this, sampleBestFloorAt, clearStuckMovement, syncTeleportedPositionToServer, forceServerTeleportCommand]() {
|
||||
if (!renderer || !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;
|
||||
|
|
@ -716,15 +936,45 @@ void Application::setupUICallbacks() {
|
|||
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;
|
||||
}
|
||||
|
||||
// No safe position — snap 50m upward to clear all WMO geometry
|
||||
uint32_t bindMap = 0;
|
||||
glm::vec3 bindPos(0.0f);
|
||||
if (gameHandler && 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;
|
||||
pos.z += 50.0f;
|
||||
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);
|
||||
LOG_INFO("Unstuck: snapped 50m upward");
|
||||
syncTeleportedPositionToServer(pos);
|
||||
forceServerTeleportCommand(pos);
|
||||
clearStuckMovement();
|
||||
LOG_INFO("Unstuck: high fallback snap");
|
||||
});
|
||||
|
||||
// Auto-unstuck: falling for > 5 seconds = void fall, teleport to map entry
|
||||
|
|
@ -2295,19 +2545,23 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Skip if already spawned
|
||||
if (creatureInstances_.count(guid)) return;
|
||||
|
||||
if (nonRenderableCreatureDisplayIds_.count(displayId)) {
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get model path from displayId
|
||||
std::string m2Path = getModelPathForDisplayId(displayId);
|
||||
if (m2Path.empty()) {
|
||||
LOG_WARNING("No model path for displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")");
|
||||
nonRenderableCreatureDisplayIds_.insert(displayId);
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
{
|
||||
// Intentionally invisible helper creatures should not consume retry budget.
|
||||
std::string lowerPath = m2Path;
|
||||
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowerPath.find("invisiblestalker") != std::string::npos ||
|
||||
lowerPath.find("invisible_stalker") != std::string::npos) {
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
|
||||
|
|
@ -2325,15 +2579,12 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("Failed to read creature M2: ", m2Path);
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty()) {
|
||||
LOG_WARNING("Failed to parse creature M2: ", m2Path);
|
||||
nonRenderableCreatureDisplayIds_.insert(displayId);
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2360,7 +2611,6 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
|
||||
if (!charRenderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("Failed to load creature model: ", m2Path);
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2502,28 +2752,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Convert canonical → render coordinates
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
|
||||
// Smart filtering for bad spawn data:
|
||||
// - If over ocean AND at continental height (Z > 50): bad data, skip
|
||||
// - If over ocean AND near sea level (Z <= 50): water creature, allow
|
||||
// - If over land: preserve server Z for elevated platforms/roofs and only
|
||||
// correct clearly underground spawns.
|
||||
if (renderer->getTerrainManager()) {
|
||||
auto terrainH = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
|
||||
if (!terrainH) {
|
||||
// No terrain at this X,Y position (ocean/void)
|
||||
if (z > 50.0f) {
|
||||
// High altitude over ocean = bad spawn data (e.g., bears at Z=94 over water)
|
||||
return;
|
||||
}
|
||||
// Low altitude = probably legitimate water creature, allow spawn at original Z
|
||||
} else {
|
||||
// Keep authentic server Z for NPCs on raised geometry (e.g. flight masters).
|
||||
// Only lift up if spawn is clearly below terrain.
|
||||
if (renderPos.z < (*terrainH - 2.0f)) {
|
||||
renderPos.z = *terrainH + 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep authoritative server Z for online creature spawns.
|
||||
// Terrain-based lifting can incorrectly move elevated NPCs (e.g. flight masters on
|
||||
// Stormwind ramparts) to bad heights relative to WMO geometry.
|
||||
|
||||
// Convert canonical WoW orientation (0=north) -> render yaw (0=west)
|
||||
float renderYaw = orientation + glm::radians(90.0f);
|
||||
|
|
@ -2537,8 +2768,16 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
return;
|
||||
}
|
||||
|
||||
// NOTE: Custom humanoid NPC geoset/equipment overrides are currently too
|
||||
// aggressive and can make NPCs invisible (targetable but not rendered).
|
||||
// Keep default model geosets for online creatures until this path is made
|
||||
// data-accurate per display model.
|
||||
static constexpr bool kEnableNpcHumanoidOverrides = false;
|
||||
|
||||
// Set geosets for humanoid NPCs based on CreatureDisplayInfoExtra
|
||||
if (itDisplayData != displayDataMap_.end() && itDisplayData->second.extraDisplayId != 0) {
|
||||
if (kEnableNpcHumanoidOverrides &&
|
||||
itDisplayData != displayDataMap_.end() &&
|
||||
itDisplayData->second.extraDisplayId != 0) {
|
||||
auto itExtra = humanoidExtraMap_.find(itDisplayData->second.extraDisplayId);
|
||||
if (itExtra != humanoidExtraMap_.end()) {
|
||||
const auto& extra = itExtra->second;
|
||||
|
|
@ -2768,6 +3007,83 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
}
|
||||
}
|
||||
|
||||
// Optional NPC helmet attachments (kept disabled for stability: this path
|
||||
// can increase spawn-time pressure and regress NPC visibility in crowded areas).
|
||||
static constexpr bool kEnableNpcHelmetAttachments = false;
|
||||
if (kEnableNpcHelmetAttachments &&
|
||||
itDisplayData != displayDataMap_.end() &&
|
||||
itDisplayData->second.extraDisplayId != 0) {
|
||||
auto itExtra = humanoidExtraMap_.find(itDisplayData->second.extraDisplayId);
|
||||
if (itExtra != humanoidExtraMap_.end()) {
|
||||
const auto& extra = itExtra->second;
|
||||
if (extra.equipDisplayId[0] != 0) { // Helm slot
|
||||
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
||||
if (itemDisplayDbc) {
|
||||
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
|
||||
if (helmIdx >= 0) {
|
||||
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 1);
|
||||
if (!helmModelName.empty()) {
|
||||
size_t dotPos = helmModelName.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
helmModelName = helmModelName.substr(0, dotPos);
|
||||
}
|
||||
|
||||
static const std::unordered_map<uint8_t, std::string> racePrefix = {
|
||||
{1, "Hu"}, {2, "Or"}, {3, "Dw"}, {4, "Ni"}, {5, "Sc"},
|
||||
{6, "Ta"}, {7, "Gn"}, {8, "Tr"}, {10, "Be"}, {11, "Dr"}
|
||||
};
|
||||
std::string genderSuffix = (extra.sexId == 0) ? "M" : "F";
|
||||
std::string raceSuffix;
|
||||
auto itRace = racePrefix.find(extra.raceId);
|
||||
if (itRace != racePrefix.end()) {
|
||||
raceSuffix = "_" + itRace->second + genderSuffix;
|
||||
}
|
||||
|
||||
std::string helmPath;
|
||||
std::vector<uint8_t> helmData;
|
||||
if (!raceSuffix.empty()) {
|
||||
helmPath = "Item\\ObjectComponents\\Head\\" + helmModelName + raceSuffix + ".m2";
|
||||
helmData = assetManager->readFile(helmPath);
|
||||
}
|
||||
if (helmData.empty()) {
|
||||
helmPath = "Item\\ObjectComponents\\Head\\" + helmModelName + ".m2";
|
||||
helmData = assetManager->readFile(helmPath);
|
||||
}
|
||||
|
||||
if (!helmData.empty()) {
|
||||
auto helmModel = pipeline::M2Loader::load(helmData);
|
||||
std::string skinPath = helmPath.substr(0, helmPath.size() - 3) + "00.skin";
|
||||
auto skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, helmModel);
|
||||
}
|
||||
|
||||
if (helmModel.isValid()) {
|
||||
uint32_t helmModelId = nextCreatureModelId_++;
|
||||
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 3);
|
||||
std::string helmTexPath;
|
||||
if (!helmTexName.empty()) {
|
||||
if (!raceSuffix.empty()) {
|
||||
std::string suffixedTex = "Item\\ObjectComponents\\Head\\" + helmTexName + raceSuffix + ".blp";
|
||||
if (assetManager->fileExists(suffixedTex)) {
|
||||
helmTexPath = suffixedTex;
|
||||
}
|
||||
}
|
||||
if (helmTexPath.empty()) {
|
||||
helmTexPath = "Item\\ObjectComponents\\Head\\" + helmTexName + ".blp";
|
||||
}
|
||||
}
|
||||
// Attachment point 11 = Head
|
||||
charRenderer->attachWeapon(instanceId, 11, helmModel, helmModelId, helmTexPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Play idle animation and fade in
|
||||
charRenderer->playAnimation(instanceId, 0, true);
|
||||
charRenderer->startFadeIn(instanceId, 0.5f);
|
||||
|
|
@ -2775,8 +3091,6 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Track instance
|
||||
creatureInstances_[guid] = instanceId;
|
||||
creatureModelIds_[guid] = modelId;
|
||||
creaturePermanentFailureGuids_.erase(guid);
|
||||
|
||||
LOG_INFO("Spawned creature: guid=0x", std::hex, guid, std::dec,
|
||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
}
|
||||
|
|
@ -3440,7 +3754,7 @@ void Application::updateQuestMarkers() {
|
|||
|
||||
static int logCounter = 0;
|
||||
if (++logCounter % 300 == 0) { // Log every ~10 seconds at 30fps
|
||||
LOG_INFO("Quest markers: ", questStatuses.size(), " NPCs with quest status");
|
||||
LOG_DEBUG("Quest markers: ", questStatuses.size(), " NPCs with quest status");
|
||||
}
|
||||
|
||||
// Clear all markers (we'll re-add active ones)
|
||||
|
|
@ -3493,7 +3807,7 @@ void Application::updateQuestMarkers() {
|
|||
}
|
||||
|
||||
if (firstRun && markersAdded > 0) {
|
||||
LOG_INFO("Quest markers: Added ", markersAdded, " markers on first run");
|
||||
LOG_DEBUG("Quest markers: Added ", markersAdded, " markers on first run");
|
||||
firstRun = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,31 @@
|
|||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <sys/sysinfo.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
size_t readMemAvailableBytesFromProc() {
|
||||
std::ifstream meminfo("/proc/meminfo");
|
||||
if (!meminfo.is_open()) return 0;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(meminfo, line)) {
|
||||
// Format: "MemAvailable: 123456789 kB"
|
||||
if (line.rfind("MemAvailable:", 0) != 0) continue;
|
||||
std::istringstream iss(line.substr(13));
|
||||
size_t kb = 0;
|
||||
iss >> kb;
|
||||
if (kb > 0) return kb * 1024ull;
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MemoryMonitor& MemoryMonitor::getInstance() {
|
||||
static MemoryMonitor instance;
|
||||
return instance;
|
||||
|
|
@ -25,10 +45,18 @@ void MemoryMonitor::initialize() {
|
|||
}
|
||||
|
||||
size_t MemoryMonitor::getAvailableRAM() const {
|
||||
// Best source on Linux for reclaimable memory headroom.
|
||||
if (size_t memAvailable = readMemAvailableBytesFromProc(); memAvailable > 0) {
|
||||
return memAvailable;
|
||||
}
|
||||
|
||||
struct sysinfo info;
|
||||
if (sysinfo(&info) == 0) {
|
||||
// Available = free + buffers + cached
|
||||
return static_cast<size_t>(info.freeram) * info.mem_unit;
|
||||
// Fallback approximation if /proc/meminfo is unavailable.
|
||||
size_t freeBytes = static_cast<size_t>(info.freeram) * info.mem_unit;
|
||||
size_t bufferBytes = static_cast<size_t>(info.bufferram) * info.mem_unit;
|
||||
size_t available = freeBytes + bufferBytes;
|
||||
return (totalRAM_ > 0 && available > totalRAM_) ? totalRAM_ : available;
|
||||
}
|
||||
return totalRAM_ / 2; // Fallback: assume 50% available
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ void GameHandler::update(float deltaTime) {
|
|||
// Send periodic heartbeat if in world
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
timeSinceLastPing += deltaTime;
|
||||
timeSinceLastMoveHeartbeat_ += deltaTime;
|
||||
|
||||
if (timeSinceLastPing >= pingInterval) {
|
||||
if (socket) {
|
||||
|
|
@ -168,6 +169,11 @@ void GameHandler::update(float deltaTime) {
|
|||
timeSinceLastPing = 0.0f;
|
||||
}
|
||||
|
||||
if (timeSinceLastMoveHeartbeat_ >= moveHeartbeatInterval_) {
|
||||
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
timeSinceLastMoveHeartbeat_ = 0.0f;
|
||||
}
|
||||
|
||||
// Update cast timer (Phase 3)
|
||||
if (casting && castTimeRemaining > 0.0f) {
|
||||
castTimeRemaining -= deltaTime;
|
||||
|
|
@ -203,6 +209,9 @@ void GameHandler::update(float deltaTime) {
|
|||
if (taxiLandingCooldown_ > 0.0f) {
|
||||
taxiLandingCooldown_ -= deltaTime;
|
||||
}
|
||||
if (taxiStartGrace_ > 0.0f) {
|
||||
taxiStartGrace_ -= deltaTime;
|
||||
}
|
||||
|
||||
// Taxi logic timing
|
||||
auto taxiStart = std::chrono::high_resolution_clock::now();
|
||||
|
|
@ -215,7 +224,8 @@ void GameHandler::update(float deltaTime) {
|
|||
if (unit &&
|
||||
(unit->getUnitFlags() & 0x00000100) == 0 &&
|
||||
!taxiClientActive_ &&
|
||||
!taxiActivatePending_) {
|
||||
!taxiActivatePending_ &&
|
||||
taxiStartGrace_ <= 0.0f) {
|
||||
onTaxiFlight_ = false;
|
||||
taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering
|
||||
if (taxiMountActive_ && mountCallback_) {
|
||||
|
|
@ -247,7 +257,7 @@ void GameHandler::update(float deltaTime) {
|
|||
serverStillTaxi = (playerUnit->getUnitFlags() & 0x00000100) != 0;
|
||||
}
|
||||
|
||||
if (serverStillTaxi || taxiClientActive_ || taxiActivatePending_) {
|
||||
if (taxiStartGrace_ > 0.0f || serverStillTaxi || taxiClientActive_ || taxiActivatePending_) {
|
||||
onTaxiFlight_ = true;
|
||||
} else {
|
||||
if (mountCallback_) mountCallback_(0);
|
||||
|
|
@ -264,6 +274,25 @@ void GameHandler::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// Keep non-taxi mount state server-authoritative.
|
||||
// Some server paths don't emit explicit mount field updates in lockstep
|
||||
// with local visual state changes, so reconcile continuously.
|
||||
if (!onTaxiFlight_ && !taxiMountActive_) {
|
||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
auto playerUnit = std::dynamic_pointer_cast<Unit>(playerEntity);
|
||||
if (playerUnit) {
|
||||
uint32_t serverMountDisplayId = playerUnit->getMountDisplayId();
|
||||
if (serverMountDisplayId != currentMountDisplayId_) {
|
||||
LOG_INFO("Mount reconcile: server=", serverMountDisplayId,
|
||||
" local=", currentMountDisplayId_);
|
||||
currentMountDisplayId_ = serverMountDisplayId;
|
||||
if (mountCallback_) {
|
||||
mountCallback_(serverMountDisplayId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (taxiRecoverPending_ && state == WorldState::IN_WORLD) {
|
||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
if (playerEntity) {
|
||||
|
|
@ -408,7 +437,7 @@ void GameHandler::update(float deltaTime) {
|
|||
|
||||
// Log profiling every 60 frames
|
||||
if (++profileCounter >= 60) {
|
||||
LOG_INFO("UPDATE PROFILE (60 frames): socket=", socketTime / 60.0f, "ms taxi=", taxiTime / 60.0f,
|
||||
LOG_DEBUG("UPDATE PROFILE (60 frames): socket=", socketTime / 60.0f, "ms taxi=", taxiTime / 60.0f,
|
||||
"ms distance=", distanceCheckTime / 60.0f, "ms entity=", entityUpdateTime / 60.0f,
|
||||
"ms TOTAL=", totalTime / 60.0f, "ms");
|
||||
profileCounter = 0;
|
||||
|
|
@ -1603,6 +1632,20 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
movementInfo.time = 0;
|
||||
resurrectPending_ = false;
|
||||
resurrectRequestPending_ = false;
|
||||
onTaxiFlight_ = false;
|
||||
taxiMountActive_ = false;
|
||||
taxiActivatePending_ = false;
|
||||
taxiClientActive_ = false;
|
||||
taxiClientPath_.clear();
|
||||
taxiRecoverPending_ = false;
|
||||
taxiStartGrace_ = 0.0f;
|
||||
currentMountDisplayId_ = 0;
|
||||
taxiMountDisplayId_ = 0;
|
||||
if (mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
|
||||
// Send CMSG_SET_ACTIVE_MOVER (required by some servers)
|
||||
if (playerGuid != 0 && socket) {
|
||||
|
|
@ -1705,9 +1748,17 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Block manual movement while taxi is active/mounted, but still allow heartbeat packets.
|
||||
if ((onTaxiFlight_ || taxiMountActive_) && opcode != Opcode::CMSG_MOVE_HEARTBEAT) return;
|
||||
if (resurrectPending_) return;
|
||||
// Block manual movement while taxi is active/mounted, but always allow
|
||||
// stop/heartbeat opcodes so stuck states can be recovered.
|
||||
bool taxiAllowed =
|
||||
(opcode == Opcode::CMSG_MOVE_HEARTBEAT) ||
|
||||
(opcode == Opcode::CMSG_MOVE_STOP) ||
|
||||
(opcode == Opcode::CMSG_MOVE_STOP_STRAFE) ||
|
||||
(opcode == Opcode::CMSG_MOVE_STOP_TURN) ||
|
||||
(opcode == Opcode::CMSG_MOVE_STOP_SWIM) ||
|
||||
(opcode == Opcode::CMSG_MOVE_FALL_LAND);
|
||||
if ((onTaxiFlight_ || taxiMountActive_) && !taxiAllowed) return;
|
||||
if (resurrectPending_ && !taxiAllowed) return;
|
||||
|
||||
// Use real millisecond timestamp (server validates for anti-cheat)
|
||||
static auto startTime = std::chrono::steady_clock::now();
|
||||
|
|
@ -1817,6 +1868,45 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::forceClearTaxiAndMovementState() {
|
||||
taxiActivatePending_ = false;
|
||||
taxiActivateTimer_ = 0.0f;
|
||||
taxiClientActive_ = false;
|
||||
taxiClientPath_.clear();
|
||||
taxiRecoverPending_ = false;
|
||||
taxiStartGrace_ = 0.0f;
|
||||
onTaxiFlight_ = false;
|
||||
|
||||
if (taxiMountActive_ && mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
currentMountDisplayId_ = 0;
|
||||
resurrectPending_ = false;
|
||||
resurrectRequestPending_ = false;
|
||||
playerDead_ = false;
|
||||
releasedSpirit_ = false;
|
||||
repopPending_ = false;
|
||||
pendingSpiritHealerGuid_ = 0;
|
||||
resurrectCasterGuid_ = 0;
|
||||
|
||||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
movementInfo.transportGuid = 0;
|
||||
clearPlayerTransport();
|
||||
|
||||
if (socket && state == WorldState::IN_WORLD) {
|
||||
sendMovement(Opcode::CMSG_MOVE_STOP);
|
||||
sendMovement(Opcode::CMSG_MOVE_STOP_STRAFE);
|
||||
sendMovement(Opcode::CMSG_MOVE_STOP_TURN);
|
||||
sendMovement(Opcode::CMSG_MOVE_STOP_SWIM);
|
||||
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
|
||||
LOG_INFO("Force-cleared taxi/movement state");
|
||||
}
|
||||
|
||||
void GameHandler::setPosition(float x, float y, float z) {
|
||||
movementInfo.x = x;
|
||||
movementInfo.y = y;
|
||||
|
|
@ -2001,6 +2091,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100;
|
||||
if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_ && taxiLandingCooldown_ <= 0.0f) {
|
||||
onTaxiFlight_ = true;
|
||||
taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f);
|
||||
applyTaxiMountForCurrentNode();
|
||||
}
|
||||
}
|
||||
|
|
@ -2559,6 +2650,15 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
|
|||
LOG_INFO("Ignoring destroy for transport entity: 0x", std::hex, data.guid, std::dec);
|
||||
return;
|
||||
}
|
||||
// Mirror out-of-range handling: invoke render-layer despawn callbacks before entity removal.
|
||||
auto entity = entityManager.getEntity(data.guid);
|
||||
if (entity) {
|
||||
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
|
||||
creatureDespawnCallback_(data.guid);
|
||||
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
||||
gameObjectDespawnCallback_(data.guid);
|
||||
}
|
||||
}
|
||||
entityManager.removeEntity(data.guid);
|
||||
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
|
||||
" (", (data.isDeath ? "death" : "despawn"), ")");
|
||||
|
|
@ -4114,7 +4214,20 @@ void GameHandler::handleAttackStop(network::Packet& packet) {
|
|||
}
|
||||
|
||||
void GameHandler::dismount() {
|
||||
if (!isMounted() || !socket) return;
|
||||
if (!socket) return;
|
||||
if (!isMounted()) {
|
||||
// Local/server desync guard: clear visual mount even when server says unmounted.
|
||||
if (mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
currentMountDisplayId_ = 0;
|
||||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
onTaxiFlight_ = false;
|
||||
taxiActivatePending_ = false;
|
||||
taxiClientActive_ = false;
|
||||
LOG_INFO("Dismount desync recovery: force-cleared local mount state");
|
||||
}
|
||||
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_CANCEL_MOUNT_AURA));
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
|
||||
|
|
@ -4144,20 +4257,27 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
|
|||
|
||||
if (guid != playerGuid) return;
|
||||
|
||||
// Always ACK the speed change to prevent server stall
|
||||
// Always ACK the speed change to prevent server stall.
|
||||
// Packet format mirrors movement packets: packed guid + counter + movement info + new speed.
|
||||
if (socket) {
|
||||
network::Packet ack(static_cast<uint16_t>(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK));
|
||||
ack.writeUInt64(playerGuid);
|
||||
MovementPacket::writePackedGuid(ack, playerGuid);
|
||||
ack.writeUInt32(counter);
|
||||
// MovementInfo (minimal — no flags set means no optional fields)
|
||||
ack.writeUInt32(0); // moveFlags
|
||||
ack.writeUInt16(0); // moveFlags2
|
||||
ack.writeUInt32(movementTime);
|
||||
ack.writeFloat(movementInfo.x);
|
||||
ack.writeFloat(movementInfo.y);
|
||||
ack.writeFloat(movementInfo.z);
|
||||
ack.writeFloat(movementInfo.orientation);
|
||||
ack.writeUInt32(0); // fallTime
|
||||
|
||||
MovementInfo wire = movementInfo;
|
||||
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z));
|
||||
wire.x = serverPos.x;
|
||||
wire.y = serverPos.y;
|
||||
wire.z = serverPos.z;
|
||||
if (wire.hasFlag(MovementFlags::ONTRANSPORT)) {
|
||||
glm::vec3 serverTransport =
|
||||
core::coords::canonicalToServer(glm::vec3(wire.transportX, wire.transportY, wire.transportZ));
|
||||
wire.transportX = serverTransport.x;
|
||||
wire.transportY = serverTransport.y;
|
||||
wire.transportZ = serverTransport.z;
|
||||
}
|
||||
MovementPacket::writeMovementPayload(ack, wire);
|
||||
|
||||
ack.writeFloat(newSpeed);
|
||||
socket->send(ack);
|
||||
}
|
||||
|
|
@ -5899,6 +6019,21 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
|||
movementInfo.z = canonical.z;
|
||||
movementInfo.orientation = orientation;
|
||||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
resurrectPending_ = false;
|
||||
resurrectRequestPending_ = false;
|
||||
onTaxiFlight_ = false;
|
||||
taxiMountActive_ = false;
|
||||
taxiActivatePending_ = false;
|
||||
taxiClientActive_ = false;
|
||||
taxiClientPath_.clear();
|
||||
taxiRecoverPending_ = false;
|
||||
taxiStartGrace_ = 0.0f;
|
||||
currentMountDisplayId_ = 0;
|
||||
taxiMountDisplayId_ = 0;
|
||||
if (mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
|
||||
// Clear world state for the new map
|
||||
entityManager.clear();
|
||||
|
|
@ -6359,6 +6494,13 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Guard against stray/mis-mapped packets being treated as taxi replies.
|
||||
// Only honor taxi replies when a taxi flow is actually active.
|
||||
if (!taxiActivatePending_ && !taxiWindowOpen_ && !onTaxiFlight_) {
|
||||
LOG_DEBUG("Ignoring stray taxi reply: result=", data.result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.result == 0) {
|
||||
// Some cores can emit duplicate success replies (e.g. basic + express activate).
|
||||
// Ignore repeats once taxi is already active and no activation is pending.
|
||||
|
|
@ -6366,6 +6508,7 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
onTaxiFlight_ = true;
|
||||
taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f);
|
||||
taxiWindowOpen_ = false;
|
||||
taxiActivatePending_ = false;
|
||||
taxiActivateTimer_ = 0.0f;
|
||||
|
|
@ -6391,6 +6534,12 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
|
|||
void GameHandler::closeTaxi() {
|
||||
taxiWindowOpen_ = false;
|
||||
|
||||
// Closing the taxi UI must not cancel an active/pending flight.
|
||||
// The window can auto-close due distance checks while takeoff begins.
|
||||
if (taxiActivatePending_ || onTaxiFlight_ || taxiClientActive_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we optimistically mounted during node selection, dismount now
|
||||
if (taxiMountActive_ && mountCallback_) {
|
||||
mountCallback_(0); // Dismount
|
||||
|
|
@ -6444,6 +6593,11 @@ uint32_t GameHandler::getTaxiCostTo(uint32_t destNodeId) const {
|
|||
void GameHandler::activateTaxi(uint32_t destNodeId) {
|
||||
if (!socket || state != WorldState::IN_WORLD) return;
|
||||
|
||||
// One-shot taxi activation until server replies or timeout.
|
||||
if (taxiActivatePending_ || onTaxiFlight_) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t startNode = currentTaxiData_.nearestNode;
|
||||
if (startNode == 0 || destNodeId == 0 || startNode == destNodeId) return;
|
||||
|
||||
|
|
@ -6514,13 +6668,14 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
|
|||
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);
|
||||
// AzerothCore in this setup rejects/misparses CMSG_ACTIVATETAXIEXPRESS (0x312),
|
||||
// so keep taxi activation on the basic packet only.
|
||||
|
||||
// Optimistically start taxi visuals; server will correct if it denies.
|
||||
taxiWindowOpen_ = false;
|
||||
taxiActivatePending_ = true;
|
||||
taxiActivateTimer_ = 0.0f;
|
||||
taxiStartGrace_ = 2.0f;
|
||||
if (!onTaxiFlight_) {
|
||||
onTaxiFlight_ = true;
|
||||
applyTaxiMountForCurrentNode();
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
if (debugFrameCount++ % 120 == 0) {
|
||||
// Log canonical position AND render position to check coordinate conversion
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(transport.position);
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
LOG_DEBUG("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" pathTime=", pathTimeMs, "ms / ", path.durationMs, "ms",
|
||||
" canonicalPos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")",
|
||||
" renderPos=(", renderPos.x, ", ", renderPos.y, ", ", renderPos.z, ")",
|
||||
|
|
|
|||
|
|
@ -523,23 +523,12 @@ bool PongParser::parse(network::Packet& packet, PongData& data) {
|
|||
return true;
|
||||
}
|
||||
|
||||
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(opcode));
|
||||
|
||||
// Movement packet format (WoW 3.3.5a):
|
||||
// packed GUID
|
||||
// uint32 flags
|
||||
// uint16 flags2
|
||||
// uint32 time
|
||||
// float x, y, z
|
||||
// float orientation
|
||||
|
||||
// Write packed GUID
|
||||
void MovementPacket::writePackedGuid(network::Packet& packet, uint64_t guid) {
|
||||
uint8_t mask = 0;
|
||||
uint8_t guidBytes[8];
|
||||
int guidByteCount = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = static_cast<uint8_t>((playerGuid >> (i * 8)) & 0xFF);
|
||||
uint8_t byte = static_cast<uint8_t>((guid >> (i * 8)) & 0xFF);
|
||||
if (byte != 0) {
|
||||
mask |= (1 << i);
|
||||
guidBytes[guidByteCount++] = byte;
|
||||
|
|
@ -549,6 +538,15 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
|
|||
for (int i = 0; i < guidByteCount; i++) {
|
||||
packet.writeUInt8(guidBytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void MovementPacket::writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
|
||||
// Movement packet format (WoW 3.3.5a) payload:
|
||||
// uint32 flags
|
||||
// uint16 flags2
|
||||
// uint32 time
|
||||
// float x, y, z
|
||||
// float orientation
|
||||
|
||||
// Write movement flags
|
||||
packet.writeUInt32(info.flags);
|
||||
|
|
@ -617,6 +615,15 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
|
|||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(opcode));
|
||||
|
||||
// Movement packet format (WoW 3.3.5a):
|
||||
// packed GUID + movement payload
|
||||
writePackedGuid(packet, playerGuid);
|
||||
writeMovementPayload(packet, info);
|
||||
|
||||
// Detailed hex dump for debugging
|
||||
static int mvLog = 5;
|
||||
|
|
@ -828,16 +835,27 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
block.z = packet.readFloat();
|
||||
block.onTransport = (transportGuid != 0);
|
||||
block.transportGuid = transportGuid;
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
float tx = packet.readFloat();
|
||||
float ty = packet.readFloat();
|
||||
float tz = packet.readFloat();
|
||||
if (block.onTransport) {
|
||||
block.transportX = tx;
|
||||
block.transportY = ty;
|
||||
block.transportZ = tz;
|
||||
} else {
|
||||
block.transportX = 0.0f;
|
||||
block.transportY = 0.0f;
|
||||
block.transportZ = 0.0f;
|
||||
}
|
||||
block.orientation = packet.readFloat();
|
||||
/*float corpseOrientation =*/ packet.readFloat();
|
||||
block.hasMovement = true;
|
||||
|
||||
LOG_INFO(" TRANSPORT POSITION UPDATE: guid=0x", std::hex, transportGuid, std::dec,
|
||||
" pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation,
|
||||
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
|
||||
if (block.onTransport) {
|
||||
LOG_INFO(" TRANSPORT POSITION UPDATE: guid=0x", std::hex, transportGuid, std::dec,
|
||||
" pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation,
|
||||
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
|
||||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) {
|
||||
// Simple stationary position (4 floats)
|
||||
|
|
@ -895,9 +913,9 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
}
|
||||
|
||||
uint32_t fieldsCapacity = blockCount * 32;
|
||||
LOG_INFO(" UPDATE MASK PARSE:");
|
||||
LOG_INFO(" maskBlockCount = ", (int)blockCount);
|
||||
LOG_INFO(" fieldsCapacity (blocks * 32) = ", fieldsCapacity);
|
||||
LOG_DEBUG(" UPDATE MASK PARSE:");
|
||||
LOG_DEBUG(" maskBlockCount = ", (int)blockCount);
|
||||
LOG_DEBUG(" fieldsCapacity (blocks * 32) = ", fieldsCapacity);
|
||||
|
||||
// Read update mask
|
||||
std::vector<uint32_t> updateMask(blockCount);
|
||||
|
|
@ -932,11 +950,11 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
size_t bytesUsed = endPos - startPos;
|
||||
size_t bytesRemaining = packet.getSize() - endPos;
|
||||
|
||||
LOG_INFO(" highestSetBitIndex = ", highestSetBit);
|
||||
LOG_INFO(" valuesReadCount = ", valuesReadCount);
|
||||
LOG_INFO(" bytesUsedForFields = ", bytesUsed);
|
||||
LOG_INFO(" bytesRemainingInPacket = ", bytesRemaining);
|
||||
LOG_INFO(" Parsed ", block.fields.size(), " fields");
|
||||
LOG_DEBUG(" highestSetBitIndex = ", highestSetBit);
|
||||
LOG_DEBUG(" valuesReadCount = ", valuesReadCount);
|
||||
LOG_DEBUG(" bytesUsedForFields = ", bytesUsed);
|
||||
LOG_DEBUG(" bytesRemainingInPacket = ", bytesRemaining);
|
||||
LOG_DEBUG(" Parsed ", block.fields.size(), " fields");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1009,9 +1027,9 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
|
|||
// Read block count
|
||||
data.blockCount = packet.readUInt32();
|
||||
|
||||
LOG_INFO("SMSG_UPDATE_OBJECT:");
|
||||
LOG_INFO(" objectCount = ", data.blockCount);
|
||||
LOG_INFO(" packetSize = ", packet.getSize());
|
||||
LOG_DEBUG("SMSG_UPDATE_OBJECT:");
|
||||
LOG_DEBUG(" objectCount = ", data.blockCount);
|
||||
LOG_DEBUG(" packetSize = ", packet.getSize());
|
||||
|
||||
// Check for out-of-range objects first
|
||||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||
|
|
|
|||
|
|
@ -163,14 +163,23 @@ std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
|
|||
fileCacheHits++;
|
||||
return it->second.data;
|
||||
}
|
||||
fileCacheMisses++;
|
||||
}
|
||||
|
||||
// Cache miss - decompress from MPQ.
|
||||
// Keep MPQ reads serialized, but do not block cache-hit readers on this mutex.
|
||||
// Cache miss path: serialize MPQ reads. Before reading, re-check cache while holding
|
||||
// readMutex so only one thread performs decompression per hot path at a time.
|
||||
std::vector<uint8_t> data;
|
||||
{
|
||||
std::lock_guard<std::mutex> readLock(readMutex);
|
||||
{
|
||||
std::lock_guard<std::mutex> cacheLock(cacheMutex);
|
||||
auto it = fileCache.find(normalized);
|
||||
if (it != fileCache.end()) {
|
||||
it->second.lastAccessTime = ++fileCacheAccessCounter;
|
||||
fileCacheHits++;
|
||||
return it->second.data;
|
||||
}
|
||||
fileCacheMisses++;
|
||||
}
|
||||
data = mpqManager.readFile(normalized);
|
||||
}
|
||||
if (data.empty()) {
|
||||
|
|
|
|||
|
|
@ -735,9 +735,45 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
|
||||
// ===== WoW-style orbit camera =====
|
||||
// Pivot point at upper chest/neck
|
||||
// Pivot point at upper chest/neck.
|
||||
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f;
|
||||
glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset);
|
||||
float pivotLift = 0.0f;
|
||||
if (terrainManager && !externalFollow_) {
|
||||
float moved = glm::length(glm::vec2(targetPos.x - lastPivotLiftQueryPos_.x,
|
||||
targetPos.y - lastPivotLiftQueryPos_.y));
|
||||
float distDelta = std::abs(currentDistance - lastPivotLiftDistance_);
|
||||
bool queryLift = (++pivotLiftQueryCounter_ >= PIVOT_LIFT_QUERY_INTERVAL) ||
|
||||
(moved >= PIVOT_LIFT_POS_THRESHOLD) ||
|
||||
(distDelta >= PIVOT_LIFT_DIST_THRESHOLD);
|
||||
if (queryLift) {
|
||||
pivotLiftQueryCounter_ = 0;
|
||||
lastPivotLiftQueryPos_ = targetPos;
|
||||
lastPivotLiftDistance_ = currentDistance;
|
||||
|
||||
// Estimate where camera sits horizontally and ensure enough terrain clearance.
|
||||
glm::vec3 probeCam = targetPos + (-forward3D) * currentDistance;
|
||||
auto terrainAtCam = terrainManager->getHeightAt(probeCam.x, probeCam.y);
|
||||
auto terrainAtPivot = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
||||
|
||||
float desiredLift = 0.0f;
|
||||
if (terrainAtCam) {
|
||||
// Keep pivot high enough so near-hill camera rays don't cut through terrain.
|
||||
constexpr float kMinRayClearance = 2.0f;
|
||||
float basePivotZ = targetPos.z + PIVOT_HEIGHT + mountedOffset;
|
||||
float rayClearance = basePivotZ - *terrainAtCam;
|
||||
if (rayClearance < kMinRayClearance) {
|
||||
desiredLift = std::clamp(kMinRayClearance - rayClearance, 0.0f, 1.4f);
|
||||
}
|
||||
}
|
||||
// If character is already below local terrain sample, avoid lifting aggressively.
|
||||
if (terrainAtPivot && targetPos.z < *terrainAtPivot - 0.2f) {
|
||||
desiredLift = 0.0f;
|
||||
}
|
||||
cachedPivotLift_ = desiredLift;
|
||||
}
|
||||
pivotLift = cachedPivotLift_;
|
||||
}
|
||||
glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset + pivotLift);
|
||||
|
||||
// Camera direction from yaw/pitch (already computed as forward3D)
|
||||
glm::vec3 camDir = -forward3D; // Camera looks at pivot, so it's behind
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ void Celestial::renderSun(const Camera& camera, float timeOfDay,
|
|||
// Create model matrix
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, sunPos);
|
||||
model = glm::scale(model, glm::vec3(500.0f, 500.0f, 1.0f)); // Large and visible
|
||||
model = glm::scale(model, glm::vec3(95.0f, 95.0f, 1.0f)); // Match WotLK-like apparent size
|
||||
|
||||
// Set uniforms
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
|
|
|
|||
|
|
@ -986,7 +986,7 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
|
|||
|
||||
static int logCounter = 0;
|
||||
if (++logCounter >= 300) { // Log every 10 seconds at 30fps
|
||||
LOG_INFO("CharacterRenderer: ", updatedCount, "/", instances.size(), " instances updated (",
|
||||
LOG_DEBUG("CharacterRenderer: ", updatedCount, "/", instances.size(), " instances updated (",
|
||||
instances.size() - updatedCount, " culled)");
|
||||
logCounter = 0;
|
||||
}
|
||||
|
|
@ -1584,7 +1584,17 @@ void CharacterRenderer::setInstanceVisible(uint32_t instanceId, bool visible) {
|
|||
}
|
||||
|
||||
void CharacterRenderer::removeInstance(uint32_t instanceId) {
|
||||
instances.erase(instanceId);
|
||||
auto it = instances.find(instanceId);
|
||||
if (it == instances.end()) return;
|
||||
|
||||
// Remove child attachments first (helmets/weapons), otherwise they leak as
|
||||
// orphan render instances when the parent creature despawns.
|
||||
auto attachments = it->second.weaponAttachments;
|
||||
for (const auto& wa : attachments) {
|
||||
removeInstance(wa.weaponInstanceId);
|
||||
}
|
||||
|
||||
instances.erase(it);
|
||||
}
|
||||
|
||||
bool CharacterRenderer::getAnimationState(uint32_t instanceId, uint32_t& animationId,
|
||||
|
|
|
|||
|
|
@ -2053,7 +2053,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
static int frameCounter = 0;
|
||||
if (++frameCounter >= 120) {
|
||||
frameCounter = 0;
|
||||
LOG_INFO("M2 Render: ", totalMs, " ms (culling/sort: ", cullingSortMs,
|
||||
LOG_DEBUG("M2 Render: ", totalMs, " ms (culling/sort: ", cullingSortMs,
|
||||
" ms, draw: ", drawLoopMs, " ms) | ", sortedVisible_.size(), " visible | ",
|
||||
totalBatchesDrawn, " batches | ", boneMatrixUploads, " bone uploads");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2143,7 +2143,7 @@ void Renderer::update(float deltaTime) {
|
|||
|
||||
// Log renderer profiling every 60 frames
|
||||
if (++rendProfileCounter >= 60) {
|
||||
LOG_INFO("RENDERER UPDATE PROFILE (60 frames): camera=", camTime / 60.0f,
|
||||
LOG_DEBUG("RENDERER UPDATE PROFILE (60 frames): camera=", camTime / 60.0f,
|
||||
"ms light=", lightTime / 60.0f, "ms charAnim=", charAnimTime / 60.0f,
|
||||
"ms terrain=", terrainTime / 60.0f, "ms sky=", skyTime / 60.0f,
|
||||
"ms charRend=", charRendTime / 60.0f, "ms audio=", audioTime / 60.0f,
|
||||
|
|
|
|||
|
|
@ -30,9 +30,58 @@
|
|||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <cctype>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace {
|
||||
std::string trim(const std::string& s) {
|
||||
size_t first = s.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) return "";
|
||||
size_t last = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
std::string toLower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
bool isPortBotTarget(const std::string& target) {
|
||||
std::string t = toLower(trim(target));
|
||||
return t == "portbot" || t == "gmbot" || t == "telebot";
|
||||
}
|
||||
|
||||
std::string buildPortBotCommand(const std::string& rawInput) {
|
||||
std::string input = trim(rawInput);
|
||||
if (input.empty()) return "";
|
||||
|
||||
std::string lower = toLower(input);
|
||||
if (lower == "help" || lower == "?") {
|
||||
return "__help__";
|
||||
}
|
||||
|
||||
if (lower.rfind(".tele ", 0) == 0 || lower.rfind(".go ", 0) == 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (lower.rfind("xyz ", 0) == 0) {
|
||||
return ".go " + input;
|
||||
}
|
||||
|
||||
if (lower == "sw" || lower == "stormwind") return ".tele stormwind";
|
||||
if (lower == "if" || lower == "ironforge") return ".tele ironforge";
|
||||
if (lower == "darn" || lower == "darnassus") return ".tele darnassus";
|
||||
if (lower == "org" || lower == "orgrimmar") return ".tele orgrimmar";
|
||||
if (lower == "tb" || lower == "thunderbluff") return ".tele thunderbluff";
|
||||
if (lower == "uc" || lower == "undercity") return ".tele undercity";
|
||||
if (lower == "shatt" || lower == "shattrath") return ".tele shattrath";
|
||||
if (lower == "dal" || lower == "dalaran") return ".tele dalaran";
|
||||
|
||||
return ".tele " + input;
|
||||
}
|
||||
|
||||
bool raySphereIntersect(const wowee::rendering::Ray& ray, const glm::vec3& center, float radius, float& tOut) {
|
||||
glm::vec3 oc = ray.origin - center;
|
||||
float b = glm::dot(oc, ray.direction);
|
||||
|
|
@ -1200,13 +1249,42 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// /who command
|
||||
if (cmdLower == "who") {
|
||||
std::string playerName;
|
||||
// /who commands
|
||||
if (cmdLower == "who" || cmdLower == "whois" || cmdLower == "online" || cmdLower == "players") {
|
||||
std::string query;
|
||||
if (spacePos != std::string::npos) {
|
||||
playerName = command.substr(spacePos + 1);
|
||||
query = command.substr(spacePos + 1);
|
||||
// Trim leading/trailing whitespace
|
||||
size_t first = query.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) {
|
||||
query.clear();
|
||||
} else {
|
||||
size_t last = query.find_last_not_of(" \t\r\n");
|
||||
query = query.substr(first, last - first + 1);
|
||||
}
|
||||
}
|
||||
gameHandler.queryWho(playerName);
|
||||
|
||||
if ((cmdLower == "whois") && query.empty()) {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /whois <playerName>";
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmdLower == "who" && (query == "help" || query == "?")) {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Who commands: /who [name/filter], /whois <name>, /online";
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
gameHandler.queryWho(query);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
|
@ -1953,6 +2031,26 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// Whisper shortcuts to PortBot/GMBot: translate to GM teleport commands.
|
||||
if (type == game::ChatType::WHISPER && isPortBotTarget(target)) {
|
||||
std::string cmd = buildPortBotCommand(message);
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
if (cmd.empty() || cmd == "__help__") {
|
||||
msg.message = "PortBot: /w PortBot <dest>. Aliases: sw if darn org tb uc shatt dal. Also supports '.tele ...' or 'xyz x y z [map [o]]'.";
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
gameHandler.sendChatMessage(game::ChatType::SAY, cmd, "");
|
||||
msg.message = "PortBot executed: " + cmd;
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate whisper has a target
|
||||
if (type == game::ChatType::WHISPER && target.empty()) {
|
||||
game::MessageChatData msg;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue