diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index ba729be9..adbd0e33 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1862,6 +1862,7 @@ private: float timeSinceLastMoveHeartbeat_ = 0.0f; // Periodic movement heartbeat to keep server position synced float moveHeartbeatInterval_ = 0.5f; uint32_t lastLatency = 0; // Last measured latency (milliseconds) + std::chrono::steady_clock::time_point pingTimestamp_; // Time CMSG_PING was sent // Player GUID and map uint64_t playerGuid = 0; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index c0693566..d81b69a3 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -113,6 +113,7 @@ private: bool pendingMinimapRotate = false; bool pendingMinimapSquare = false; bool pendingMinimapNpcDots = false; + bool pendingShowLatencyMeter = true; bool pendingSeparateBags = true; bool pendingAutoLoot = false; @@ -159,6 +160,7 @@ private: bool minimapRotate_ = false; bool minimapSquare_ = false; bool minimapNpcDots_ = false; + bool showLatencyMeter_ = true; // Show server latency indicator bool minimapSettingsApplied_ = false; bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 14abdb94..c92e9943 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7642,6 +7642,9 @@ void GameHandler::sendPing() { LOG_DEBUG("Sending CMSG_PING (heartbeat)"); LOG_DEBUG(" Sequence: ", pingSequence); + // Record send time for RTT measurement + pingTimestamp_ = std::chrono::steady_clock::now(); + // Build and send ping packet auto packet = PingPacket::build(pingSequence, lastLatency); socket->send(packet); @@ -7663,7 +7666,12 @@ void GameHandler::handlePong(network::Packet& packet) { return; } - LOG_DEBUG("Heartbeat acknowledged (sequence: ", data.sequence, ")"); + // Measure round-trip time + auto rtt = std::chrono::steady_clock::now() - pingTimestamp_; + lastLatency = static_cast( + std::chrono::duration_cast(rtt).count()); + + LOG_DEBUG("Heartbeat acknowledged (sequence: ", data.sequence, ", latency: ", lastLatency, "ms)"); } uint32_t GameHandler::nextMovementTimestampMs() { @@ -15077,10 +15085,12 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { // animation/sound and expects the client to request the mail list. bool isMailbox = false; bool chestLike = false; - // Chest-type game objects (type=3): on all expansions, also send CMSG_LOOT so - // the server opens the loot response. Other harvestable/interactive types rely - // on the server auto-sending SMSG_LOOT_RESPONSE after CMSG_GAMEOBJ_USE. - bool shouldSendLoot = isActiveExpansion("classic") || isActiveExpansion("turtle"); + // Always send CMSG_LOOT after CMSG_GAMEOBJ_USE for any gameobject that could be + // lootable. The server silently ignores CMSG_LOOT for non-lootable objects + // (doors, buttons, etc.), so this is safe. Not sending it is the main reason + // chests fail to open when their GO type is not yet cached or their name doesn't + // contain the word "chest" (e.g. lockboxes, coffers, strongboxes, caches). + bool shouldSendLoot = true; if (entity && entity->getType() == ObjectType::GAMEOBJECT) { auto go = std::static_pointer_cast(entity); auto* info = getCachedGameObjectInfo(go->getEntry()); @@ -15096,22 +15106,20 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { refreshMailList(); } else if (info && info->type == 3) { chestLike = true; - // Type-3 chests require CMSG_LOOT on all expansions (AzerothCore WotLK included) - shouldSendLoot = true; - } else if (turtleMode) { - // Turtle compatibility: keep eager loot open behavior. - shouldSendLoot = true; } } if (!chestLike && !goName.empty()) { std::string lower = goName; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - chestLike = (lower.find("chest") != std::string::npos); - if (chestLike) shouldSendLoot = true; + chestLike = (lower.find("chest") != std::string::npos || + lower.find("lockbox") != std::string::npos || + lower.find("strongbox") != std::string::npos || + lower.find("coffer") != std::string::npos || + lower.find("cache") != std::string::npos); } - // For WotLK chest-like gameobjects, also send CMSG_GAMEOBJ_REPORT_USE. - if (!isMailbox && chestLike && isActiveExpansion("wotlk")) { + // For WotLK, CMSG_GAMEOBJ_REPORT_USE is required for chests (and is harmless for others). + if (!isMailbox && isActiveExpansion("wotlk")) { network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)); reportUse.writeUInt64(guid); socket->send(reportUse); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index be43d5d8..acb6cf22 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8581,6 +8581,7 @@ void GameScreen::renderSettingsWindow() { pendingMinimapRotate = minimapRotate_; pendingMinimapSquare = minimapSquare_; pendingMinimapNpcDots = minimapNpcDots_; + pendingShowLatencyMeter = showLatencyMeter_; if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->setRotateWithCamera(minimapRotate_); @@ -8952,6 +8953,16 @@ void GameScreen::renderSettingsWindow() { } } + ImGui::Spacing(); + ImGui::SeparatorText("Network"); + ImGui::Spacing(); + if (ImGui::Checkbox("Show Latency Meter", &pendingShowLatencyMeter)) { + showLatencyMeter_ = pendingShowLatencyMeter; + saveSettings(); + } + ImGui::SameLine(); + ImGui::TextDisabled("(ms indicator near minimap)"); + ImGui::EndChild(); ImGui::EndTabItem(); } @@ -10080,9 +10091,9 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { break; // Show at most one queue slot indicator } - // Latency indicator (shown when in world and last latency is known) + // Latency indicator (toggleable in Interface settings) uint32_t latMs = gameHandler.getLatencyMs(); - if (latMs > 0 && gameHandler.getState() == game::WorldState::IN_WORLD) { + if (showLatencyMeter_ && latMs > 0 && gameHandler.getState() == game::WorldState::IN_WORLD) { ImVec4 latColor; if (latMs < 100) latColor = ImVec4(0.3f, 1.0f, 0.3f, 0.8f); // Green < 100ms else if (latMs < 250) latColor = ImVec4(1.0f, 1.0f, 0.3f, 0.8f); // Yellow < 250ms @@ -10340,6 +10351,7 @@ void GameScreen::saveSettings() { out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n"; out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n"; out << "minimap_npc_dots=" << (pendingMinimapNpcDots ? 1 : 0) << "\n"; + out << "show_latency_meter=" << (pendingShowLatencyMeter ? 1 : 0) << "\n"; out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n"; out << "action_bar2_offset_x=" << pendingActionBar2OffsetX << "\n"; @@ -10440,6 +10452,9 @@ void GameScreen::loadSettings() { int v = std::stoi(val); minimapNpcDots_ = (v != 0); pendingMinimapNpcDots = minimapNpcDots_; + } else if (key == "show_latency_meter") { + showLatencyMeter_ = (std::stoi(val) != 0); + pendingShowLatencyMeter = showLatencyMeter_; } else if (key == "separate_bags") { pendingSeparateBags = (std::stoi(val) != 0); inventoryScreen.setSeparateBags(pendingSeparateBags);