refactor main.cpp
This commit is contained in:
718
src/main.cpp
718
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> 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()));
|
||||
|
||||
|
||||
// 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<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);
|
||||
|
||||
// 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<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
|
||||
|
||||
// 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"};
|
||||
|
||||
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<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();
|
||||
|
||||
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.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<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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user