fixed main screen
This commit is contained in:
@ -55,6 +55,8 @@ set(TETRIS_SOURCES
|
|||||||
src/ui/MenuLayout.cpp
|
src/ui/MenuLayout.cpp
|
||||||
src/app/BackgroundManager.cpp
|
src/app/BackgroundManager.cpp
|
||||||
src/app/Fireworks.cpp
|
src/app/Fireworks.cpp
|
||||||
|
src/app/AssetLoader.cpp
|
||||||
|
src/states/LoadingManager.cpp
|
||||||
# State implementations (new)
|
# State implementations (new)
|
||||||
src/states/LoadingState.cpp
|
src/states/LoadingState.cpp
|
||||||
src/states/MenuState.cpp
|
src/states/MenuState.cpp
|
||||||
|
|||||||
122
src/app/AssetLoader.cpp
Normal file
122
src/app/AssetLoader.cpp
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#include "app/AssetLoader.h"
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
AssetLoader::AssetLoader() = default;
|
||||||
|
|
||||||
|
AssetLoader::~AssetLoader() {
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetLoader::init(SDL_Renderer* renderer) {
|
||||||
|
m_renderer = renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetLoader::shutdown() {
|
||||||
|
// Destroy textures
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_texturesMutex);
|
||||||
|
for (auto &p : m_textures) {
|
||||||
|
if (p.second) SDL_DestroyTexture(p.second);
|
||||||
|
}
|
||||||
|
m_textures.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear queue and errors
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_queueMutex);
|
||||||
|
m_queue.clear();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_errorsMutex);
|
||||||
|
m_errors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_totalTasks = 0;
|
||||||
|
m_loadedTasks = 0;
|
||||||
|
m_renderer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetLoader::setBasePath(const std::string& basePath) {
|
||||||
|
m_basePath = basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetLoader::queueTexture(const std::string& path) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_queueMutex);
|
||||||
|
m_queue.push_back(path);
|
||||||
|
}
|
||||||
|
m_totalTasks.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssetLoader::performStep() {
|
||||||
|
std::string path;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_queueMutex);
|
||||||
|
if (m_queue.empty()) return true;
|
||||||
|
path = m_queue.front();
|
||||||
|
m_queue.erase(m_queue.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_currentLoadingMutex);
|
||||||
|
m_currentLoading = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fullPath = m_basePath.empty() ? path : (m_basePath + "/" + path);
|
||||||
|
|
||||||
|
SDL_Surface* surf = IMG_Load(fullPath.c_str());
|
||||||
|
if (!surf) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_errorsMutex);
|
||||||
|
m_errors.push_back(std::string("IMG_Load failed: ") + fullPath + " -> " + SDL_GetError());
|
||||||
|
} else {
|
||||||
|
SDL_Texture* tex = SDL_CreateTextureFromSurface(m_renderer, surf);
|
||||||
|
SDL_DestroySurface(surf);
|
||||||
|
if (!tex) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_errorsMutex);
|
||||||
|
m_errors.push_back(std::string("CreateTexture failed: ") + fullPath);
|
||||||
|
} else {
|
||||||
|
std::lock_guard<std::mutex> lk(m_texturesMutex);
|
||||||
|
m_textures[path] = tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_loadedTasks.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_currentLoadingMutex);
|
||||||
|
m_currentLoading.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true when no more queued tasks
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_queueMutex);
|
||||||
|
return m_queue.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float AssetLoader::getProgress() const {
|
||||||
|
int total = m_totalTasks.load(std::memory_order_relaxed);
|
||||||
|
if (total <= 0) return 1.0f;
|
||||||
|
int loaded = m_loadedTasks.load(std::memory_order_relaxed);
|
||||||
|
return static_cast<float>(loaded) / static_cast<float>(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> AssetLoader::getAndClearErrors() {
|
||||||
|
std::lock_guard<std::mutex> lk(m_errorsMutex);
|
||||||
|
std::vector<std::string> out = m_errors;
|
||||||
|
m_errors.clear();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Texture* AssetLoader::getTexture(const std::string& path) const {
|
||||||
|
std::lock_guard<std::mutex> lk(m_texturesMutex);
|
||||||
|
auto it = m_textures.find(path);
|
||||||
|
if (it == m_textures.end()) return nullptr;
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AssetLoader::getCurrentLoading() const {
|
||||||
|
std::lock_guard<std::mutex> lk(m_currentLoadingMutex);
|
||||||
|
return m_currentLoading;
|
||||||
|
}
|
||||||
64
src/app/AssetLoader.h
Normal file
64
src/app/AssetLoader.h
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
// Lightweight AssetLoader scaffold.
|
||||||
|
// Responsibilities:
|
||||||
|
// - Queue textures to load (main thread) and perform incremental loads via performStep().
|
||||||
|
// - Store loaded SDL_Texture* instances and provide accessors.
|
||||||
|
// - Collect loading errors thread-safely.
|
||||||
|
// NOTE: All SDL texture creation MUST happen on the thread that owns the SDL_Renderer.
|
||||||
|
class AssetLoader {
|
||||||
|
public:
|
||||||
|
AssetLoader();
|
||||||
|
~AssetLoader();
|
||||||
|
|
||||||
|
void init(SDL_Renderer* renderer);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
void setBasePath(const std::string& basePath);
|
||||||
|
|
||||||
|
// Queue a texture path (relative to base path) for loading.
|
||||||
|
void queueTexture(const std::string& path);
|
||||||
|
|
||||||
|
// Perform a single loading step (load one queued asset).
|
||||||
|
// Returns true when all queued tasks are complete, false otherwise.
|
||||||
|
bool performStep();
|
||||||
|
|
||||||
|
// Progress in [0,1]. If no tasks, returns 1.0f.
|
||||||
|
float getProgress() const;
|
||||||
|
|
||||||
|
// Retrieve and clear accumulated error messages.
|
||||||
|
std::vector<std::string> getAndClearErrors();
|
||||||
|
|
||||||
|
// Get a loaded texture (or nullptr if not loaded).
|
||||||
|
SDL_Texture* getTexture(const std::string& path) const;
|
||||||
|
|
||||||
|
// Return currently-loading path (empty when idle).
|
||||||
|
std::string getCurrentLoading() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_Renderer* m_renderer = nullptr;
|
||||||
|
std::string m_basePath;
|
||||||
|
|
||||||
|
// queued paths (simple FIFO)
|
||||||
|
std::vector<std::string> m_queue;
|
||||||
|
mutable std::mutex m_queueMutex;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, SDL_Texture*> m_textures;
|
||||||
|
mutable std::mutex m_texturesMutex;
|
||||||
|
|
||||||
|
std::vector<std::string> m_errors;
|
||||||
|
mutable std::mutex m_errorsMutex;
|
||||||
|
|
||||||
|
std::atomic<int> m_totalTasks{0};
|
||||||
|
std::atomic<int> m_loadedTasks{0};
|
||||||
|
|
||||||
|
std::string m_currentLoading;
|
||||||
|
mutable std::mutex m_currentLoadingMutex;
|
||||||
|
};
|
||||||
359
src/main.cpp
359
src/main.cpp
@ -38,6 +38,8 @@
|
|||||||
#include "states/LevelSelectorState.h"
|
#include "states/LevelSelectorState.h"
|
||||||
#include "states/PlayingState.h"
|
#include "states/PlayingState.h"
|
||||||
#include "audio/MenuWrappers.h"
|
#include "audio/MenuWrappers.h"
|
||||||
|
#include "app/AssetLoader.h"
|
||||||
|
#include "states/LoadingManager.h"
|
||||||
#include "utils/ImagePathResolver.h"
|
#include "utils/ImagePathResolver.h"
|
||||||
#include "graphics/renderers/GameRenderer.h"
|
#include "graphics/renderers/GameRenderer.h"
|
||||||
#include "core/Config.h"
|
#include "core/Config.h"
|
||||||
@ -486,6 +488,11 @@ int main(int, char **)
|
|||||||
SDL_GetError());
|
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)
|
// Font and UI asset handles (actual loading deferred until Loading state)
|
||||||
FontAtlas pixelFont;
|
FontAtlas pixelFont;
|
||||||
FontAtlas font;
|
FontAtlas font;
|
||||||
@ -544,108 +551,10 @@ int main(int, char **)
|
|||||||
std::atomic_bool g_loadingComplete{false};
|
std::atomic_bool g_loadingComplete{false};
|
||||||
std::atomic<size_t> g_loadingStep{0};
|
std::atomic<size_t> g_loadingStep{0};
|
||||||
|
|
||||||
// Define performLoadingStep to execute one load operation per frame on main thread
|
// Loading is now handled by AssetLoader + LoadingManager.
|
||||||
auto performLoadingStep = [&]() -> bool {
|
// Old incremental lambda removed; use LoadingManager to queue texture loads and
|
||||||
size_t step = g_loadingStep.fetch_add(1);
|
// perform a single step per frame. Non-texture initialization (fonts, SFX)
|
||||||
|
// is performed on the first loading frame below when the loader is started.
|
||||||
// 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
|
|
||||||
};
|
|
||||||
|
|
||||||
Game game(startLevelSelection);
|
Game game(startLevelSelection);
|
||||||
// Apply global gravity speed multiplier from config
|
// Apply global gravity speed multiplier from config
|
||||||
@ -1261,9 +1170,10 @@ int main(int, char **)
|
|||||||
currentTrackLoading = Audio::instance().getLoadedTrackCount();
|
currentTrackLoading = Audio::instance().getLoadedTrackCount();
|
||||||
if (Audio::instance().isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) {
|
if (Audio::instance().isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) {
|
||||||
Audio::instance().shuffle();
|
Audio::instance().shuffle();
|
||||||
if (musicEnabled) {
|
// Defer starting playback until the app has entered the Menu/Playing state.
|
||||||
Audio::instance().start();
|
// 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;
|
musicLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1300,17 +1210,168 @@ int main(int, char **)
|
|||||||
}
|
}
|
||||||
else if (state == AppState::Loading)
|
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 (g_loadingStarted.load() && !g_loadingComplete.load()) {
|
||||||
if (performLoadingStep()) {
|
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);
|
g_loadingComplete.store(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer task-based progress if we have tasks registered
|
// Prefer task-based progress if we have tasks registered
|
||||||
const int totalTasks = g_totalLoadingTasks.load(std::memory_order_acquire);
|
const int totalTasks = g_totalLoadingTasks.load(std::memory_order_acquire);
|
||||||
const int musicDone = std::min(totalTracks, currentTrackLoading);
|
const int musicDone = std::min(totalTracks, currentTrackLoading);
|
||||||
int doneTasks = g_loadedTasks.load(std::memory_order_acquire) + musicDone;
|
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 (doneTasks > totalTasks) doneTasks = totalTasks;
|
||||||
if (totalTasks > 0) {
|
if (totalTasks > 0) {
|
||||||
loadingProgress = std::min(1.0, double(doneTasks) / double(totalTasks));
|
loadingProgress = std::min(1.0, double(doneTasks) / double(totalTasks));
|
||||||
@ -1696,8 +1757,53 @@ int main(int, char **)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AppState::Menu:
|
case AppState::Menu:
|
||||||
// Delegate full menu rendering to MenuState object now
|
// 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);
|
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;
|
break;
|
||||||
case AppState::Options:
|
case AppState::Options:
|
||||||
optionsState->render(renderer, logicalScale, logicalVP);
|
optionsState->render(renderer, logicalScale, logicalVP);
|
||||||
@ -1924,43 +2030,6 @@ int main(int, char **)
|
|||||||
HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY);
|
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_RenderPresent(renderer);
|
||||||
SDL_SetRenderScale(renderer, 1.f, 1.f);
|
SDL_SetRenderScale(renderer, 1.f, 1.f);
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/states/LoadingManager.cpp
Normal file
39
src/states/LoadingManager.cpp
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#include "states/LoadingManager.h"
|
||||||
|
#include "app/AssetLoader.h"
|
||||||
|
|
||||||
|
LoadingManager::LoadingManager(AssetLoader* loader)
|
||||||
|
: m_loader(loader)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadingManager::queueTexture(const std::string& path) {
|
||||||
|
if (!m_loader) return;
|
||||||
|
m_loader->queueTexture(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadingManager::start() {
|
||||||
|
m_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadingManager::update() {
|
||||||
|
if (!m_loader) return true;
|
||||||
|
// perform a single step on the loader; AssetLoader::performStep returns true when
|
||||||
|
// there are no more queued tasks.
|
||||||
|
bool done = m_loader->performStep();
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
float LoadingManager::getProgress() const {
|
||||||
|
if (!m_loader) return 1.0f;
|
||||||
|
return m_loader->getProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> LoadingManager::getAndClearErrors() {
|
||||||
|
if (!m_loader) return {};
|
||||||
|
return m_loader->getAndClearErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LoadingManager::getCurrentLoading() const {
|
||||||
|
if (!m_loader) return {};
|
||||||
|
return m_loader->getCurrentLoading();
|
||||||
|
}
|
||||||
36
src/states/LoadingManager.h
Normal file
36
src/states/LoadingManager.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class AssetLoader;
|
||||||
|
|
||||||
|
// LoadingManager: thin facade over AssetLoader for incremental loading.
|
||||||
|
// Main thread only. Call update() once per frame to perform a single step.
|
||||||
|
class LoadingManager {
|
||||||
|
public:
|
||||||
|
explicit LoadingManager(AssetLoader* loader);
|
||||||
|
|
||||||
|
// Queue a texture path (relative to base path) for loading.
|
||||||
|
void queueTexture(const std::string& path);
|
||||||
|
|
||||||
|
// Start loading (idempotent).
|
||||||
|
void start();
|
||||||
|
|
||||||
|
// Perform a single loading step. Returns true when loading complete.
|
||||||
|
bool update();
|
||||||
|
|
||||||
|
// Progress in [0,1]
|
||||||
|
float getProgress() const;
|
||||||
|
|
||||||
|
// Return and clear any accumulated loading errors.
|
||||||
|
std::vector<std::string> getAndClearErrors();
|
||||||
|
|
||||||
|
// Current path being loaded (or empty)
|
||||||
|
std::string getCurrentLoading() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
AssetLoader* m_loader = nullptr;
|
||||||
|
bool m_started = false;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user