From 697c4b82183b9e1c7a1ee425613e2c5f5b14bf75 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 5 Feb 2026 17:40:15 -0800 Subject: [PATCH] Persist single-player settings and add defaults --- include/game/game_handler.hpp | 16 +++++ include/rendering/camera_controller.hpp | 4 ++ src/core/application.cpp | 26 ++++++++ src/game/game_handler.cpp | 73 +++++++++++++++++++++ src/rendering/camera_controller.cpp | 3 +- src/ui/game_screen.cpp | 85 ++++++++++++++++++++++++- 6 files changed, 205 insertions(+), 2 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 6b0267bc..f5c178d0 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -173,6 +173,19 @@ public: // Single-player: mark character list ready for selection UI void setSinglePlayerCharListReady(); + struct SinglePlayerSettings { + bool fullscreen = false; + bool vsync = true; + bool shadows = true; + int resWidth = 1920; + int resHeight = 1080; + int musicVolume = 30; + int sfxVolume = 100; + float mouseSensitivity = 0.2f; + bool invertMouse = false; + }; + bool getSinglePlayerSettings(SinglePlayerSettings& out) const; + void setSinglePlayerSettings(const SinglePlayerSettings& settings); // Inventory Inventory& getInventory() { return inventory; } @@ -600,6 +613,7 @@ private: SP_DIRTY_XP = 1 << 7, SP_DIRTY_POSITION = 1 << 8, SP_DIRTY_STATS = 1 << 9, + SP_DIRTY_SETTINGS = 1 << 10, SP_DIRTY_ALL = 0xFFFFFFFFu }; void markSinglePlayerDirty(uint32_t flags, bool highPriority); @@ -616,6 +630,8 @@ private: float spLastDirtyOrientation_ = 0.0f; std::unordered_map spHasState_; std::unordered_map spSavedOrientation_; + SinglePlayerSettings spSettings_{}; + bool spSettingsLoaded_ = false; }; } // namespace game diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 744804d4..df9af3fa 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -23,6 +23,9 @@ public: void setMovementSpeed(float speed) { movementSpeed = speed; } void setMouseSensitivity(float sensitivity) { mouseSensitivity = sensitivity; } + float getMouseSensitivity() const { return mouseSensitivity; } + void setInvertMouse(bool invert) { invertMouse = invert; } + bool isInvertMouse() const { return invertMouse; } void setEnabled(bool enabled) { this->enabled = enabled; } void setTerrainManager(TerrainManager* tm) { terrainManager = tm; } void setWMORenderer(WMORenderer* wmo) { wmoRenderer = wmo; } @@ -90,6 +93,7 @@ private: // Mouse settings float mouseSensitivity = 0.2f; + bool invertMouse = false; bool mouseButtonDown = false; bool leftMouseDown = false; bool rightMouseDown = false; diff --git a/src/core/application.cpp b/src/core/application.cpp index cf1bed33..4759b87b 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1151,6 +1151,32 @@ void Application::startSinglePlayer() { // Load weapon models for equipped items (after inventory is populated) loadEquippedWeapons(); + if (gameHandler && renderer && window) { + game::GameHandler::SinglePlayerSettings settings; + if (gameHandler->getSinglePlayerSettings(settings)) { + window->setVsync(settings.vsync); + window->setFullscreen(settings.fullscreen); + if (settings.resWidth > 0 && settings.resHeight > 0) { + window->applyResolution(settings.resWidth, settings.resHeight); + } + renderer->setShadowsEnabled(settings.shadows); + if (auto* music = renderer->getMusicManager()) { + music->setVolume(settings.musicVolume); + } + float sfxScale = static_cast(settings.sfxVolume) / 100.0f; + if (auto* footstep = renderer->getFootstepManager()) { + footstep->setVolumeScale(sfxScale); + } + if (auto* activity = renderer->getActivitySoundManager()) { + activity->setVolumeScale(sfxScale); + } + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setMouseSensitivity(settings.mouseSensitivity); + cameraController->setInvertMouse(settings.invertMouse); + } + } + } + // --- Loading screen: load terrain and wait for streaming before spawning --- const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN")); // Canonical WoW coords: +X=North, +Y=West, +Z=Up diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 49d6649b..b70bf0db 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -193,6 +193,18 @@ struct SinglePlayerSqlite { " status INTEGER," " progress INTEGER," " PRIMARY KEY (guid, quest)" + ");" + "CREATE TABLE IF NOT EXISTS character_settings (" + " guid INTEGER PRIMARY KEY," + " fullscreen INTEGER," + " vsync INTEGER," + " shadows INTEGER," + " res_w INTEGER," + " res_h INTEGER," + " music_volume INTEGER," + " sfx_volume INTEGER," + " mouse_sensitivity REAL," + " invert_mouse INTEGER" ");"; return exec(kSchema); } @@ -1354,6 +1366,19 @@ void GameHandler::setSinglePlayerCharListReady() { setState(WorldState::CHAR_LIST_RECEIVED); } +bool GameHandler::getSinglePlayerSettings(SinglePlayerSettings& out) const { + if (!singlePlayerMode_ || !spSettingsLoaded_) return false; + out = spSettings_; + return true; +} + +void GameHandler::setSinglePlayerSettings(const SinglePlayerSettings& settings) { + if (!singlePlayerMode_) return; + spSettings_ = settings; + spSettingsLoaded_ = true; + markSinglePlayerDirty(SP_DIRTY_SETTINGS, true); +} + bool GameHandler::getSinglePlayerCreateInfo(Race race, Class cls, SinglePlayerCreateInfo& out) const { auto& db = getSinglePlayerCreateDb(); uint16_t key = static_cast((static_cast(race) << 8) | @@ -1433,6 +1458,8 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) { auto& sp = getSinglePlayerSqlite(); if (!sp.db) return false; + spSettingsLoaded_ = false; + const char* sqlChar = "SELECT level, zone, map, position_x, position_y, position_z, orientation, money, xp, health, max_health, has_state " "FROM characters WHERE guid=?;"; @@ -1608,6 +1635,26 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) { spLastDirtyZ_ = movementInfo.z; spLastDirtyOrientation_ = movementInfo.orientation; + const char* sqlSettings = + "SELECT fullscreen, vsync, shadows, res_w, res_h, music_volume, sfx_volume, mouse_sensitivity, invert_mouse " + "FROM character_settings WHERE guid=?;"; + if (sqlite3_prepare_v2(sp.db, sqlSettings, -1, &stmt, nullptr) == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, static_cast(guid)); + if (sqlite3_step(stmt) == SQLITE_ROW) { + spSettings_.fullscreen = sqlite3_column_int(stmt, 0) != 0; + spSettings_.vsync = sqlite3_column_int(stmt, 1) != 0; + spSettings_.shadows = sqlite3_column_int(stmt, 2) != 0; + spSettings_.resWidth = sqlite3_column_int(stmt, 3); + spSettings_.resHeight = sqlite3_column_int(stmt, 4); + spSettings_.musicVolume = sqlite3_column_int(stmt, 5); + spSettings_.sfxVolume = sqlite3_column_int(stmt, 6); + spSettings_.mouseSensitivity = static_cast(sqlite3_column_double(stmt, 7)); + spSettings_.invertMouse = sqlite3_column_int(stmt, 8) != 0; + spSettingsLoaded_ = true; + } + sqlite3_finalize(stmt); + } + return true; } @@ -1760,6 +1807,32 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) { spHasState_[activeCharacterGuid_] = true; spSavedOrientation_[activeCharacterGuid_] = movementInfo.orientation; + if (spSettingsLoaded_ && (force || (spDirtyFlags_ & SP_DIRTY_SETTINGS))) { + const char* upsertSettings = + "INSERT INTO character_settings " + "(guid, fullscreen, vsync, shadows, res_w, res_h, music_volume, sfx_volume, mouse_sensitivity, invert_mouse) " + "VALUES (?,?,?,?,?,?,?,?,?,?) " + "ON CONFLICT(guid) DO UPDATE SET " + "fullscreen=excluded.fullscreen, vsync=excluded.vsync, shadows=excluded.shadows, " + "res_w=excluded.res_w, res_h=excluded.res_h, music_volume=excluded.music_volume, " + "sfx_volume=excluded.sfx_volume, mouse_sensitivity=excluded.mouse_sensitivity, " + "invert_mouse=excluded.invert_mouse;"; + if (sqlite3_prepare_v2(sp.db, upsertSettings, -1, &stmt, nullptr) == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, static_cast(activeCharacterGuid_)); + sqlite3_bind_int(stmt, 2, spSettings_.fullscreen ? 1 : 0); + sqlite3_bind_int(stmt, 3, spSettings_.vsync ? 1 : 0); + sqlite3_bind_int(stmt, 4, spSettings_.shadows ? 1 : 0); + sqlite3_bind_int(stmt, 5, spSettings_.resWidth); + sqlite3_bind_int(stmt, 6, spSettings_.resHeight); + sqlite3_bind_int(stmt, 7, spSettings_.musicVolume); + sqlite3_bind_int(stmt, 8, spSettings_.sfxVolume); + sqlite3_bind_double(stmt, 9, spSettings_.mouseSensitivity); + sqlite3_bind_int(stmt, 10, spSettings_.invertMouse ? 1 : 0); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + } + sqlite3_stmt* del = nullptr; const char* delInv = "DELETE FROM character_inventory WHERE guid=?;"; if (sqlite3_prepare_v2(sp.db, delInv, -1, &del, nullptr) == SQLITE_OK) { diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 777a6377..3d3799b4 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -952,7 +952,8 @@ void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) { // Directly update stored yaw/pitch (no lossy forward-vector derivation) yaw -= event.xrel * mouseSensitivity; - pitch += event.yrel * mouseSensitivity; + float invert = invertMouse ? -1.0f : 1.0f; + pitch += event.yrel * mouseSensitivity * invert; // WoW-style pitch limits: can look almost straight down, limited upward pitch = glm::clamp(pitch, MIN_PITCH, MAX_PITCH); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e5e08902..e840771d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1802,6 +1802,23 @@ void GameScreen::renderSettingsWindow() { {3840, 2160}, }; static const int kResCount = sizeof(kResolutions) / sizeof(kResolutions[0]); + constexpr int kDefaultResW = 1920; + constexpr int kDefaultResH = 1080; + constexpr bool kDefaultFullscreen = false; + constexpr bool kDefaultVsync = true; + constexpr bool kDefaultShadows = true; + constexpr int kDefaultMusicVolume = 30; + constexpr int kDefaultSfxVolume = 100; + constexpr float kDefaultMouseSensitivity = 0.2f; + constexpr bool kDefaultInvertMouse = false; + + int defaultResIndex = 0; + for (int i = 0; i < kResCount; i++) { + if (kResolutions[i][0] == kDefaultResW && kResolutions[i][1] == kDefaultResH) { + defaultResIndex = i; + break; + } + } if (!settingsInit) { pendingFullscreen = window->isFullscreen(); @@ -1822,6 +1839,10 @@ void GameScreen::renderSettingsWindow() { if (pendingSfxVolume < 0) pendingSfxVolume = 0; if (pendingSfxVolume > 100) pendingSfxVolume = 100; } + if (auto* cameraController = renderer->getCameraController()) { + pendingMouseSensitivity = cameraController->getMouseSensitivity(); + pendingInvertMouse = cameraController->isInvertMouse(); + } } pendingResIndex = 0; int curW = window->getWidth(); @@ -1832,13 +1853,34 @@ void GameScreen::renderSettingsWindow() { break; } } + if (auto* gameHandler = core::Application::getInstance().getGameHandler()) { + if (gameHandler->isSinglePlayerMode()) { + game::GameHandler::SinglePlayerSettings spSettings; + if (gameHandler->getSinglePlayerSettings(spSettings)) { + pendingFullscreen = spSettings.fullscreen; + pendingVsync = spSettings.vsync; + pendingShadows = spSettings.shadows; + pendingMusicVolume = spSettings.musicVolume; + pendingSfxVolume = spSettings.sfxVolume; + pendingMouseSensitivity = spSettings.mouseSensitivity; + pendingInvertMouse = spSettings.invertMouse; + for (int i = 0; i < kResCount; i++) { + if (kResolutions[i][0] == spSettings.resWidth && + kResolutions[i][1] == spSettings.resHeight) { + pendingResIndex = i; + break; + } + } + } + } + } settingsInit = true; } ImGuiIO& io = ImGui::GetIO(); float screenW = io.DisplaySize.x; float screenH = io.DisplaySize.y; - ImVec2 size(380.0f, 320.0f); + ImVec2 size(400.0f, 420.0f); ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f); ImGui::SetNextWindowPos(pos, ImGuiCond_Always); @@ -1863,6 +1905,12 @@ void GameScreen::renderSettingsWindow() { resItems[i] = resBuf[i]; } ImGui::Combo(resLabel, &pendingResIndex, resItems, kResCount); + if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) { + pendingFullscreen = kDefaultFullscreen; + pendingVsync = kDefaultVsync; + pendingShadows = kDefaultShadows; + pendingResIndex = defaultResIndex; + } ImGui::Spacing(); ImGui::Separator(); @@ -1871,6 +1919,22 @@ void GameScreen::renderSettingsWindow() { ImGui::Text("Audio"); ImGui::SliderInt("Music Volume", &pendingMusicVolume, 0, 100, "%d"); ImGui::SliderInt("SFX Volume", &pendingSfxVolume, 0, 100, "%d"); + if (ImGui::Button("Restore Audio Defaults", ImVec2(-1, 0))) { + pendingMusicVolume = kDefaultMusicVolume; + pendingSfxVolume = kDefaultSfxVolume; + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::Text("Controls"); + ImGui::SliderFloat("Mouse Sensitivity", &pendingMouseSensitivity, 0.05f, 1.0f, "%.2f"); + ImGui::Checkbox("Invert Mouse", &pendingInvertMouse); + if (ImGui::Button("Restore Control Defaults", ImVec2(-1, 0))) { + pendingMouseSensitivity = kDefaultMouseSensitivity; + pendingInvertMouse = kDefaultInvertMouse; + } ImGui::Spacing(); ImGui::Separator(); @@ -1891,6 +1955,25 @@ void GameScreen::renderSettingsWindow() { if (auto* activity = renderer->getActivitySoundManager()) { activity->setVolumeScale(sfxScale); } + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setMouseSensitivity(pendingMouseSensitivity); + cameraController->setInvertMouse(pendingInvertMouse); + } + } + if (auto* gameHandler = core::Application::getInstance().getGameHandler()) { + if (gameHandler->isSinglePlayerMode()) { + game::GameHandler::SinglePlayerSettings spSettings; + spSettings.fullscreen = pendingFullscreen; + spSettings.vsync = pendingVsync; + spSettings.shadows = pendingShadows; + spSettings.resWidth = kResolutions[pendingResIndex][0]; + spSettings.resHeight = kResolutions[pendingResIndex][1]; + spSettings.musicVolume = pendingMusicVolume; + spSettings.sfxVolume = pendingSfxVolume; + spSettings.mouseSensitivity = pendingMouseSensitivity; + spSettings.invertMouse = pendingInvertMouse; + gameHandler->setSinglePlayerSettings(spSettings); + } } } ImGui::Spacing();