Persist single-player settings and add defaults

This commit is contained in:
Kelsi 2026-02-05 17:40:15 -08:00
parent 46939808ad
commit 697c4b8218
6 changed files with 205 additions and 2 deletions

View file

@ -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<uint64_t, bool> spHasState_;
std::unordered_map<uint64_t, float> spSavedOrientation_;
SinglePlayerSettings spSettings_{};
bool spSettingsLoaded_ = false;
};
} // namespace game

View file

@ -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;

View file

@ -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<float>(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

View file

@ -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<uint16_t>((static_cast<uint32_t>(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<sqlite3_int64>(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<float>(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<sqlite3_int64>(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) {

View file

@ -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);

View file

@ -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();