From 783c12790dfdc9c07d7738062edc17c8e4b03462 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Fri, 19 Dec 2025 19:25:56 +0100 Subject: [PATCH] refactor main.cpp --- src/main.cpp | 718 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 450 insertions(+), 268 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 7a3a0cc..7a417c8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -117,23 +117,121 @@ static bool isNewHighScore = false; static std::string playerName = ""; static bool helpOverlayPausedGame = false; +namespace { -int main(int, char **) +struct AppRuntime { + SDL_Window* window = nullptr; + SDL_Renderer* renderer = nullptr; + + AssetLoader assetLoader; + std::unique_ptr loadingManager; + std::unique_ptr textureLoader; + + FontAtlas pixelFont; + FontAtlas font; + + ScoreManager scores; + std::atomic scoresLoadComplete{false}; + std::jthread scoreLoader; + std::jthread menuTrackLoader; + + Starfield starfield; + Starfield3D starfield3D; + SpaceWarp spaceWarp; + SpaceWarpFlightMode warpFlightMode = SpaceWarpFlightMode::Forward; + bool warpAutoPilotEnabled = true; + + LineEffect lineEffect; + + SDL_Texture* logoTex = nullptr; + SDL_Texture* logoSmallTex = nullptr; + int logoSmallW = 0; + int logoSmallH = 0; + SDL_Texture* backgroundTex = nullptr; + SDL_Texture* mainScreenTex = nullptr; + int mainScreenW = 0; + int mainScreenH = 0; + + SDL_Texture* blocksTex = nullptr; + SDL_Texture* scorePanelTex = nullptr; + SDL_Texture* statisticsPanelTex = nullptr; + SDL_Texture* nextPanelTex = nullptr; + + BackgroundManager levelBackgrounds; + int startLevelSelection = 0; + + // Music loading tracking + int totalTracks = 0; + int currentTrackLoading = 0; + bool musicLoaded = false; + bool musicStarted = false; + bool musicLoadingStarted = false; + + // Loader control: execute incrementally on main thread to avoid SDL threading issues + std::atomic_bool loadingStarted{false}; + std::atomic_bool loadingComplete{false}; + std::atomic loadingStep{0}; + + std::unique_ptr game; + std::vector singleSounds; + std::vector doubleSounds; + std::vector tripleSounds; + std::vector tetrisSounds; + bool suppressLineVoiceForLevelUp = false; + + AppState state = AppState::Loading; + double loadingProgress = 0.0; + Uint64 loadStart = 0; + bool running = true; + bool isFullscreen = false; + bool leftHeld = false; + bool rightHeld = false; + double moveTimerMs = 0.0; + double DAS = 170.0; + double ARR = 40.0; + SDL_Rect logicalVP{0, 0, LOGICAL_W, LOGICAL_H}; + float logicalScale = 1.f; + Uint64 lastMs = 0; + + enum class MenuFadePhase { None, FadeOut, FadeIn }; + MenuFadePhase menuFadePhase = MenuFadePhase::None; + double menuFadeClockMs = 0.0; + float menuFadeAlpha = 0.0f; + double MENU_PLAY_FADE_DURATION_MS = 450.0; + AppState menuFadeTarget = AppState::Menu; + bool menuPlayCountdownArmed = false; + bool gameplayCountdownActive = false; + double gameplayCountdownElapsed = 0.0; + int gameplayCountdownIndex = 0; + double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0; + std::array GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" }; + double gameplayBackgroundClockMs = 0.0; + + std::unique_ptr stateMgr; + StateContext ctx{}; + std::unique_ptr loadingState; + std::unique_ptr menuState; + std::unique_ptr optionsState; + std::unique_ptr levelSelectorState; + std::unique_ptr playingState; +}; + +static int initApp(AppRuntime& app) { // Initialize random seed for procedural effects srand(static_cast(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); if (sdlInitRes < 0) { @@ -147,30 +245,31 @@ int main(int, char **) SDL_Quit(); return 1; } - + 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) + + app.window = SDL_CreateWindow("Tetris (SDL3)", LOGICAL_W, LOGICAL_H, windowFlags); + if (!app.window) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError()); TTF_Quit(); SDL_Quit(); return 1; } - SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr); - if (!renderer) + app.renderer = SDL_CreateRenderer(app.window, nullptr); + if (!app.renderer) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateRenderer failed: %s", SDL_GetError()); - SDL_DestroyWindow(window); + SDL_DestroyWindow(app.window); + app.window = nullptr; TTF_Quit(); SDL_Quit(); return 1; } - SDL_SetRenderVSync(renderer, 1); + SDL_SetRenderVSync(app.renderer, 1); if (const char* basePathRaw = SDL_GetBasePath()) { std::filesystem::path exeDir(basePathRaw); @@ -192,100 +291,54 @@ int main(int, char **) } // Asset loader (creates SDL_Textures on the main thread) - AssetLoader assetLoader; - assetLoader.init(renderer); - LoadingManager loadingManager(&assetLoader); + app.assetLoader.init(app.renderer); + app.loadingManager = std::make_unique(&app.assetLoader); // Legacy image loader (used only as a fallback when AssetLoader misses) - TextureLoader textureLoader(g_loadedTasks, g_currentLoadingFile, g_currentLoadingMutex, g_assetLoadErrors, g_assetLoadErrorsMutex); + app.textureLoader = std::make_unique( + g_loadedTasks, + g_currentLoadingFile, + g_currentLoadingMutex, + g_assetLoadErrors, + g_assetLoadErrorsMutex); - // Font and UI asset handles (actual loading deferred until Loading state) - FontAtlas pixelFont; - FontAtlas font; - - ScoreManager scores; - std::atomic scoresLoadComplete{false}; // Load scores asynchronously but keep the worker alive until shutdown to avoid lifetime issues - std::jthread scoreLoader([&scores, &scoresLoadComplete]() { - scores.load(); - scoresLoadComplete.store(true, std::memory_order_release); + app.scoreLoader = std::jthread([&app]() { + app.scores.load(); + app.scoresLoadComplete.store(true, std::memory_order_release); }); - std::jthread menuTrackLoader; - Starfield starfield; - starfield.init(200, LOGICAL_W, LOGICAL_H); - Starfield3D starfield3D; - starfield3D.init(LOGICAL_W, LOGICAL_H, 200); - SpaceWarp spaceWarp; - spaceWarp.init(LOGICAL_W, LOGICAL_H, 420); - SpaceWarpFlightMode warpFlightMode = SpaceWarpFlightMode::Forward; - spaceWarp.setFlightMode(warpFlightMode); - bool warpAutoPilotEnabled = true; - spaceWarp.setAutoPilotEnabled(true); - + + app.starfield.init(200, LOGICAL_W, LOGICAL_H); + app.starfield3D.init(LOGICAL_W, LOGICAL_H, 200); + app.spaceWarp.init(LOGICAL_W, LOGICAL_H, 420); + app.spaceWarp.setFlightMode(app.warpFlightMode); + app.warpAutoPilotEnabled = true; + app.spaceWarp.setAutoPilotEnabled(true); + // Initialize line clearing effects - LineEffect lineEffect; - lineEffect.init(renderer); - - // Asset handles (textures initialized by loader thread when Loading state starts) - SDL_Texture* logoTex = nullptr; - int logoSmallW = 0, logoSmallH = 0; - SDL_Texture* logoSmallTex = nullptr; - SDL_Texture* backgroundTex = nullptr; // No static background texture is used - int mainScreenW = 0, mainScreenH = 0; - SDL_Texture* mainScreenTex = nullptr; + app.lineEffect.init(app.renderer); - // Level background manager (moved to BackgroundManager) - BackgroundManager levelBackgrounds; - - // Default start level selection: 0 (declare here so it's in scope for all handlers) - int startLevelSelection = 0; - - SDL_Texture* blocksTex = nullptr; - SDL_Texture* scorePanelTex = nullptr; - SDL_Texture* statisticsPanelTex = nullptr; - SDL_Texture* nextPanelTex = nullptr; - - // Music loading tracking - int totalTracks = 0; - int currentTrackLoading = 0; - bool musicLoaded = false; - bool musicStarted = false; - bool musicLoadingStarted = false; - - // Loader control: execute incrementally on main thread to avoid SDL threading issues - std::atomic_bool g_loadingStarted{false}; - std::atomic_bool g_loadingComplete{false}; - std::atomic g_loadingStep{0}; - - // Loading is now handled by AssetLoader + LoadingManager. - // Old incremental lambda removed; use LoadingManager to queue texture loads and - // perform a single step per frame. Non-texture initialization (fonts, SFX) - // is performed on the first loading frame below when the loader is started. - - Game game(startLevelSelection); + app.game = std::make_unique(app.startLevelSelection); // Apply global gravity speed multiplier from config - game.setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER); - game.reset(startLevelSelection); - - // Sound effects system already initialized; audio loads are handled by loader thread - - // Define voice line banks for gameplay callbacks - std::vector singleSounds = {"well_played", "smooth_clear", "great_move"}; - std::vector doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"}; - std::vector tripleSounds = {"impressive", "triple_strike"}; - std::vector tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"}; - - bool suppressLineVoiceForLevelUp = false; + app.game->setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER); + app.game->reset(app.startLevelSelection); - auto playVoiceCue = [&](int linesCleared) { + // Define voice line banks for gameplay callbacks + app.singleSounds = {"well_played", "smooth_clear", "great_move"}; + app.doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"}; + app.tripleSounds = {"impressive", "triple_strike"}; + app.tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"}; + app.suppressLineVoiceForLevelUp = false; + + auto playVoiceCue = [&app](int linesCleared) { const std::vector* bank = nullptr; switch (linesCleared) { - case 1: bank = &singleSounds; break; - case 2: bank = &doubleSounds; break; - case 3: bank = &tripleSounds; break; + case 1: bank = &app.singleSounds; break; + case 2: bank = &app.doubleSounds; break; + case 3: bank = &app.tripleSounds; break; default: if (linesCleared >= 4) { - bank = &tetrisSounds; + bank = &app.tetrisSounds; } break; } @@ -295,7 +348,7 @@ int main(int, char **) }; // Set up sound effect callbacks - game.setSoundCallback([&, playVoiceCue](int linesCleared) { + app.game->setSoundCallback([&app, playVoiceCue](int linesCleared) { if (linesCleared <= 0) { return; } @@ -304,92 +357,257 @@ int main(int, char **) SoundEffectManager::instance().playSound("clear_line", 1.0f); // Layer a voiced callout based on the number of cleared lines - if (!suppressLineVoiceForLevelUp) { + if (!app.suppressLineVoiceForLevelUp) { playVoiceCue(linesCleared); } - suppressLineVoiceForLevelUp = false; + app.suppressLineVoiceForLevelUp = false; }); - - game.setLevelUpCallback([&](int newLevel) { + + app.game->setLevelUpCallback([&app](int newLevel) { SoundEffectManager::instance().playSound("new_level", 1.0f); SoundEffectManager::instance().playSound("lets_go", 1.0f); // Existing voice line - suppressLineVoiceForLevelUp = true; + app.suppressLineVoiceForLevelUp = true; }); - - AppState state = AppState::Loading; - double loadingProgress = 0.0; - Uint64 loadStart = SDL_GetTicks(); - bool running = true; - bool isFullscreen = Settings::instance().isFullscreen(); - bool leftHeld = false, rightHeld = false; - double moveTimerMs = 0; - const double DAS = 170.0, ARR = 40.0; - SDL_Rect logicalVP{0, 0, LOGICAL_W, LOGICAL_H}; - float logicalScale = 1.f; - Uint64 lastMs = SDL_GetPerformanceCounter(); - enum class MenuFadePhase { None, FadeOut, FadeIn }; - MenuFadePhase menuFadePhase = MenuFadePhase::None; - double menuFadeClockMs = 0.0; - float menuFadeAlpha = 0.0f; - const double MENU_PLAY_FADE_DURATION_MS = 450.0; - AppState menuFadeTarget = AppState::Menu; - bool menuPlayCountdownArmed = false; - bool gameplayCountdownActive = false; - double gameplayCountdownElapsed = 0.0; - int gameplayCountdownIndex = 0; - const double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0; - const std::array GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" }; - double gameplayBackgroundClockMs = 0.0; - + app.state = AppState::Loading; + app.loadingProgress = 0.0; + app.loadStart = SDL_GetTicks(); + app.running = true; + app.isFullscreen = Settings::instance().isFullscreen(); + app.leftHeld = false; + app.rightHeld = false; + app.moveTimerMs = 0; + app.DAS = 170.0; + app.ARR = 40.0; + app.logicalVP = SDL_Rect{0, 0, LOGICAL_W, LOGICAL_H}; + app.logicalScale = 1.f; + app.lastMs = SDL_GetPerformanceCounter(); + + app.menuFadePhase = AppRuntime::MenuFadePhase::None; + app.menuFadeClockMs = 0.0; + app.menuFadeAlpha = 0.0f; + app.MENU_PLAY_FADE_DURATION_MS = 450.0; + app.menuFadeTarget = AppState::Menu; + app.menuPlayCountdownArmed = false; + app.gameplayCountdownActive = false; + app.gameplayCountdownElapsed = 0.0; + app.gameplayCountdownIndex = 0; + app.GAMEPLAY_COUNTDOWN_STEP_MS = 400.0; + app.GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" }; + app.gameplayBackgroundClockMs = 0.0; + // Instantiate state manager - StateManager stateMgr(state); + app.stateMgr = std::make_unique(app.state); // Prepare shared context for states - StateContext ctx{}; - // Allow states to access the state manager for transitions - ctx.stateManager = &stateMgr; - ctx.game = &game; - ctx.scores = nullptr; // populated once async load finishes - ctx.starfield = &starfield; - ctx.starfield3D = &starfield3D; - ctx.font = &font; - ctx.pixelFont = &pixelFont; - ctx.lineEffect = &lineEffect; - ctx.logoTex = logoTex; - ctx.logoSmallTex = logoSmallTex; - ctx.logoSmallW = logoSmallW; - ctx.logoSmallH = logoSmallH; - ctx.backgroundTex = nullptr; - ctx.blocksTex = blocksTex; - ctx.scorePanelTex = scorePanelTex; - ctx.statisticsPanelTex = statisticsPanelTex; - ctx.nextPanelTex = nextPanelTex; - ctx.mainScreenTex = mainScreenTex; - ctx.mainScreenW = mainScreenW; - ctx.mainScreenH = mainScreenH; - ctx.musicEnabled = &musicEnabled; - ctx.startLevelSelection = &startLevelSelection; - ctx.hoveredButton = &hoveredButton; - ctx.showSettingsPopup = &showSettingsPopup; - ctx.showHelpOverlay = &showHelpOverlay; - ctx.showExitConfirmPopup = &showExitConfirmPopup; - ctx.exitPopupSelectedButton = &exitPopupSelectedButton; - ctx.gameplayCountdownActive = &gameplayCountdownActive; - ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed; - ctx.playerName = &playerName; - ctx.fullscreenFlag = &isFullscreen; - ctx.applyFullscreen = [window, &isFullscreen](bool enable) { - SDL_SetWindowFullscreen(window, enable ? SDL_WINDOW_FULLSCREEN : 0); - isFullscreen = enable; + app.ctx = StateContext{}; + app.ctx.stateManager = app.stateMgr.get(); + app.ctx.game = app.game.get(); + app.ctx.scores = nullptr; // populated once async load finishes + app.ctx.starfield = &app.starfield; + app.ctx.starfield3D = &app.starfield3D; + app.ctx.font = &app.font; + app.ctx.pixelFont = &app.pixelFont; + app.ctx.lineEffect = &app.lineEffect; + app.ctx.logoTex = app.logoTex; + app.ctx.logoSmallTex = app.logoSmallTex; + app.ctx.logoSmallW = app.logoSmallW; + app.ctx.logoSmallH = app.logoSmallH; + app.ctx.backgroundTex = nullptr; + app.ctx.blocksTex = app.blocksTex; + app.ctx.scorePanelTex = app.scorePanelTex; + app.ctx.statisticsPanelTex = app.statisticsPanelTex; + app.ctx.nextPanelTex = app.nextPanelTex; + app.ctx.mainScreenTex = app.mainScreenTex; + app.ctx.mainScreenW = app.mainScreenW; + app.ctx.mainScreenH = app.mainScreenH; + app.ctx.musicEnabled = &musicEnabled; + app.ctx.startLevelSelection = &app.startLevelSelection; + app.ctx.hoveredButton = &hoveredButton; + app.ctx.showSettingsPopup = &showSettingsPopup; + app.ctx.showHelpOverlay = &showHelpOverlay; + app.ctx.showExitConfirmPopup = &showExitConfirmPopup; + app.ctx.exitPopupSelectedButton = &exitPopupSelectedButton; + app.ctx.gameplayCountdownActive = &app.gameplayCountdownActive; + app.ctx.menuPlayCountdownArmed = &app.menuPlayCountdownArmed; + app.ctx.playerName = &playerName; + app.ctx.fullscreenFlag = &app.isFullscreen; + app.ctx.applyFullscreen = [&app](bool enable) { + SDL_SetWindowFullscreen(app.window, enable ? SDL_WINDOW_FULLSCREEN : 0); + app.isFullscreen = enable; }; - ctx.queryFullscreen = [window]() -> bool { - return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0; + app.ctx.queryFullscreen = [&app]() -> bool { + return (SDL_GetWindowFlags(app.window) & SDL_WINDOW_FULLSCREEN) != 0; }; - ctx.requestQuit = [&running]() { - running = false; + app.ctx.requestQuit = [&app]() { + app.running = false; }; + auto beginStateFade = [&app](AppState targetState, bool armGameplayCountdown) { + if (!app.ctx.stateManager) { + return; + } + if (app.state == targetState) { + return; + } + if (app.menuFadePhase != AppRuntime::MenuFadePhase::None) { + return; + } + + app.menuFadePhase = AppRuntime::MenuFadePhase::FadeOut; + app.menuFadeClockMs = 0.0; + app.menuFadeAlpha = 0.0f; + app.menuFadeTarget = targetState; + app.menuPlayCountdownArmed = armGameplayCountdown; + app.gameplayCountdownActive = false; + app.gameplayCountdownIndex = 0; + app.gameplayCountdownElapsed = 0.0; + + if (!armGameplayCountdown) { + if (app.game) { + app.game->setPaused(false); + } + } + }; + + auto startMenuPlayTransition = [&app, beginStateFade]() { + if (!app.ctx.stateManager) { + return; + } + if (app.state != AppState::Menu) { + app.state = AppState::Playing; + app.ctx.stateManager->setState(app.state); + return; + } + beginStateFade(AppState::Playing, true); + }; + app.ctx.startPlayTransition = startMenuPlayTransition; + + auto requestStateFade = [&app, startMenuPlayTransition, beginStateFade](AppState targetState) { + if (!app.ctx.stateManager) { + return; + } + if (targetState == AppState::Playing) { + startMenuPlayTransition(); + return; + } + beginStateFade(targetState, false); + }; + app.ctx.requestFadeTransition = requestStateFade; + + // Instantiate state objects + app.loadingState = std::make_unique(app.ctx); + app.menuState = std::make_unique(app.ctx); + app.optionsState = std::make_unique(app.ctx); + app.levelSelectorState = std::make_unique(app.ctx); + app.playingState = std::make_unique(app.ctx); + + // Register handlers and lifecycle hooks + app.stateMgr->registerHandler(AppState::Loading, [&app](const SDL_Event& e){ app.loadingState->handleEvent(e); }); + app.stateMgr->registerOnEnter(AppState::Loading, [&app](){ app.loadingState->onEnter(); app.loadingStarted.store(true); }); + app.stateMgr->registerOnExit(AppState::Loading, [&app](){ app.loadingState->onExit(); }); + + app.stateMgr->registerHandler(AppState::Menu, [&app](const SDL_Event& e){ app.menuState->handleEvent(e); }); + app.stateMgr->registerOnEnter(AppState::Menu, [&app](){ app.menuState->onEnter(); }); + app.stateMgr->registerOnExit(AppState::Menu, [&app](){ app.menuState->onExit(); }); + + app.stateMgr->registerHandler(AppState::Options, [&app](const SDL_Event& e){ app.optionsState->handleEvent(e); }); + app.stateMgr->registerOnEnter(AppState::Options, [&app](){ app.optionsState->onEnter(); }); + app.stateMgr->registerOnExit(AppState::Options, [&app](){ app.optionsState->onExit(); }); + + app.stateMgr->registerHandler(AppState::LevelSelector, [&app](const SDL_Event& e){ app.levelSelectorState->handleEvent(e); }); + app.stateMgr->registerOnEnter(AppState::LevelSelector, [&app](){ app.levelSelectorState->onEnter(); }); + app.stateMgr->registerOnExit(AppState::LevelSelector, [&app](){ app.levelSelectorState->onExit(); }); + + app.stateMgr->registerHandler(AppState::Playing, [&app](const SDL_Event& e){ app.playingState->handleEvent(e); }); + app.stateMgr->registerOnEnter(AppState::Playing, [&app](){ app.playingState->onEnter(); }); + app.stateMgr->registerOnExit(AppState::Playing, [&app](){ app.playingState->onExit(); }); + + // Manually trigger the initial Loading state's onEnter + app.loadingState->onEnter(); + app.loadingStarted.store(true); + + return 0; +} + +static void runGameLoop(AppRuntime& app) +{ + using MenuFadePhase = AppRuntime::MenuFadePhase; + + SDL_Window* window = app.window; + SDL_Renderer* renderer = app.renderer; + AssetLoader& assetLoader = app.assetLoader; + LoadingManager& loadingManager = *app.loadingManager; + TextureLoader& textureLoader = *app.textureLoader; + FontAtlas& pixelFont = app.pixelFont; + FontAtlas& font = app.font; + ScoreManager& scores = app.scores; + std::atomic& scoresLoadComplete = app.scoresLoadComplete; + std::jthread& scoreLoader = app.scoreLoader; + std::jthread& menuTrackLoader = app.menuTrackLoader; + Starfield& starfield = app.starfield; + Starfield3D& starfield3D = app.starfield3D; + SpaceWarp& spaceWarp = app.spaceWarp; + SpaceWarpFlightMode& warpFlightMode = app.warpFlightMode; + bool& warpAutoPilotEnabled = app.warpAutoPilotEnabled; + LineEffect& lineEffect = app.lineEffect; + SDL_Texture*& logoTex = app.logoTex; + int& logoSmallW = app.logoSmallW; + int& logoSmallH = app.logoSmallH; + SDL_Texture*& logoSmallTex = app.logoSmallTex; + SDL_Texture*& backgroundTex = app.backgroundTex; + int& mainScreenW = app.mainScreenW; + int& mainScreenH = app.mainScreenH; + SDL_Texture*& mainScreenTex = app.mainScreenTex; + BackgroundManager& levelBackgrounds = app.levelBackgrounds; + int& startLevelSelection = app.startLevelSelection; + SDL_Texture*& blocksTex = app.blocksTex; + SDL_Texture*& scorePanelTex = app.scorePanelTex; + SDL_Texture*& statisticsPanelTex = app.statisticsPanelTex; + SDL_Texture*& nextPanelTex = app.nextPanelTex; + int& totalTracks = app.totalTracks; + int& currentTrackLoading = app.currentTrackLoading; + bool& musicLoaded = app.musicLoaded; + bool& musicStarted = app.musicStarted; + bool& musicLoadingStarted = app.musicLoadingStarted; + std::atomic_bool& g_loadingStarted = app.loadingStarted; + std::atomic_bool& g_loadingComplete = app.loadingComplete; + std::atomic& g_loadingStep = app.loadingStep; + Game& game = *app.game; + bool& suppressLineVoiceForLevelUp = app.suppressLineVoiceForLevelUp; + AppState& state = app.state; + double& loadingProgress = app.loadingProgress; + Uint64& loadStart = app.loadStart; + bool& running = app.running; + bool& isFullscreen = app.isFullscreen; + bool& leftHeld = app.leftHeld; + bool& rightHeld = app.rightHeld; + double& moveTimerMs = app.moveTimerMs; + const double DAS = app.DAS; + const double ARR = app.ARR; + SDL_Rect& logicalVP = app.logicalVP; + float& logicalScale = app.logicalScale; + Uint64& lastMs = app.lastMs; + AppRuntime::MenuFadePhase& menuFadePhase = app.menuFadePhase; + double& menuFadeClockMs = app.menuFadeClockMs; + float& menuFadeAlpha = app.menuFadeAlpha; + const double MENU_PLAY_FADE_DURATION_MS = app.MENU_PLAY_FADE_DURATION_MS; + AppState& menuFadeTarget = app.menuFadeTarget; + bool& menuPlayCountdownArmed = app.menuPlayCountdownArmed; + bool& gameplayCountdownActive = app.gameplayCountdownActive; + double& gameplayCountdownElapsed = app.gameplayCountdownElapsed; + int& gameplayCountdownIndex = app.gameplayCountdownIndex; + const double GAMEPLAY_COUNTDOWN_STEP_MS = app.GAMEPLAY_COUNTDOWN_STEP_MS; + const std::array& GAMEPLAY_COUNTDOWN_LABELS = app.GAMEPLAY_COUNTDOWN_LABELS; + double& gameplayBackgroundClockMs = app.gameplayBackgroundClockMs; + StateManager& stateMgr = *app.stateMgr; + StateContext& ctx = app.ctx; + auto& loadingState = app.loadingState; + auto& menuState = app.menuState; + auto& optionsState = app.optionsState; + auto& levelSelectorState = app.levelSelectorState; + auto& playingState = app.playingState; + auto ensureScoresLoaded = [&]() { if (scoreLoader.joinable()) { scoreLoader.join(); @@ -399,91 +617,17 @@ int main(int, char **) } }; - auto beginStateFade = [&](AppState targetState, bool armGameplayCountdown) { - if (!ctx.stateManager) { - return; - } - if (state == targetState) { - return; - } - if (menuFadePhase != MenuFadePhase::None) { - return; - } - - menuFadePhase = MenuFadePhase::FadeOut; - menuFadeClockMs = 0.0; - menuFadeAlpha = 0.0f; - menuFadeTarget = targetState; - menuPlayCountdownArmed = armGameplayCountdown; - gameplayCountdownActive = false; - gameplayCountdownIndex = 0; - gameplayCountdownElapsed = 0.0; - - if (!armGameplayCountdown) { - game.setPaused(false); - } - }; - auto startMenuPlayTransition = [&]() { - if (!ctx.stateManager) { - return; + if (ctx.startPlayTransition) { + ctx.startPlayTransition(); } - if (state != AppState::Menu) { - state = AppState::Playing; - ctx.stateManager->setState(state); - return; - } - beginStateFade(AppState::Playing, true); }; - ctx.startPlayTransition = startMenuPlayTransition; auto requestStateFade = [&](AppState targetState) { - if (!ctx.stateManager) { - return; + if (ctx.requestFadeTransition) { + ctx.requestFadeTransition(targetState); } - if (targetState == AppState::Playing) { - startMenuPlayTransition(); - return; - } - beginStateFade(targetState, false); }; - ctx.requestFadeTransition = requestStateFade; - - // Instantiate state objects - auto loadingState = std::make_unique(ctx); - auto menuState = std::make_unique(ctx); - auto optionsState = std::make_unique(ctx); - auto levelSelectorState = std::make_unique(ctx); - auto playingState = std::make_unique(ctx); - - // Register handlers and lifecycle hooks - stateMgr.registerHandler(AppState::Loading, [&](const SDL_Event& e){ loadingState->handleEvent(e); }); - stateMgr.registerOnEnter(AppState::Loading, [&](){ loadingState->onEnter(); g_loadingStarted.store(true); }); - stateMgr.registerOnExit(AppState::Loading, [&](){ loadingState->onExit(); }); - - stateMgr.registerHandler(AppState::Menu, [&](const SDL_Event& e){ menuState->handleEvent(e); }); - stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); }); - stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); }); - - stateMgr.registerHandler(AppState::Options, [&](const SDL_Event& e){ optionsState->handleEvent(e); }); - stateMgr.registerOnEnter(AppState::Options, [&](){ optionsState->onEnter(); }); - stateMgr.registerOnExit(AppState::Options, [&](){ optionsState->onExit(); }); - - stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); }); - stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); }); - stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); }); - - // Combined Playing state handler: run playingState handler - stateMgr.registerHandler(AppState::Playing, [&](const SDL_Event& e){ - // First give the PlayingState a chance to handle the event - playingState->handleEvent(e); - }); - stateMgr.registerOnEnter(AppState::Playing, [&](){ playingState->onEnter(); }); - stateMgr.registerOnExit(AppState::Playing, [&](){ playingState->onExit(); }); - - // Manually trigger the initial Loading state's onEnter - loadingState->onEnter(); - g_loadingStarted.store(true); // Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later while (running) @@ -1735,37 +1879,75 @@ int main(int, char **) SDL_RenderPresent(renderer); SDL_SetRenderScale(renderer, 1.f, 1.f); } - if (logoTex) - SDL_DestroyTexture(logoTex); - if (mainScreenTex) - SDL_DestroyTexture(mainScreenTex); - levelBackgrounds.reset(); - if (blocksTex) - SDL_DestroyTexture(blocksTex); - if (scorePanelTex) - SDL_DestroyTexture(scorePanelTex); - if (logoSmallTex) - SDL_DestroyTexture(logoSmallTex); - +} + +static void shutdownApp(AppRuntime& app) +{ // Save settings on exit Settings::instance().save(); - - if (scoreLoader.joinable()) { - scoreLoader.join(); - if (!ctx.scores) { - ctx.scores = &scores; + + if (app.logoTex) { + SDL_DestroyTexture(app.logoTex); + app.logoTex = nullptr; + } + if (app.mainScreenTex) { + SDL_DestroyTexture(app.mainScreenTex); + app.mainScreenTex = nullptr; + } + app.levelBackgrounds.reset(); + if (app.blocksTex) { + SDL_DestroyTexture(app.blocksTex); + app.blocksTex = nullptr; + } + if (app.scorePanelTex) { + SDL_DestroyTexture(app.scorePanelTex); + app.scorePanelTex = nullptr; + } + if (app.logoSmallTex) { + SDL_DestroyTexture(app.logoSmallTex); + app.logoSmallTex = nullptr; + } + + if (app.scoreLoader.joinable()) { + app.scoreLoader.join(); + if (!app.ctx.scores) { + app.ctx.scores = &app.scores; } } - if (menuTrackLoader.joinable()) { - menuTrackLoader.join(); + if (app.menuTrackLoader.joinable()) { + app.menuTrackLoader.join(); } - lineEffect.shutdown(); + + app.lineEffect.shutdown(); Audio::instance().shutdown(); SoundEffectManager::instance().shutdown(); - font.shutdown(); + app.font.shutdown(); + TTF_Quit(); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); + + if (app.renderer) { + SDL_DestroyRenderer(app.renderer); + app.renderer = nullptr; + } + if (app.window) { + SDL_DestroyWindow(app.window); + app.window = nullptr; + } SDL_Quit(); +} + +} // namespace + +int main(int, char **) +{ + AppRuntime app; + const int initRc = initApp(app); + if (initRc != 0) { + shutdownApp(app); + return initRc; + } + + runGameLoop(app); + shutdownApp(app); return 0; }