Added settings.ini

This commit is contained in:
2025-11-23 19:08:35 +01:00
parent adec55526e
commit f0a6b0d974
9 changed files with 258 additions and 26 deletions

View File

@ -39,6 +39,7 @@ add_executable(tetris
src/core/input/InputManager.cpp src/core/input/InputManager.cpp
src/core/assets/AssetManager.cpp src/core/assets/AssetManager.cpp
src/core/GlobalState.cpp src/core/GlobalState.cpp
src/core/Settings.cpp
src/graphics/renderers/RenderManager.cpp src/graphics/renderers/RenderManager.cpp
src/persistence/Scores.cpp src/persistence/Scores.cpp
src/graphics/effects/Starfield.cpp src/graphics/effects/Starfield.cpp

15
settings.ini Normal file
View File

@ -0,0 +1,15 @@
; Tetris Game Settings
; This file is auto-generated
[Display]
Fullscreen=1
[Audio]
Music=1
Sound=0
[Player]
Name=Player
[Debug]
Enabled=0

View File

@ -85,6 +85,7 @@ void Audio::start(){
} }
void Audio::toggleMute(){ muted=!muted; } void Audio::toggleMute(){ muted=!muted; }
void Audio::setMuted(bool m){ muted=m; }
void Audio::nextTrack(){ void Audio::nextTrack(){
if(tracks.empty()) { current = -1; return; } if(tracks.empty()) { current = -1; return; }

View File

@ -43,6 +43,8 @@ public:
void shuffle(); // randomize order void shuffle(); // randomize order
void start(); // begin playback void start(); // begin playback
void toggleMute(); void toggleMute();
void setMuted(bool m);
bool isMuted() const { return muted; }
// Menu music support // Menu music support
void setMenuTrack(const std::string& path); void setMenuTrack(const std::string& path);

112
src/core/Settings.cpp Normal file
View File

@ -0,0 +1,112 @@
#include "Settings.h"
#include <fstream>
#include <sstream>
#include <SDL3/SDL.h>
// Singleton instance
Settings& Settings::instance() {
static Settings s_instance;
return s_instance;
}
Settings::Settings() {
// Constructor - defaults already set in header
}
std::string Settings::getSettingsPath() {
// Save settings.ini in the game's directory
return "settings.ini";
}
bool Settings::load() {
std::ifstream file(getSettingsPath());
if (!file.is_open()) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Settings file not found, using defaults");
return false;
}
std::string line;
std::string currentSection;
while (std::getline(file, line)) {
// Trim whitespace
size_t start = line.find_first_not_of(" \t\r\n");
size_t end = line.find_last_not_of(" \t\r\n");
if (start == std::string::npos) continue; // Empty line
line = line.substr(start, end - start + 1);
// Skip comments
if (line[0] == ';' || line[0] == '#') continue;
// Check for section header
if (line[0] == '[' && line[line.length() - 1] == ']') {
currentSection = line.substr(1, line.length() - 2);
continue;
}
// Parse key=value
size_t equalsPos = line.find('=');
if (equalsPos == std::string::npos) continue;
std::string key = line.substr(0, equalsPos);
std::string value = line.substr(equalsPos + 1);
// Trim key and value
key.erase(key.find_last_not_of(" \t") + 1);
value.erase(0, value.find_first_not_of(" \t"));
// Parse settings
if (currentSection == "Display") {
if (key == "Fullscreen") {
m_fullscreen = (value == "1" || value == "true" || value == "True");
}
} else if (currentSection == "Audio") {
if (key == "Music") {
m_musicEnabled = (value == "1" || value == "true" || value == "True");
} else if (key == "Sound") {
m_soundEnabled = (value == "1" || value == "true" || value == "True");
}
} else if (currentSection == "Player") {
if (key == "Name") {
m_playerName = value;
}
} else if (currentSection == "Debug") {
if (key == "Enabled") {
m_debugEnabled = (value == "1" || value == "true" || value == "True");
}
}
}
file.close();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Settings loaded from %s", getSettingsPath().c_str());
return true;
}
bool Settings::save() {
std::ofstream file(getSettingsPath());
if (!file.is_open()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to save settings to %s", getSettingsPath().c_str());
return false;
}
// Write settings in INI format
file << "; Tetris Game Settings\n";
file << "; This file is auto-generated\n\n";
file << "[Display]\n";
file << "Fullscreen=" << (m_fullscreen ? "1" : "0") << "\n\n";
file << "[Audio]\n";
file << "Music=" << (m_musicEnabled ? "1" : "0") << "\n";
file << "Sound=" << (m_soundEnabled ? "1" : "0") << "\n\n";
file << "[Player]\n";
file << "Name=" << m_playerName << "\n\n";
file << "[Debug]\n";
file << "Enabled=" << (m_debugEnabled ? "1" : "0") << "\n";
file.close();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Settings saved to %s", getSettingsPath().c_str());
return true;
}

49
src/core/Settings.h Normal file
View File

@ -0,0 +1,49 @@
#pragma once
#include <string>
/**
* Settings - Persistent game settings manager
* Handles loading/saving settings to settings.ini
*/
class Settings {
public:
// Singleton access
static Settings& instance();
// Load settings from file (returns true if file existed)
bool load();
// Save settings to file
bool save();
// Settings accessors
bool isFullscreen() const { return m_fullscreen; }
void setFullscreen(bool value) { m_fullscreen = value; }
bool isMusicEnabled() const { return m_musicEnabled; }
void setMusicEnabled(bool value) { m_musicEnabled = value; }
bool isSoundEnabled() const { return m_soundEnabled; }
void setSoundEnabled(bool value) { m_soundEnabled = value; }
bool isDebugEnabled() const { return m_debugEnabled; }
void setDebugEnabled(bool value) { m_debugEnabled = value; }
const std::string& getPlayerName() const { return m_playerName; }
void setPlayerName(const std::string& name) { m_playerName = name; }
// Get the settings file path
static std::string getSettingsPath();
private:
Settings(); // Private constructor for singleton
Settings(const Settings&) = delete;
Settings& operator=(const Settings&) = delete;
// Settings values
bool m_fullscreen = false;
bool m_musicEnabled = true;
bool m_soundEnabled = true;
bool m_debugEnabled = false;
std::string m_playerName = "Player";
};

View File

@ -6,6 +6,7 @@
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include "../../core/Settings.h"
// Color constants (copied from main.cpp) // Color constants (copied from main.cpp)
static const SDL_Color COLORS[] = { static const SDL_Color COLORS[] = {
@ -413,30 +414,32 @@ void GameRenderer::renderPlayingState(
pixelFont->draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255}); pixelFont->draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255});
// Debug: Gravity timing info // Debug: Gravity timing info
pixelFont->draw(renderer, scoreX, baseY + 330, "GRAVITY", 0.8f, {150, 150, 150, 255}); if (Settings::instance().isDebugEnabled()) {
double gravityMs = game->getGravityMs(); pixelFont->draw(renderer, scoreX, baseY + 330, "GRAVITY", 0.8f, {150, 150, 150, 255});
double fallAcc = game->getFallAccumulator(); double gravityMs = game->getGravityMs();
double fallAcc = game->getFallAccumulator();
// Calculate effective gravity (accounting for soft drop)
bool isSoftDrop = game->isSoftDropping(); // Calculate effective gravity (accounting for soft drop)
double effectiveGravityMs = isSoftDrop ? (gravityMs / 2.0) : gravityMs; bool isSoftDrop = game->isSoftDropping();
double timeUntilDrop = std::max(0.0, effectiveGravityMs - fallAcc); double effectiveGravityMs = isSoftDrop ? (gravityMs / 2.0) : gravityMs;
double timeUntilDrop = std::max(0.0, effectiveGravityMs - fallAcc);
char gravityStr[32];
snprintf(gravityStr, sizeof(gravityStr), "%.0f ms%s", gravityMs, isSoftDrop ? " (SD)" : ""); char gravityStr[32];
pixelFont->draw(renderer, scoreX, baseY + 350, gravityStr, 0.7f, {180, 180, 180, 255}); snprintf(gravityStr, sizeof(gravityStr), "%.0f ms%s", gravityMs, isSoftDrop ? " (SD)" : "");
pixelFont->draw(renderer, scoreX, baseY + 350, gravityStr, 0.7f, {180, 180, 180, 255});
char dropStr[32];
snprintf(dropStr, sizeof(dropStr), "Drop: %.0f", timeUntilDrop); char dropStr[32];
SDL_Color dropColor = isSoftDrop ? SDL_Color{255, 200, 100, 255} : SDL_Color{100, 255, 100, 255}; snprintf(dropStr, sizeof(dropStr), "Drop: %.0f", timeUntilDrop);
pixelFont->draw(renderer, scoreX, baseY + 370, dropStr, 0.7f, dropColor); SDL_Color dropColor = isSoftDrop ? SDL_Color{255, 200, 100, 255} : SDL_Color{100, 255, 100, 255};
pixelFont->draw(renderer, scoreX, baseY + 370, dropStr, 0.7f, dropColor);
// Gravity HUD
char gms[64]; // Gravity HUD (Top)
double gms_val = game->getGravityMs(); char gms[64];
double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0; double gms_val = game->getGravityMs();
snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps); double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0;
pixelFont->draw(renderer, logicalW - 260, 10, gms, 0.9f, {200, 200, 220, 255}); snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps);
pixelFont->draw(renderer, logicalW - 260, 10, gms, 0.9f, {200, 200, 220, 255});
}
// Hold piece (if implemented) // Hold piece (if implemented)
if (game->held().type < PIECE_COUNT) { if (game->held().type < PIECE_COUNT) {

View File

@ -34,6 +34,7 @@
#include "utils/ImagePathResolver.h" #include "utils/ImagePathResolver.h"
#include "graphics/renderers/GameRenderer.h" #include "graphics/renderers/GameRenderer.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/Settings.h"
// Debug logging removed: no-op in this build (previously LOG_DEBUG) // Debug logging removed: no-op in this build (previously LOG_DEBUG)
@ -534,6 +535,17 @@ int main(int, char **)
// Initialize random seed for fireworks // Initialize random seed for fireworks
srand(static_cast<unsigned int>(SDL_GetTicks())); srand(static_cast<unsigned int>(SDL_GetTicks()));
// Load settings
Settings::instance().load();
// Sync static variables with settings
musicEnabled = Settings::instance().isMusicEnabled();
playerName = Settings::instance().getPlayerName();
if (playerName.empty()) playerName = "Player";
// Apply sound settings to manager
SoundEffectManager::instance().setEnabled(Settings::instance().isSoundEnabled());
int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
if (sdlInitRes < 0) if (sdlInitRes < 0)
{ {
@ -547,7 +559,13 @@ int main(int, char **)
SDL_Quit(); SDL_Quit();
return 1; return 1;
} }
SDL_Window *window = SDL_CreateWindow("Tetris (SDL3)", LOGICAL_W, LOGICAL_H, SDL_WINDOW_RESIZABLE);
SDL_WindowFlags windowFlags = SDL_WINDOW_RESIZABLE;
if (Settings::instance().isFullscreen()) {
windowFlags |= SDL_WINDOW_FULLSCREEN;
}
SDL_Window *window = SDL_CreateWindow("Tetris (SDL3)", LOGICAL_W, LOGICAL_H, windowFlags);
if (!window) if (!window)
{ {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError()); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError());
@ -711,7 +729,8 @@ int main(int, char **)
AppState state = AppState::Loading; AppState state = AppState::Loading;
double loadingProgress = 0.0; double loadingProgress = 0.0;
Uint64 loadStart = SDL_GetTicks(); Uint64 loadStart = SDL_GetTicks();
bool running = true, isFullscreen = false; bool running = true;
bool isFullscreen = Settings::instance().isFullscreen();
bool leftHeld = false, rightHeld = false; bool leftHeld = false, rightHeld = false;
double moveTimerMs = 0; double moveTimerMs = 0;
const double DAS = 170.0, ARR = 40.0; const double DAS = 170.0, ARR = 40.0;
@ -869,11 +888,13 @@ int main(int, char **)
{ {
Audio::instance().toggleMute(); Audio::instance().toggleMute();
musicEnabled = !musicEnabled; musicEnabled = !musicEnabled;
Settings::instance().setMusicEnabled(musicEnabled);
} }
if (e.key.scancode == SDL_SCANCODE_S) if (e.key.scancode == SDL_SCANCODE_S)
{ {
// Toggle sound effects // Toggle sound effects
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled()); SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
Settings::instance().setSoundEnabled(SoundEffectManager::instance().isEnabled());
} }
if (e.key.scancode == SDL_SCANCODE_N) if (e.key.scancode == SDL_SCANCODE_N)
{ {
@ -884,6 +905,7 @@ int main(int, char **)
{ {
isFullscreen = !isFullscreen; isFullscreen = !isFullscreen;
SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0); SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0);
Settings::instance().setFullscreen(isFullscreen);
} }
} }
@ -901,6 +923,7 @@ int main(int, char **)
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
if (playerName.empty()) playerName = "PLAYER"; if (playerName.empty()) playerName = "PLAYER";
scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName); scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName);
Settings::instance().setPlayerName(playerName);
isNewHighScore = false; isNewHighScore = false;
SDL_StopTextInput(window); SDL_StopTextInput(window);
} }
@ -1169,6 +1192,9 @@ int main(int, char **)
// Initialize audio system and start background loading on first frame // Initialize audio system and start background loading on first frame
if (!musicLoaded && currentTrackLoading == 0) { if (!musicLoaded && currentTrackLoading == 0) {
Audio::instance().init(); Audio::instance().init();
// Apply audio settings
Audio::instance().setMuted(!Settings::instance().isMusicEnabled());
// Note: SoundEffectManager doesn't have a global mute yet, but we can add it or handle it in playSound
// Count actual music files first // Count actual music files first
totalTracks = 0; totalTracks = 0;
@ -1686,6 +1712,10 @@ int main(int, char **)
SDL_DestroyTexture(blocksTex); SDL_DestroyTexture(blocksTex);
if (logoSmallTex) if (logoSmallTex)
SDL_DestroyTexture(logoSmallTex); SDL_DestroyTexture(logoSmallTex);
// Save settings on exit
Settings::instance().save();
lineEffect.shutdown(); lineEffect.shutdown();
Audio::instance().shutdown(); Audio::instance().shutdown();
SoundEffectManager::instance().shutdown(); SoundEffectManager::instance().shutdown();

View File

@ -6,6 +6,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include "../core/Settings.h"
OptionsState::OptionsState(StateContext& ctx) : State(ctx) {} OptionsState::OptionsState(StateContext& ctx) : State(ctx) {}
@ -233,18 +234,36 @@ void OptionsState::toggleFullscreen() {
if (ctx.fullscreenFlag) { if (ctx.fullscreenFlag) {
*ctx.fullscreenFlag = nextState; *ctx.fullscreenFlag = nextState;
} }
// Save setting
Settings::instance().setFullscreen(nextState);
Settings::instance().save();
} }
void OptionsState::toggleMusic() { void OptionsState::toggleMusic() {
Audio::instance().toggleMute(); Audio::instance().toggleMute();
// If muted, music is disabled. If not muted, music is enabled.
// Note: Audio::instance().isMuted() returns true if muted.
// But Audio class doesn't expose isMuted directly in header usually?
// Let's assume toggleMute toggles internal state.
// We can track it via ctx.musicEnabled if it's synced.
bool enabled = true;
if (ctx.musicEnabled) { if (ctx.musicEnabled) {
*ctx.musicEnabled = !*ctx.musicEnabled; *ctx.musicEnabled = !*ctx.musicEnabled;
enabled = *ctx.musicEnabled;
} }
// Save setting
Settings::instance().setMusicEnabled(enabled);
Settings::instance().save();
} }
void OptionsState::toggleSoundFx() { void OptionsState::toggleSoundFx() {
bool next = !SoundEffectManager::instance().isEnabled(); bool next = !SoundEffectManager::instance().isEnabled();
SoundEffectManager::instance().setEnabled(next); SoundEffectManager::instance().setEnabled(next);
// Save setting
Settings::instance().setSoundEnabled(next);
Settings::instance().save();
} }
void OptionsState::exitToMenu() { void OptionsState::exitToMenu() {