mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix mount stability, speed parsing, combat dismount, and self-targeting
- Fix SMSG_FORCE_RUN_SPEED_CHANGE parsing (missing uint32 field caused garbage speed) - Always send speed ACK to prevent server stall, even on invalid values - Defer mount model loading to next frame to avoid render-loop hang - Compute mount height from tight vertex bounds instead of M2 header bounds - Dismount when entering combat or casting spells while mounted - Prevent auto-attacking yourself when self-targeted - Leave combat when 40+ yards from target, close vendor at 15+ yards - Pre-open X11 display for reliable mouse release in signal handlers
This commit is contained in:
parent
643611ee79
commit
0874f4f239
6 changed files with 242 additions and 146 deletions
|
|
@ -154,6 +154,8 @@ private:
|
|||
// Mount model tracking
|
||||
uint32_t mountInstanceId_ = 0;
|
||||
uint32_t mountModelId_ = 0;
|
||||
uint32_t pendingMountDisplayId_ = 0; // Deferred mount load (0 = none pending)
|
||||
void processPendingMount();
|
||||
bool creatureLookupsBuilt_ = false;
|
||||
|
||||
// Deferred creature spawn queue (throttles spawning to avoid hangs)
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ enum class Opcode : uint16_t {
|
|||
|
||||
// ---- Speed Changes ----
|
||||
SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2,
|
||||
CMSG_FORCE_RUN_SPEED_CHANGE_ACK = 0x00E3,
|
||||
|
||||
// ---- Mount ----
|
||||
CMSG_CANCEL_MOUNT_AURA = 0x0375,
|
||||
|
|
|
|||
|
|
@ -385,6 +385,7 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
// Process deferred online creature spawns (throttled)
|
||||
processCreatureSpawnQueue();
|
||||
processPendingMount();
|
||||
if (npcManager && renderer && renderer->getCharacterRenderer()) {
|
||||
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
||||
}
|
||||
|
|
@ -559,148 +560,22 @@ void Application::setupUICallbacks() {
|
|||
despawnOnlineCreature(guid);
|
||||
});
|
||||
|
||||
// Mount callback (online mode) - load/destroy mount model
|
||||
// Mount callback (online mode) - defer heavy model load to next frame
|
||||
gameHandler->setMountCallback([this](uint32_t mountDisplayId) {
|
||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return;
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
|
||||
if (mountDisplayId == 0) {
|
||||
// Dismount: remove mount instance and model
|
||||
if (mountInstanceId_ != 0) {
|
||||
charRenderer->removeInstance(mountInstanceId_);
|
||||
// Dismount is instant (no loading needed)
|
||||
if (renderer && renderer->getCharacterRenderer() && mountInstanceId_ != 0) {
|
||||
renderer->getCharacterRenderer()->removeInstance(mountInstanceId_);
|
||||
mountInstanceId_ = 0;
|
||||
}
|
||||
mountModelId_ = 0;
|
||||
renderer->clearMount();
|
||||
pendingMountDisplayId_ = 0;
|
||||
if (renderer) renderer->clearMount();
|
||||
LOG_INFO("Dismounted");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mount: load mount model
|
||||
std::string m2Path = getModelPathForDisplayId(mountDisplayId);
|
||||
if (m2Path.empty()) {
|
||||
LOG_WARNING("No model path for mount displayId ", mountDisplayId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check model cache
|
||||
uint32_t modelId = 0;
|
||||
bool modelCached = false;
|
||||
auto cacheIt = displayIdModelCache_.find(mountDisplayId);
|
||||
if (cacheIt != displayIdModelCache_.end()) {
|
||||
modelId = cacheIt->second;
|
||||
modelCached = true;
|
||||
} else {
|
||||
modelId = nextCreatureModelId_++;
|
||||
|
||||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("Failed to read mount M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty()) {
|
||||
LOG_WARNING("Failed to parse mount M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load skin file
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
auto skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, model);
|
||||
}
|
||||
|
||||
// Load external .anim files
|
||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFile(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!charRenderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("Failed to load mount model: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
displayIdModelCache_[mountDisplayId] = modelId;
|
||||
}
|
||||
|
||||
// Apply creature skin textures from CreatureDisplayInfo.dbc
|
||||
if (!modelCached) {
|
||||
auto itDisplayData = displayDataMap_.find(mountDisplayId);
|
||||
if (itDisplayData != displayDataMap_.end()) {
|
||||
const auto& dispData = itDisplayData->second;
|
||||
const auto* modelData = charRenderer->getModelData(modelId);
|
||||
if (modelData) {
|
||||
std::string modelDir;
|
||||
size_t lastSlash = m2Path.find_last_of("\\/");
|
||||
if (lastSlash != std::string::npos) {
|
||||
modelDir = m2Path.substr(0, lastSlash + 1);
|
||||
}
|
||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||
const auto& tex = modelData->textures[ti];
|
||||
std::string texPath;
|
||||
if (tex.type == 11 && !dispData.skin1.empty()) {
|
||||
texPath = modelDir + dispData.skin1 + ".blp";
|
||||
} else if (tex.type == 12 && !dispData.skin2.empty()) {
|
||||
texPath = modelDir + dispData.skin2 + ".blp";
|
||||
} else if (tex.type == 13 && !dispData.skin3.empty()) {
|
||||
texPath = modelDir + dispData.skin3 + ".blp";
|
||||
}
|
||||
if (!texPath.empty()) {
|
||||
GLuint skinTex = charRenderer->loadTexture(texPath);
|
||||
if (skinTex != 0) {
|
||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), skinTex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mountModelId_ = modelId;
|
||||
|
||||
// Create mount instance at player position
|
||||
glm::vec3 mountPos = renderer->getCharacterPosition();
|
||||
float yawRad = glm::radians(renderer->getCharacterYaw());
|
||||
uint32_t instanceId = charRenderer->createInstance(modelId, mountPos,
|
||||
glm::vec3(0.0f, 0.0f, yawRad), 1.0f);
|
||||
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create mount instance");
|
||||
return;
|
||||
}
|
||||
|
||||
mountInstanceId_ = instanceId;
|
||||
|
||||
// Compute height offset — place player above mount's back
|
||||
const auto* modelData = charRenderer->getModelData(modelId);
|
||||
float heightOffset = 1.2f; // Default fallback
|
||||
if (modelData) {
|
||||
// No coord swizzle in character renderer, so Z is up in model space too.
|
||||
// Use the top of the bounding box as the saddle height.
|
||||
float topZ = modelData->boundMax.z;
|
||||
if (topZ > 0.1f) {
|
||||
heightOffset = topZ * 0.85f;
|
||||
}
|
||||
LOG_INFO("Mount bounds: min=(", modelData->boundMin.x, ",", modelData->boundMin.y, ",", modelData->boundMin.z,
|
||||
") max=(", modelData->boundMax.x, ",", modelData->boundMax.y, ",", modelData->boundMax.z,
|
||||
") radius=", modelData->boundRadius, " → heightOffset=", heightOffset);
|
||||
}
|
||||
|
||||
renderer->setMounted(instanceId, heightOffset);
|
||||
charRenderer->playAnimation(instanceId, 0, true); // Idle animation
|
||||
|
||||
LOG_INFO("Mounted: displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset);
|
||||
// Queue the mount for processing in the next update() frame
|
||||
pendingMountDisplayId_ = mountDisplayId;
|
||||
});
|
||||
|
||||
// Creature move callback (online mode) - update creature positions
|
||||
|
|
@ -2279,6 +2154,150 @@ void Application::processCreatureSpawnQueue() {
|
|||
}
|
||||
}
|
||||
|
||||
void Application::processPendingMount() {
|
||||
if (pendingMountDisplayId_ == 0) return;
|
||||
uint32_t mountDisplayId = pendingMountDisplayId_;
|
||||
pendingMountDisplayId_ = 0;
|
||||
LOG_INFO("processPendingMount: loading displayId ", mountDisplayId);
|
||||
|
||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return;
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
|
||||
std::string m2Path = getModelPathForDisplayId(mountDisplayId);
|
||||
if (m2Path.empty()) {
|
||||
LOG_WARNING("No model path for mount displayId ", mountDisplayId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check model cache
|
||||
uint32_t modelId = 0;
|
||||
bool modelCached = false;
|
||||
auto cacheIt = displayIdModelCache_.find(mountDisplayId);
|
||||
if (cacheIt != displayIdModelCache_.end()) {
|
||||
modelId = cacheIt->second;
|
||||
modelCached = true;
|
||||
} else {
|
||||
modelId = nextCreatureModelId_++;
|
||||
|
||||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("Failed to read mount M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty()) {
|
||||
LOG_WARNING("Failed to parse mount M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load skin file
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
auto skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, model);
|
||||
}
|
||||
|
||||
// Load external .anim files (only idle + run needed for mounts)
|
||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
uint32_t animId = model.sequences[si].id;
|
||||
// Only load stand(0), walk(4), run(5) anims to avoid hang
|
||||
if (animId != 0 && animId != 4 && animId != 5) continue;
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), animId, model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFile(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!charRenderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("Failed to load mount model: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
displayIdModelCache_[mountDisplayId] = modelId;
|
||||
}
|
||||
|
||||
// Apply creature skin textures from CreatureDisplayInfo.dbc
|
||||
if (!modelCached) {
|
||||
auto itDisplayData = displayDataMap_.find(mountDisplayId);
|
||||
if (itDisplayData != displayDataMap_.end()) {
|
||||
const auto& dispData = itDisplayData->second;
|
||||
const auto* md = charRenderer->getModelData(modelId);
|
||||
if (md) {
|
||||
std::string modelDir;
|
||||
size_t lastSlash = m2Path.find_last_of("\\/");
|
||||
if (lastSlash != std::string::npos) {
|
||||
modelDir = m2Path.substr(0, lastSlash + 1);
|
||||
}
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
const auto& tex = md->textures[ti];
|
||||
std::string texPath;
|
||||
if (tex.type == 11 && !dispData.skin1.empty()) {
|
||||
texPath = modelDir + dispData.skin1 + ".blp";
|
||||
} else if (tex.type == 12 && !dispData.skin2.empty()) {
|
||||
texPath = modelDir + dispData.skin2 + ".blp";
|
||||
} else if (tex.type == 13 && !dispData.skin3.empty()) {
|
||||
texPath = modelDir + dispData.skin3 + ".blp";
|
||||
}
|
||||
if (!texPath.empty()) {
|
||||
GLuint skinTex = charRenderer->loadTexture(texPath);
|
||||
if (skinTex != 0) {
|
||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), skinTex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mountModelId_ = modelId;
|
||||
|
||||
// Create mount instance at player position
|
||||
glm::vec3 mountPos = renderer->getCharacterPosition();
|
||||
float yawRad = glm::radians(renderer->getCharacterYaw());
|
||||
uint32_t instanceId = charRenderer->createInstance(modelId, mountPos,
|
||||
glm::vec3(0.0f, 0.0f, yawRad), 1.0f);
|
||||
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create mount instance");
|
||||
return;
|
||||
}
|
||||
|
||||
mountInstanceId_ = instanceId;
|
||||
|
||||
// Compute height offset — place player above mount's back
|
||||
// Use tight bounds from actual vertices (M2 header bounds can be inaccurate)
|
||||
const auto* modelData = charRenderer->getModelData(modelId);
|
||||
float heightOffset = 1.8f;
|
||||
if (modelData && !modelData->vertices.empty()) {
|
||||
float minZ = std::numeric_limits<float>::max();
|
||||
float maxZ = -std::numeric_limits<float>::max();
|
||||
for (const auto& v : modelData->vertices) {
|
||||
if (v.position.z < minZ) minZ = v.position.z;
|
||||
if (v.position.z > maxZ) maxZ = v.position.z;
|
||||
}
|
||||
float extentZ = maxZ - minZ;
|
||||
LOG_INFO("Mount tight bounds: minZ=", minZ, " maxZ=", maxZ, " extentZ=", extentZ);
|
||||
if (extentZ > 0.5f) {
|
||||
// Saddle point is roughly 75% up the model, measured from model origin
|
||||
heightOffset = maxZ * 0.8f;
|
||||
if (heightOffset < 1.0f) heightOffset = extentZ * 0.75f;
|
||||
if (heightOffset < 1.0f) heightOffset = 1.8f;
|
||||
}
|
||||
}
|
||||
|
||||
renderer->setMounted(instanceId, heightOffset);
|
||||
charRenderer->playAnimation(instanceId, 0, true);
|
||||
|
||||
LOG_INFO("processPendingMount: DONE displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset);
|
||||
}
|
||||
|
||||
void Application::despawnOnlineCreature(uint64_t guid) {
|
||||
auto it = creatureInstances_.find(guid);
|
||||
if (it == creatureInstances_.end()) return;
|
||||
|
|
|
|||
|
|
@ -189,6 +189,34 @@ void GameHandler::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// Leave combat if auto-attack target is too far away (leash range)
|
||||
if (autoAttacking && autoAttackTarget != 0) {
|
||||
auto targetEntity = entityManager.getEntity(autoAttackTarget);
|
||||
if (targetEntity) {
|
||||
float dx = movementInfo.x - targetEntity->getX();
|
||||
float dy = movementInfo.y - targetEntity->getY();
|
||||
float dist = std::sqrt(dx * dx + dy * dy);
|
||||
if (dist > 40.0f) {
|
||||
stopAutoAttack();
|
||||
LOG_INFO("Left combat: target too far (", dist, " yards)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close vendor/gossip window if player walks too far from NPC
|
||||
if (vendorWindowOpen && currentVendorItems.vendorGuid != 0) {
|
||||
auto npc = entityManager.getEntity(currentVendorItems.vendorGuid);
|
||||
if (npc) {
|
||||
float dx = movementInfo.x - npc->getX();
|
||||
float dy = movementInfo.y - npc->getY();
|
||||
float dist = std::sqrt(dx * dx + dy * dy);
|
||||
if (dist > 15.0f) {
|
||||
closeVendor();
|
||||
LOG_INFO("Vendor closed: walked too far from NPC");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update entity movement interpolation (keeps targeting in sync with visuals)
|
||||
for (auto& [guid, entity] : entityManager.getEntities()) {
|
||||
entity->updateMovement(deltaTime);
|
||||
|
|
@ -2924,6 +2952,13 @@ void GameHandler::rebuildOnlineInventory() {
|
|||
// ============================================================
|
||||
|
||||
void GameHandler::startAutoAttack(uint64_t targetGuid) {
|
||||
// Can't attack yourself
|
||||
if (targetGuid == playerGuid) return;
|
||||
|
||||
// Dismount when entering combat
|
||||
if (isMounted()) {
|
||||
dismount();
|
||||
}
|
||||
autoAttacking = true;
|
||||
autoAttackTarget = targetGuid;
|
||||
autoAttackOutOfRange_ = false;
|
||||
|
|
@ -3001,15 +3036,43 @@ void GameHandler::dismount() {
|
|||
void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
|
||||
// Packed GUID
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
// uint32 counter (ack counter, we ignore)
|
||||
// uint32 counter
|
||||
uint32_t counter = packet.readUInt32();
|
||||
// uint32 unknown (TrinityCore/AzerothCore adds this for run speed)
|
||||
packet.readUInt32();
|
||||
// float newSpeed
|
||||
float newSpeed = packet.readFloat();
|
||||
|
||||
if (guid == playerGuid) {
|
||||
serverRunSpeed_ = newSpeed;
|
||||
LOG_INFO("Server run speed changed to ", newSpeed);
|
||||
LOG_INFO("SMSG_FORCE_RUN_SPEED_CHANGE: guid=0x", std::hex, guid, std::dec,
|
||||
" counter=", counter, " speed=", newSpeed);
|
||||
|
||||
if (guid != playerGuid) return;
|
||||
|
||||
// Always ACK the speed change to prevent server stall
|
||||
if (socket) {
|
||||
network::Packet ack(static_cast<uint16_t>(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK));
|
||||
ack.writeUInt64(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
|
||||
ack.writeFloat(newSpeed);
|
||||
socket->send(ack);
|
||||
}
|
||||
|
||||
// Validate speed - reject garbage/NaN values but still ACK
|
||||
if (std::isnan(newSpeed) || newSpeed < 0.1f || newSpeed > 100.0f) {
|
||||
LOG_WARNING("Ignoring invalid run speed: ", newSpeed);
|
||||
return;
|
||||
}
|
||||
|
||||
serverRunSpeed_ = newSpeed;
|
||||
}
|
||||
|
||||
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||
|
|
@ -3145,6 +3208,12 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
|||
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
|
||||
// Casting any spell while mounted → dismount instead
|
||||
if (isMounted()) {
|
||||
dismount();
|
||||
return;
|
||||
}
|
||||
|
||||
if (casting) return; // Already casting
|
||||
|
||||
uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid;
|
||||
|
|
|
|||
17
src/main.cpp
17
src/main.cpp
|
|
@ -5,14 +5,15 @@
|
|||
#include <SDL2/SDL.h>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
// Keep a persistent X11 connection for emergency mouse release in signal handlers.
|
||||
// XOpenDisplay inside a signal handler is unreliable, so we open it once at startup.
|
||||
static Display* g_emergencyDisplay = nullptr;
|
||||
|
||||
static void releaseMouseGrab() {
|
||||
// Bypass SDL — talk to X11 directly (signal-safe enough for our purposes)
|
||||
Display* dpy = XOpenDisplay(nullptr);
|
||||
if (dpy) {
|
||||
XUngrabPointer(dpy, CurrentTime);
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
XFlush(dpy);
|
||||
XCloseDisplay(dpy);
|
||||
if (g_emergencyDisplay) {
|
||||
XUngrabPointer(g_emergencyDisplay, CurrentTime);
|
||||
XUngrabKeyboard(g_emergencyDisplay, CurrentTime);
|
||||
XFlush(g_emergencyDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ static void crashHandler(int sig) {
|
|||
}
|
||||
|
||||
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
||||
g_emergencyDisplay = XOpenDisplay(nullptr);
|
||||
std::signal(SIGSEGV, crashHandler);
|
||||
std::signal(SIGABRT, crashHandler);
|
||||
std::signal(SIGFPE, crashHandler);
|
||||
|
|
@ -44,6 +46,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
|||
app.shutdown();
|
||||
|
||||
LOG_INFO("Application exited successfully");
|
||||
if (g_emergencyDisplay) { XCloseDisplay(g_emergencyDisplay); g_emergencyDisplay = nullptr; }
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
|
|
|
|||
|
|
@ -190,8 +190,10 @@ void CameraController::update(float deltaTime) {
|
|||
speed = WOW_BACK_SPEED;
|
||||
} else if (ctrlDown) {
|
||||
speed = WOW_WALK_SPEED;
|
||||
} else if (runSpeedOverride_ > 0.0f && runSpeedOverride_ < 100.0f && !std::isnan(runSpeedOverride_)) {
|
||||
speed = runSpeedOverride_;
|
||||
} else {
|
||||
speed = (runSpeedOverride_ > 0.0f) ? runSpeedOverride_ : WOW_RUN_SPEED;
|
||||
speed = WOW_RUN_SPEED;
|
||||
}
|
||||
} else {
|
||||
// Exploration mode (original behavior)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue