refactor main.cpp

This commit is contained in:
2025-12-19 19:25:56 +01:00
parent 64fce596ce
commit 783c12790d

View File

@ -117,8 +117,106 @@ 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> loadingManager;
std::unique_ptr<TextureLoader> textureLoader;
FontAtlas pixelFont;
FontAtlas font;
ScoreManager scores;
std::atomic<bool> 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<size_t> loadingStep{0};
std::unique_ptr<Game> game;
std::vector<std::string> singleSounds;
std::vector<std::string> doubleSounds;
std::vector<std::string> tripleSounds;
std::vector<std::string> 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<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
double gameplayBackgroundClockMs = 0.0;
std::unique_ptr<StateManager> stateMgr;
StateContext ctx{};
std::unique_ptr<LoadingState> loadingState;
std::unique_ptr<MenuState> menuState;
std::unique_ptr<OptionsState> optionsState;
std::unique_ptr<LevelSelectorState> levelSelectorState;
std::unique_ptr<PlayingState> playingState;
};
static int initApp(AppRuntime& app)
{
// Initialize random seed for procedural effects
srand(static_cast<unsigned int>(SDL_GetTicks()));
@ -153,24 +251,25 @@ int main(int, char **)
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<LoadingManager>(&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<TextureLoader>(
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<bool> 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);
app.lineEffect.init(app.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;
// 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<size_t> 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<Game>(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
app.game->setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER);
app.game->reset(app.startLevelSelection);
// Define voice line banks for gameplay callbacks
std::vector<std::string> singleSounds = {"well_played", "smooth_clear", "great_move"};
std::vector<std::string> doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"};
std::vector<std::string> tripleSounds = {"impressive", "triple_strike"};
std::vector<std::string> tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"};
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;
bool suppressLineVoiceForLevelUp = false;
auto playVoiceCue = [&](int linesCleared) {
auto playVoiceCue = [&app](int linesCleared) {
const std::vector<std::string>* 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();
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();
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<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
double gameplayBackgroundClockMs = 0.0;
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<StateManager>(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<LoadingState>(app.ctx);
app.menuState = std::make_unique<MenuState>(app.ctx);
app.optionsState = std::make_unique<OptionsState>(app.ctx);
app.levelSelectorState = std::make_unique<LevelSelectorState>(app.ctx);
app.playingState = std::make_unique<PlayingState>(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<bool>& 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<size_t>& 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<const char*, 4>& 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<LoadingState>(ctx);
auto menuState = std::make_unique<MenuState>(ctx);
auto optionsState = std::make_unique<OptionsState>(ctx);
auto levelSelectorState = std::make_unique<LevelSelectorState>(ctx);
auto playingState = std::make_unique<PlayingState>(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;
}