fixed main screen

This commit is contained in:
2025-12-17 18:13:59 +01:00
parent 3264672be0
commit cecf5cf68e
6 changed files with 479 additions and 147 deletions

View File

@ -38,6 +38,8 @@
#include "states/LevelSelectorState.h"
#include "states/PlayingState.h"
#include "audio/MenuWrappers.h"
#include "app/AssetLoader.h"
#include "states/LoadingManager.h"
#include "utils/ImagePathResolver.h"
#include "graphics/renderers/GameRenderer.h"
#include "core/Config.h"
@ -486,6 +488,11 @@ int main(int, char **)
SDL_GetError());
}
// Asset loader (creates SDL_Textures on the main thread)
AssetLoader assetLoader;
assetLoader.init(renderer);
LoadingManager loadingManager(&assetLoader);
// Font and UI asset handles (actual loading deferred until Loading state)
FontAtlas pixelFont;
FontAtlas font;
@ -544,108 +551,10 @@ int main(int, char **)
std::atomic_bool g_loadingComplete{false};
std::atomic<size_t> g_loadingStep{0};
// Define performLoadingStep to execute one load operation per frame on main thread
auto performLoadingStep = [&]() -> bool {
size_t step = g_loadingStep.fetch_add(1);
// Initialize counters on first step
if (step == 0) {
constexpr int baseTasks = 25; // 2 fonts + 2 logos + 1 main + 1 blocks + 3 panels + 16 SFX
g_totalLoadingTasks.store(baseTasks);
g_loadedTasks.store(0);
{
std::lock_guard<std::mutex> lk(g_assetLoadErrorsMutex);
g_assetLoadErrors.clear();
}
{
std::lock_guard<std::mutex> lk(g_currentLoadingMutex);
g_currentLoadingFile.clear();
}
// Initialize background music loading
Audio::instance().init();
for (int i = 1; i <= 100; ++i) {
char base[128];
std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
if (path.empty()) break;
Audio::instance().addTrackAsync(path);
totalTracks++;
}
// Expand task budget to account for music tracks so the loading bar waits for them
g_totalLoadingTasks.store(baseTasks + totalTracks);
if (totalTracks > 0) {
Audio::instance().startBackgroundLoading();
musicLoadingStarted = true;
} else {
// No music files found, mark as loaded so game can continue
musicLoaded = true;
}
}
// Execute one load operation per step
switch (step) {
case 0: return false; // Init step
case 1: pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22); g_loadedTasks.fetch_add(1); break;
case 2: font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20); g_loadedTasks.fetch_add(1); break;
case 3: logoTex = loadTextureFromImage(renderer, "assets/images/spacetris.png"); break;
case 4: logoSmallTex = loadTextureFromImage(renderer, "assets/images/spacetris.png", &logoSmallW, &logoSmallH); break;
case 5: mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH);
if (mainScreenTex) SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); break;
case 6:
blocksTex = loadTextureFromImage(renderer, "assets/images/blocks90px_001.bmp");
if (!blocksTex) {
blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90);
SDL_SetRenderTarget(renderer, blocksTex);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
for (int i = 0; i < PIECE_COUNT; ++i) {
SDL_Color c = COLORS[i + 1];
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
SDL_FRect rect{(float)(i * 90), 0, 90, 90};
SDL_RenderFillRect(renderer, &rect);
}
SDL_SetRenderTarget(renderer, nullptr);
g_loadedTasks.fetch_add(1);
}
break;
case 7: scorePanelTex = loadTextureFromImage(renderer, "assets/images/panel_score.png");
if (scorePanelTex) SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND); break;
case 8: statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png");
if (statisticsPanelTex) SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND); break;
case 9: nextPanelTex = loadTextureFromImage(renderer, "assets/images/next_panel.png");
if (nextPanelTex) SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND); break;
case 10: SoundEffectManager::instance().init(); g_loadedTasks.fetch_add(1); break;
// Audio loading steps
default: {
const std::vector<std::string> audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level"};
size_t audioIdx = step - 11;
if (audioIdx < audioIds.size()) {
std::string id = audioIds[audioIdx];
std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : id);
{
std::lock_guard<std::mutex> lk(g_currentLoadingMutex);
g_currentLoadingFile = basePath;
}
std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" });
if (!resolved.empty()) {
SoundEffectManager::instance().loadSound(id, resolved);
}
g_loadedTasks.fetch_add(1);
{
std::lock_guard<std::mutex> lk(g_currentLoadingMutex);
g_currentLoadingFile.clear();
}
} else {
// All done
return true;
}
break;
}
}
return false; // More steps remaining
};
// 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);
// Apply global gravity speed multiplier from config
@ -1261,9 +1170,10 @@ int main(int, char **)
currentTrackLoading = Audio::instance().getLoadedTrackCount();
if (Audio::instance().isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) {
Audio::instance().shuffle();
if (musicEnabled) {
Audio::instance().start();
}
// Defer starting playback until the app has entered the Menu/Playing state.
// Actual playback is started below when `musicLoaded` is observed and
// the state is Menu or Playing (so the user doesn't hear music while
// still on the Loading screen).
musicLoaded = true;
}
}
@ -1300,10 +1210,154 @@ int main(int, char **)
}
else if (state == AppState::Loading)
{
// Execute one loading step per frame on main thread
static int queuedTextureCount = 0;
// Execute one loading step per frame on main thread via LoadingManager
if (g_loadingStarted.load() && !g_loadingComplete.load()) {
if (performLoadingStep()) {
g_loadingComplete.store(true);
static bool queuedTextures = false;
static std::vector<std::string> queuedPaths;
if (!queuedTextures) {
queuedTextures = true;
// Initialize counters and clear previous errors
constexpr int baseTasks = 25; // keep same budget as before
g_totalLoadingTasks.store(baseTasks);
g_loadedTasks.store(0);
{
std::lock_guard<std::mutex> lk(g_assetLoadErrorsMutex);
g_assetLoadErrors.clear();
}
{
std::lock_guard<std::mutex> lk(g_currentLoadingMutex);
g_currentLoadingFile.clear();
}
// Initialize background music loading
Audio::instance().init();
totalTracks = 0;
for (int i = 1; i <= 100; ++i) {
char base[128];
std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
if (path.empty()) break;
Audio::instance().addTrackAsync(path);
totalTracks++;
}
g_totalLoadingTasks.store(baseTasks + totalTracks);
if (totalTracks > 0) {
Audio::instance().startBackgroundLoading();
musicLoadingStarted = true;
} else {
musicLoaded = true;
}
// Initialize fonts (synchronous, cheap)
pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22);
g_loadedTasks.fetch_add(1);
font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20);
g_loadedTasks.fetch_add(1);
// Queue UI textures for incremental loading
queuedPaths = {
"assets/images/spacetris.png",
"assets/images/spacetris.png", // small logo uses same source
"assets/images/main_screen.png",
"assets/images/blocks90px_001.bmp",
"assets/images/panel_score.png",
"assets/images/statistics_panel.png",
"assets/images/next_panel.png"
};
for (auto &p : queuedPaths) {
loadingManager.queueTexture(p);
}
queuedTextureCount = static_cast<int>(queuedPaths.size());
// Initialize sound effects manager (counts as a loaded task)
SoundEffectManager::instance().init();
g_loadedTasks.fetch_add(1);
// Load small set of voice/audio SFX synchronously for now (keeps behavior)
const std::vector<std::string> audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level"};
for (const auto &id : audioIds) {
std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : id);
{
std::lock_guard<std::mutex> lk(g_currentLoadingMutex);
g_currentLoadingFile = basePath;
}
std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" });
if (!resolved.empty()) {
SoundEffectManager::instance().loadSound(id, resolved);
}
g_loadedTasks.fetch_add(1);
{
std::lock_guard<std::mutex> lk(g_currentLoadingMutex);
g_currentLoadingFile.clear();
}
}
}
// Perform a single texture loading step via LoadingManager
bool texturesDone = loadingManager.update();
if (texturesDone) {
// Bind loaded textures into the runtime context
logoTex = assetLoader.getTexture("assets/images/spacetris.png");
logoSmallTex = assetLoader.getTexture("assets/images/spacetris.png");
mainScreenTex = assetLoader.getTexture("assets/images/main_screen.png");
blocksTex = assetLoader.getTexture("assets/images/blocks90px_001.bmp");
scorePanelTex = assetLoader.getTexture("assets/images/panel_score.png");
statisticsPanelTex = assetLoader.getTexture("assets/images/statistics_panel.png");
nextPanelTex = assetLoader.getTexture("assets/images/next_panel.png");
auto ensureTextureSize = [&](SDL_Texture* tex, int& outW, int& outH) {
if (!tex) return;
if (outW > 0 && outH > 0) return;
float w = 0.0f, h = 0.0f;
if (SDL_GetTextureSize(tex, &w, &h)) {
outW = static_cast<int>(std::lround(w));
outH = static_cast<int>(std::lround(h));
}
};
// If a texture was created by AssetLoader (not legacy IMG_Load),
// its stored width/height may still be 0. Query the real size.
ensureTextureSize(logoSmallTex, logoSmallW, logoSmallH);
ensureTextureSize(mainScreenTex, mainScreenW, mainScreenH);
// Fallback: if any critical UI texture failed to load via AssetLoader,
// load synchronously using the legacy helper so the Menu can render.
auto legacyLoad = [&](const std::string& p, SDL_Texture*& outTex, int* outW = nullptr, int* outH = nullptr) {
if (!outTex) {
outTex = loadTextureFromImage(renderer, p, outW, outH);
}
};
legacyLoad("assets/images/spacetris.png", logoTex);
legacyLoad("assets/images/spacetris.png", logoSmallTex, &logoSmallW, &logoSmallH);
legacyLoad("assets/images/main_screen.png", mainScreenTex, &mainScreenW, &mainScreenH);
legacyLoad("assets/images/blocks90px_001.bmp", blocksTex);
legacyLoad("assets/images/panel_score.png", scorePanelTex);
legacyLoad("assets/images/statistics_panel.png", statisticsPanelTex);
legacyLoad("assets/images/next_panel.png", nextPanelTex);
// If blocks texture failed, create fallback and count it as loaded
if (!blocksTex) {
blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90);
SDL_SetRenderTarget(renderer, blocksTex);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
for (int i = 0; i < PIECE_COUNT; ++i) {
SDL_Color c = COLORS[i + 1];
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
SDL_FRect rect{(float)(i * 90), 0, 90, 90};
SDL_RenderFillRect(renderer, &rect);
}
SDL_SetRenderTarget(renderer, nullptr);
// Do not update global task counter here; textures are accounted
// for via the LoadingManager/AssetLoader progress below.
}
// Mark loading complete when music also loaded
if (musicLoaded) {
g_loadingComplete.store(true);
}
}
}
@ -1311,6 +1365,13 @@ int main(int, char **)
const int totalTasks = g_totalLoadingTasks.load(std::memory_order_acquire);
const int musicDone = std::min(totalTracks, currentTrackLoading);
int doneTasks = g_loadedTasks.load(std::memory_order_acquire) + musicDone;
// Include texture progress reported by the LoadingManager/AssetLoader
if (queuedTextureCount > 0) {
float texProg = loadingManager.getProgress();
int texDone = static_cast<int>(std::floor(texProg * queuedTextureCount + 0.5f));
if (texDone > queuedTextureCount) texDone = queuedTextureCount;
doneTasks += texDone;
}
if (doneTasks > totalTasks) doneTasks = totalTasks;
if (totalTasks > 0) {
loadingProgress = std::min(1.0, double(doneTasks) / double(totalTasks));
@ -1696,8 +1757,53 @@ int main(int, char **)
}
break;
case AppState::Menu:
// Delegate full menu rendering to MenuState object now
menuState->render(renderer, logicalScale, logicalVP);
// Ensure overlay is loaded (drawn after highscores so it sits above that layer)
if (!mainScreenTex) {
mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH);
}
// Render menu content that should appear *behind* the overlay (highscores/logo).
// Bottom buttons are drawn separately on top.
if (menuState) {
menuState->drawMainButtonNormally = false;
menuState->render(renderer, logicalScale, logicalVP);
}
// Draw main screen overlay above highscores
if (mainScreenTex) {
SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderScale(renderer, 1.f, 1.f);
float texW = mainScreenW > 0 ? static_cast<float>(mainScreenW) : 0.0f;
float texH = mainScreenH > 0 ? static_cast<float>(mainScreenH) : 0.0f;
if (texW <= 0.0f || texH <= 0.0f) {
float iwf = 0.0f, ihf = 0.0f;
if (!SDL_GetTextureSize(mainScreenTex, &iwf, &ihf)) {
iwf = ihf = 0.0f;
}
texW = iwf;
texH = ihf;
}
if (texW > 0.0f && texH > 0.0f) {
const float drawH = static_cast<float>(winH);
const float scale = drawH / texH;
const float drawW = texW * scale;
SDL_FRect dst{
(winW - drawW) * 0.5f,
0.0f,
drawW,
drawH
};
SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND);
SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst);
}
SDL_SetRenderViewport(renderer, &logicalVP);
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
}
// Draw bottom menu buttons above the overlay
if (menuState) {
menuState->renderMainButtonTop(renderer, logicalScale, logicalVP);
}
break;
case AppState::Options:
optionsState->render(renderer, logicalScale, logicalVP);
@ -1924,43 +2030,6 @@ int main(int, char **)
HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY);
}
// Top-layer overlay: render `mainScreenTex` above all other layers when in Menu
if (state == AppState::Menu && mainScreenTex) {
SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderScale(renderer, 1.f, 1.f);
float texW = mainScreenW > 0 ? static_cast<float>(mainScreenW) : 0.0f;
float texH = mainScreenH > 0 ? static_cast<float>(mainScreenH) : 0.0f;
if (texW <= 0.0f || texH <= 0.0f) {
float iwf = 0.0f, ihf = 0.0f;
if (SDL_GetTextureSize(mainScreenTex, &iwf, &ihf) != 0) {
iwf = ihf = 0.0f;
}
texW = iwf;
texH = ihf;
}
if (texW > 0.0f && texH > 0.0f) {
const float drawH = static_cast<float>(winH);
const float scale = drawH / texH;
const float drawW = texW * scale;
SDL_FRect dst{
(winW - drawW) * 0.5f,
0.0f,
drawW,
drawH
};
SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND);
SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst);
}
// Restore logical viewport/scale and draw the main PLAY button above the overlay
SDL_SetRenderViewport(renderer, &logicalVP);
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
if (menuState) {
menuState->drawMainButtonNormally = false; // ensure it isn't double-drawn
menuState->renderMainButtonTop(renderer, logicalScale, logicalVP);
menuState->drawMainButtonNormally = true;
}
}
SDL_RenderPresent(renderer);
SDL_SetRenderScale(renderer, 1.f, 1.f);
}