diff --git a/include/ui/keybinding_manager.hpp b/include/ui/keybinding_manager.hpp index 385340ab..e242ae5d 100644 --- a/include/ui/keybinding_manager.hpp +++ b/include/ui/keybinding_manager.hpp @@ -29,7 +29,6 @@ public: TOGGLE_WORLD_MAP, TOGGLE_NAMEPLATES, TOGGLE_RAID_FRAMES, - TOGGLE_QUEST_LOG, TOGGLE_ACHIEVEMENTS, ACTION_COUNT }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 83886782..0ec7fd1e 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -391,143 +391,193 @@ void Application::run() { } auto lastTime = std::chrono::high_resolution_clock::now(); + std::atomic watchdogRunning{true}; + std::atomic watchdogHeartbeatMs{ + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count() + }; + std::thread watchdogThread([this, &watchdogRunning, &watchdogHeartbeatMs]() { + bool releasedForCurrentStall = false; + while (watchdogRunning.load(std::memory_order_acquire)) { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + const int64_t nowMs = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + const int64_t lastBeatMs = watchdogHeartbeatMs.load(std::memory_order_acquire); + const int64_t stallMs = nowMs - lastBeatMs; - while (running && !window->shouldClose()) { - // Calculate delta time - auto currentTime = std::chrono::high_resolution_clock::now(); - std::chrono::duration deltaTimeDuration = currentTime - lastTime; - float deltaTime = deltaTimeDuration.count(); - lastTime = currentTime; - - // Cap delta time to prevent large jumps - if (deltaTime > 0.1f) { - deltaTime = 0.1f; + // Failsafe: if the main loop stalls while relative mouse mode is active, + // forcibly release grab so the user can move the cursor and close the app. + if (stallMs > 1500) { + if (!releasedForCurrentStall) { + SDL_SetRelativeMouseMode(SDL_FALSE); + SDL_ShowCursor(SDL_ENABLE); + if (window && window->getSDLWindow()) { + SDL_SetWindowGrab(window->getSDLWindow(), SDL_FALSE); + } + LOG_WARNING("Main-loop stall detected (", stallMs, + "ms) — force-released mouse capture failsafe"); + releasedForCurrentStall = true; + } + } else { + releasedForCurrentStall = false; + } } + }); - // Poll events - SDL_Event event; - while (SDL_PollEvent(&event)) { - // Pass event to UI manager first - if (uiManager) { - uiManager->processEvent(event); + try { + while (running && !window->shouldClose()) { + watchdogHeartbeatMs.store( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(), + std::memory_order_release); + + // Calculate delta time + auto currentTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration deltaTimeDuration = currentTime - lastTime; + float deltaTime = deltaTimeDuration.count(); + lastTime = currentTime; + + // Cap delta time to prevent large jumps + if (deltaTime > 0.1f) { + deltaTime = 0.1f; } - // Pass mouse events to camera controller (skip when UI has mouse focus) - if (renderer && renderer->getCameraController() && !ImGui::GetIO().WantCaptureMouse) { - if (event.type == SDL_MOUSEMOTION) { - renderer->getCameraController()->processMouseMotion(event.motion); + // Poll events + SDL_Event event; + while (SDL_PollEvent(&event)) { + // Pass event to UI manager first + if (uiManager) { + uiManager->processEvent(event); } - else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { - renderer->getCameraController()->processMouseButton(event.button); + + // Pass mouse events to camera controller (skip when UI has mouse focus) + if (renderer && renderer->getCameraController() && !ImGui::GetIO().WantCaptureMouse) { + if (event.type == SDL_MOUSEMOTION) { + renderer->getCameraController()->processMouseMotion(event.motion); + } + else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { + renderer->getCameraController()->processMouseButton(event.button); + } + else if (event.type == SDL_MOUSEWHEEL) { + renderer->getCameraController()->processMouseWheel(static_cast(event.wheel.y)); + } } - else if (event.type == SDL_MOUSEWHEEL) { - renderer->getCameraController()->processMouseWheel(static_cast(event.wheel.y)); + + // Handle window events + if (event.type == SDL_QUIT) { + window->setShouldClose(true); + } + else if (event.type == SDL_WINDOWEVENT) { + if (event.window.event == SDL_WINDOWEVENT_RESIZED) { + int newWidth = event.window.data1; + int newHeight = event.window.data2; + window->setSize(newWidth, newHeight); + // Vulkan viewport set in command buffer, not globally + if (renderer && renderer->getCamera()) { + renderer->getCamera()->setAspectRatio(static_cast(newWidth) / newHeight); + } + } + } + // Debug controls + else if (event.type == SDL_KEYDOWN) { + // Skip non-function-key input when UI (chat) has keyboard focus + bool uiHasKeyboard = ImGui::GetIO().WantCaptureKeyboard; + auto sc = event.key.keysym.scancode; + bool isFKey = (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12); + if (uiHasKeyboard && !isFKey) { + continue; // Let ImGui handle the keystroke + } + + // F1: Toggle performance HUD + if (event.key.keysym.scancode == SDL_SCANCODE_F1) { + if (renderer && renderer->getPerformanceHUD()) { + renderer->getPerformanceHUD()->toggle(); + bool enabled = renderer->getPerformanceHUD()->isEnabled(); + LOG_INFO("Performance HUD: ", enabled ? "ON" : "OFF"); + } + } + // F4: Toggle shadows + else if (event.key.keysym.scancode == SDL_SCANCODE_F4) { + if (renderer) { + bool enabled = !renderer->areShadowsEnabled(); + renderer->setShadowsEnabled(enabled); + LOG_INFO("Shadows: ", enabled ? "ON" : "OFF"); + } + } + // F7: Test level-up effect (ignore key repeat) + else if (event.key.keysym.scancode == SDL_SCANCODE_F7 && event.key.repeat == 0) { + if (renderer) { + renderer->triggerLevelUpEffect(renderer->getCharacterPosition()); + LOG_INFO("Triggered test level-up effect"); + } + if (uiManager) { + uiManager->getGameScreen().triggerDing(99); + } + } + // F8: Debug WMO floor at current position + else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) { + if (renderer && renderer->getWMORenderer()) { + glm::vec3 pos = renderer->getCharacterPosition(); + LOG_WARNING("F8: WMO floor debug at render pos (", pos.x, ", ", pos.y, ", ", pos.z, ")"); + renderer->getWMORenderer()->debugDumpGroupsAtPosition(pos.x, pos.y, pos.z); + } + } } } - // Handle window events - if (event.type == SDL_QUIT) { + // Update input + Input::getInstance().update(); + + // Update application state + try { + update(deltaTime); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OOM during Application::update (state=", static_cast(state), + ", dt=", deltaTime, "): ", e.what()); + throw; + } catch (const std::exception& e) { + LOG_ERROR("Exception during Application::update (state=", static_cast(state), + ", dt=", deltaTime, "): ", e.what()); + throw; + } + // Render + try { + render(); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OOM during Application::render (state=", static_cast(state), "): ", e.what()); + throw; + } catch (const std::exception& e) { + LOG_ERROR("Exception during Application::render (state=", static_cast(state), "): ", e.what()); + throw; + } + // Swap buffers + try { + window->swapBuffers(); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OOM during swapBuffers: ", e.what()); + throw; + } catch (const std::exception& e) { + LOG_ERROR("Exception during swapBuffers: ", e.what()); + throw; + } + + // Exit gracefully on GPU device lost (unrecoverable) + if (renderer && renderer->getVkContext() && renderer->getVkContext()->isDeviceLost()) { + LOG_ERROR("GPU device lost — exiting application"); window->setShouldClose(true); } - else if (event.type == SDL_WINDOWEVENT) { - if (event.window.event == SDL_WINDOWEVENT_RESIZED) { - int newWidth = event.window.data1; - int newHeight = event.window.data2; - window->setSize(newWidth, newHeight); - // Vulkan viewport set in command buffer, not globally - if (renderer && renderer->getCamera()) { - renderer->getCamera()->setAspectRatio(static_cast(newWidth) / newHeight); - } - } - } - // Debug controls - else if (event.type == SDL_KEYDOWN) { - // Skip non-function-key input when UI (chat) has keyboard focus - bool uiHasKeyboard = ImGui::GetIO().WantCaptureKeyboard; - auto sc = event.key.keysym.scancode; - bool isFKey = (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12); - if (uiHasKeyboard && !isFKey) { - continue; // Let ImGui handle the keystroke - } + } + } catch (...) { + watchdogRunning.store(false, std::memory_order_release); + if (watchdogThread.joinable()) { + watchdogThread.join(); + } + throw; + } - // F1: Toggle performance HUD - if (event.key.keysym.scancode == SDL_SCANCODE_F1) { - if (renderer && renderer->getPerformanceHUD()) { - renderer->getPerformanceHUD()->toggle(); - bool enabled = renderer->getPerformanceHUD()->isEnabled(); - LOG_INFO("Performance HUD: ", enabled ? "ON" : "OFF"); - } - } - // F4: Toggle shadows - else if (event.key.keysym.scancode == SDL_SCANCODE_F4) { - if (renderer) { - bool enabled = !renderer->areShadowsEnabled(); - renderer->setShadowsEnabled(enabled); - LOG_INFO("Shadows: ", enabled ? "ON" : "OFF"); - } - } - // F7: Test level-up effect (ignore key repeat) - else if (event.key.keysym.scancode == SDL_SCANCODE_F7 && event.key.repeat == 0) { - if (renderer) { - renderer->triggerLevelUpEffect(renderer->getCharacterPosition()); - LOG_INFO("Triggered test level-up effect"); - } - if (uiManager) { - uiManager->getGameScreen().triggerDing(99); - } - } - // F8: Debug WMO floor at current position - else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) { - if (renderer && renderer->getWMORenderer()) { - glm::vec3 pos = renderer->getCharacterPosition(); - LOG_WARNING("F8: WMO floor debug at render pos (", pos.x, ", ", pos.y, ", ", pos.z, ")"); - renderer->getWMORenderer()->debugDumpGroupsAtPosition(pos.x, pos.y, pos.z); - } - } - } - } - - // Update input - Input::getInstance().update(); - - // Update application state - try { - update(deltaTime); - } catch (const std::bad_alloc& e) { - LOG_ERROR("OOM during Application::update (state=", static_cast(state), - ", dt=", deltaTime, "): ", e.what()); - throw; - } catch (const std::exception& e) { - LOG_ERROR("Exception during Application::update (state=", static_cast(state), - ", dt=", deltaTime, "): ", e.what()); - throw; - } - // Render - try { - render(); - } catch (const std::bad_alloc& e) { - LOG_ERROR("OOM during Application::render (state=", static_cast(state), "): ", e.what()); - throw; - } catch (const std::exception& e) { - LOG_ERROR("Exception during Application::render (state=", static_cast(state), "): ", e.what()); - throw; - } - // Swap buffers - try { - window->swapBuffers(); - } catch (const std::bad_alloc& e) { - LOG_ERROR("OOM during swapBuffers: ", e.what()); - throw; - } catch (const std::exception& e) { - LOG_ERROR("Exception during swapBuffers: ", e.what()); - throw; - } - - // Exit gracefully on GPU device lost (unrecoverable) - if (renderer && renderer->getVkContext() && renderer->getVkContext()->isDeviceLost()) { - LOG_ERROR("GPU device lost — exiting application"); - window->setShouldClose(true); - } + watchdogRunning.store(false, std::memory_order_release); + if (watchdogThread.joinable()) { + watchdogThread.join(); } LOG_INFO("Main loop ended"); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 77d136ec..31cfdd04 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2319,7 +2319,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { if (!textFocus) { // Toggle character screen (C) and inventory/bags (I) if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) { + const bool wasOpen = inventoryScreen.isCharacterOpen(); inventoryScreen.toggleCharacter(); + if (!wasOpen && gameHandler.isConnected()) { + gameHandler.requestPlayedTime(); + } } if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) { @@ -2350,10 +2354,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { showRaidFrames_ = !showRaidFrames_; } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_QUEST_LOG)) { - questLogScreen.toggle(); - } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) { showAchievementWindow_ = !showAchievementWindow_; } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index fb0a0df7..ac6e3426 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -743,17 +743,6 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { bool bToggled = bagsDown && !bKeyWasDown; bKeyWasDown = bagsDown; - // Character screen toggle (C key, edge-triggered) - bool characterDown = KeybindingManager::getInstance().isActionPressed( - KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN, false); - if (characterDown && !cKeyWasDown) { - characterOpen = !characterOpen; - if (characterOpen && gameHandler_) { - gameHandler_->requestPlayedTime(); - } - } - cKeyWasDown = characterDown; - bool wantsTextInput = ImGui::GetIO().WantTextInput; if (separateBags_) { diff --git a/src/ui/keybinding_manager.cpp b/src/ui/keybinding_manager.cpp index a96a040e..20d562c5 100644 --- a/src/ui/keybinding_manager.cpp +++ b/src/ui/keybinding_manager.cpp @@ -35,7 +35,6 @@ void KeybindingManager::initializeDefaults() { bindings_[static_cast(Action::TOGGLE_WORLD_MAP)] = ImGuiKey_M; // WoW standard: M opens world map bindings_[static_cast(Action::TOGGLE_NAMEPLATES)] = ImGuiKey_V; bindings_[static_cast(Action::TOGGLE_RAID_FRAMES)] = ImGuiKey_F; // Reassigned from R (now camera reset) - bindings_[static_cast(Action::TOGGLE_QUEST_LOG)] = ImGuiKey_None; // Q conflicts with strafe-left; quest log accessible via TOGGLE_QUESTS (L) bindings_[static_cast(Action::TOGGLE_ACHIEVEMENTS)] = ImGuiKey_Y; // WoW standard key (Shift+Y in retail) } @@ -93,7 +92,6 @@ const char* KeybindingManager::getActionName(Action action) { case Action::TOGGLE_WORLD_MAP: return "World Map"; case Action::TOGGLE_NAMEPLATES: return "Nameplates"; case Action::TOGGLE_RAID_FRAMES: return "Raid Frames"; - case Action::TOGGLE_QUEST_LOG: return "Quest Log"; case Action::TOGGLE_ACHIEVEMENTS: return "Achievements"; case Action::ACTION_COUNT: break; } @@ -158,7 +156,7 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) { else if (action == "toggle_world_map") actionIdx = static_cast(Action::TOGGLE_WORLD_MAP); else if (action == "toggle_nameplates") actionIdx = static_cast(Action::TOGGLE_NAMEPLATES); else if (action == "toggle_raid_frames") actionIdx = static_cast(Action::TOGGLE_RAID_FRAMES); - else if (action == "toggle_quest_log") actionIdx = static_cast(Action::TOGGLE_QUEST_LOG); + else if (action == "toggle_quest_log") actionIdx = static_cast(Action::TOGGLE_QUESTS); // legacy alias else if (action == "toggle_achievements") actionIdx = static_cast(Action::TOGGLE_ACHIEVEMENTS); if (actionIdx < 0) continue; @@ -255,7 +253,6 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const { {Action::TOGGLE_WORLD_MAP, "toggle_world_map"}, {Action::TOGGLE_NAMEPLATES, "toggle_nameplates"}, {Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"}, - {Action::TOGGLE_QUEST_LOG, "toggle_quest_log"}, {Action::TOGGLE_ACHIEVEMENTS, "toggle_achievements"}, };