diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 132c6725..7b5a3775 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -560,6 +560,9 @@ public: // Logout commands void requestLogout(); void cancelLogout(); + + // Instance difficulty + void sendSetDifficulty(uint32_t difficulty); bool isLoggingOut() const { return loggingOut_; } float getLogoutCountdown() const { return logoutCountdown_; } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 18df947f..e928b4cf 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -13167,6 +13167,18 @@ void GameHandler::cancelLogout() { LOG_INFO("Cancelled logout"); } +void GameHandler::sendSetDifficulty(uint32_t difficulty) { + if (!isInWorld()) { + LOG_WARNING("Cannot change difficulty: not in world"); + return; + } + + network::Packet packet(wireOpcode(Opcode::CMSG_CHANGEPLAYER_DIFFICULTY)); + packet.writeUInt32(difficulty); + socket->send(packet); + LOG_INFO("CMSG_CHANGEPLAYER_DIFFICULTY sent: difficulty=", difficulty); +} + void GameHandler::setStandState(uint8_t standState) { if (!isInWorld()) { LOG_WARNING("Cannot change stand state: not in world or not connected"); @@ -15190,18 +15202,17 @@ void GameHandler::maybeDetectVisibleItemLayout() { void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map& fields) { if (guid == 0 || guid == playerGuid) return; - if (visibleItemEntryBase_ < 0 || visibleItemStride_ <= 0) { - // Layout not detected yet — queue this player for inspect as fallback. - if (socket && state == WorldState::IN_WORLD) { - pendingAutoInspect_.insert(guid); - LOG_DEBUG("Queued player 0x", std::hex, guid, std::dec, " for auto-inspect (layout not detected)"); - } - return; - } + + // Use the current base/stride (defaults are correct for WotLK 3.3.5a: base=284, stride=2). + // The heuristic may refine these later, but we proceed immediately with whatever values + // are set rather than waiting for verification. + const int base = visibleItemEntryBase_; + const int stride = visibleItemStride_; + if (base < 0 || stride <= 0) return; // Defensive: should never happen with defaults. std::array newEntries{}; for (int s = 0; s < 19; s++) { - uint16_t idx = static_cast(visibleItemEntryBase_ + s * visibleItemStride_); + uint16_t idx = static_cast(base + s * stride); auto it = fields.find(idx); if (it != fields.end()) newEntries[s] = it->second; } @@ -15210,7 +15221,7 @@ void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map 0) { LOG_INFO("updateOtherPlayerVisibleItems: guid=0x", std::hex, guid, std::dec, - " nonZero=", nonZero, + " nonZero=", nonZero, " base=", base, " stride=", stride, " head=", newEntries[0], " shoulders=", newEntries[2], " chest=", newEntries[4], " legs=", newEntries[6], " mainhand=", newEntries[15], " offhand=", newEntries[16]); @@ -15231,11 +15242,16 @@ void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map FOLLOW_MAX_DIST) { + if (distSq2D > FOLLOW_MAX_DIST * FOLLOW_MAX_DIST) { doCancelAutoFollow(); - } else if (dist2D > FOLLOW_STOP_DIST) { + } else if (distSq2D > FOLLOW_STOP_DIST * FOLLOW_STOP_DIST) { // Face target (render-space yaw: atan2(-dx, -dy) -> degrees) float targetYawRad = std::atan2(-dx, -dy); float targetYawDeg = targetYawRad * 180.0f / 3.14159265f; facingYaw = targetYawDeg; yaw = targetYawDeg; - autoFollowWalk = true; + autoFollowMove = true; } // else: within stop distance, stay put // Cancel on strafe/turn keys if (keyA || keyD || keyQ || keyE) { doCancelAutoFollow(); - autoFollowWalk = false; + autoFollowMove = false; } } // When the server has rooted the player, suppress all horizontal movement input. const bool movBlocked = movementRooted_; - bool nowForward = !movBlocked && (keyW || mouseAutorun || autoRunning || autoFollowWalk); + // Auto-follow uses run speed (same as auto-run), not walk speed + if (autoFollowMove) autoRunning = true; + bool nowForward = !movBlocked && (keyW || mouseAutorun || autoRunning); bool nowBackward = !movBlocked && keyS; bool nowStrafeLeft = false; bool nowStrafeRight = false; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 394e1d29..488d7d30 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2525,8 +2525,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { "/cancelaura", "/cancelform", "/cancellogout", "/cancelshapeshift", "/cast", "/castsequence", "/chathelp", "/clear", "/clearfocus", "/clearmainassist", "/clearmaintank", "/cleartarget", "/cloak", - "/combatlog", "/dance", "/dismount", "/dnd", "/do", "/duel", "/dump", - "/e", "/emote", "/equip", "/equipset", + "/combatlog", "/dance", "/difficulty", "/dismount", "/dnd", "/do", "/duel", "/dump", + "/e", "/emote", "/equip", "/equipset", "/exit", "/focus", "/follow", "/forfeit", "/friend", "/g", "/gdemote", "/ginvite", "/gkick", "/gleader", "/gmotd", "/gmticket", "/gpromote", "/gquit", "/grouploot", "/groster", @@ -2541,6 +2541,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { "/p", "/party", "/petaggressive", "/petattack", "/petdefensive", "/petdismiss", "/petfollow", "/pethalt", "/petpassive", "/petstay", "/played", "/pvp", + "/quit", "/r", "/raid", "/raidconvert", "/raidinfo", "/raidwarning", "/random", "/ready", "/readycheck", "/reload", "/reloadui", "/removefriend", "/reply", "/rl", "/roll", "/run", @@ -6318,7 +6319,8 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { "Target: /target /cleartarget /focus /clearfocus /inspect", "Movement: /sit /stand /kneel /dismount", "Misc: /played /time /zone /loc /afk /dnd /helm /cloak", - " /trade /score /unstuck /logout /ticket /screenshot", + " /trade /score /unstuck /logout /quit /exit /ticket", + " /screenshot /difficulty", " /macrohelp /chathelp /help", }; for (const char* line : kHelpLines) { @@ -6625,8 +6627,8 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /logout command (already exists but using /logout instead of going to login) - if (cmdLower == "logout" || cmdLower == "camp") { + // /logout command (also /camp, /quit, /exit) + if (cmdLower == "logout" || cmdLower == "camp" || cmdLower == "quit" || cmdLower == "exit") { gameHandler.requestLogout(); chatInputBuffer[0] = '\0'; return; @@ -6639,6 +6641,52 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /difficulty command — set dungeon/raid difficulty (WotLK) + if (cmdLower == "difficulty") { + std::string arg; + if (spacePos != std::string::npos) { + arg = command.substr(spacePos + 1); + // Trim whitespace + size_t first = arg.find_first_not_of(" \t"); + size_t last = arg.find_last_not_of(" \t"); + if (first != std::string::npos) + arg = arg.substr(first, last - first + 1); + else + arg.clear(); + for (auto& ch : arg) ch = static_cast(std::tolower(static_cast(ch))); + } + + uint32_t diff = 0; + bool valid = true; + if (arg == "normal" || arg == "0") diff = 0; + else if (arg == "heroic" || arg == "1") diff = 1; + else if (arg == "25" || arg == "25normal" || arg == "25man" || arg == "2") + diff = 2; + else if (arg == "25heroic" || arg == "25manheroic" || arg == "3") + diff = 3; + else valid = false; + + if (!valid || arg.empty()) { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /difficulty normal|heroic|25|25heroic (0-3)"; + gameHandler.addLocalChatMessage(msg); + } else { + static constexpr const char* kDiffNames[] = { + "Normal (5-man)", "Heroic (5-man)", "Normal (25-man)", "Heroic (25-man)" + }; + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = std::string("Setting difficulty to: ") + kDiffNames[diff]; + gameHandler.addLocalChatMessage(msg); + gameHandler.sendSetDifficulty(diff); + } + chatInputBuffer[0] = '\0'; + return; + } + // /helm command if (cmdLower == "helm" || cmdLower == "helmet" || cmdLower == "showhelm") { gameHandler.toggleHelm();