diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ffbae0..eeedb29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable(tetris # New core architecture classes src/core/ApplicationManager.cpp src/core/InputManager.cpp + src/core/AssetManager.cpp src/graphics/RenderManager.cpp src/persistence/Scores.cpp src/graphics/Starfield.cpp @@ -125,6 +126,7 @@ add_executable(tetris_refactored # New core architecture classes src/core/ApplicationManager.cpp src/core/InputManager.cpp + src/core/AssetManager.cpp src/graphics/RenderManager.cpp src/persistence/Scores.cpp src/graphics/Starfield.cpp diff --git a/REFACTORING_TODO.md b/REFACTORING_TODO.md index 4339fbd..85e33f3 100644 --- a/REFACTORING_TODO.md +++ b/REFACTORING_TODO.md @@ -44,9 +44,9 @@ - [ ] Define window size constants - [ ] Set up gameplay timing constants -## 🔧 Phase 2: Core Systems (Week 3-4) - HIGH PRIORITY +## 🔧 Phase 2: Core Systems (Week 3-4) - HIGH PRIORITY ✅ **COMPLETED** -### Input Management +### Input Management ✅ **COMPLETED** - [x] Create `InputManager` class - [x] Implement keyboard state tracking - [x] Add mouse input handling @@ -60,19 +60,20 @@ - [x] Simplify main loop event processing - [x] Test all input scenarios (keyboard, mouse, gamepad) -### Asset Management -- [ ] Create `AssetManager` class - - [ ] Design resource loading interface - - [ ] Implement texture management - - [ ] Add font loading and management - - [ ] Create sound asset handling - - [ ] Add resource cleanup and error handling +### Asset Management ✅ **COMPLETED** -- [ ] Migrate existing asset loading - - [ ] Move texture loading from main() to AssetManager - - [ ] Convert font initialization to use AssetManager - - [ ] Update StateContext to use AssetManager - - [ ] Test asset loading and memory management +- [x] Create `AssetManager` class + - [x] Design resource loading interface + - [x] Implement texture management + - [x] Add font loading and management + - [x] Create sound asset handling + - [x] Add resource cleanup and error handling + +- [x] Migrate existing asset loading + - [x] Move texture loading from main() to AssetManager + - [x] Convert font initialization to use AssetManager + - [x] Update StateContext to use AssetManager + - [x] Test asset loading and memory management ### State System Enhancement - [ ] Implement `IGameState` interface diff --git a/src/core/ApplicationManager.cpp b/src/core/ApplicationManager.cpp index 52cb560..5d5ec5f 100644 --- a/src/core/ApplicationManager.cpp +++ b/src/core/ApplicationManager.cpp @@ -1,6 +1,7 @@ #include "ApplicationManager.h" #include "StateManager.h" #include "InputManager.h" +#include "AssetManager.h" #include "../graphics/RenderManager.h" #include #include @@ -134,6 +135,13 @@ bool ApplicationManager::initializeManagers() { return false; } + // Create and initialize AssetManager + m_assetManager = std::make_unique(); + if (!m_assetManager->initialize(m_renderManager->getSDLRenderer())) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize AssetManager"); + return false; + } + // Create StateManager (will be enhanced in next steps) m_stateManager = std::make_unique(AppState::Loading); @@ -142,20 +150,62 @@ bool ApplicationManager::initializeManagers() { } bool ApplicationManager::initializeGame() { - // TODO: Initialize game-specific systems - // For now, just set a basic loading state to test the window + // Load essential assets using AssetManager + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading essential assets..."); - // Initialize a basic test render handler + // Set up asset loading tasks + AssetManager::LoadingTask logoTask{AssetManager::LoadingTask::TEXTURE, "logo", "assets/images/logo.bmp"}; + AssetManager::LoadingTask backgroundTask{AssetManager::LoadingTask::TEXTURE, "background", "assets/images/main_background.bmp"}; + AssetManager::LoadingTask blocksTask{AssetManager::LoadingTask::TEXTURE, "blocks", "assets/images/blocks90px_001.bmp"}; + AssetManager::LoadingTask fontTask{AssetManager::LoadingTask::FONT, "main_font", "FreeSans.ttf", 24}; + AssetManager::LoadingTask pixelFontTask{AssetManager::LoadingTask::FONT, "pixel_font", "assets/fonts/PressStart2P-Regular.ttf", 16}; + + // Add tasks to AssetManager + m_assetManager->addLoadingTask(logoTask); + m_assetManager->addLoadingTask(backgroundTask); + m_assetManager->addLoadingTask(blocksTask); + m_assetManager->addLoadingTask(fontTask); + m_assetManager->addLoadingTask(pixelFontTask); + + // Execute loading tasks with progress callback + m_assetManager->executeLoadingTasks([](float progress) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Asset loading progress: %.1f%%", progress * 100.0f); + }); + + // Load sound effects with fallback + m_assetManager->loadSoundEffectWithFallback("clear_line", "clear_line"); + m_assetManager->loadSoundEffectWithFallback("nice_combo", "nice_combo"); + m_assetManager->loadSoundEffectWithFallback("amazing", "amazing"); + + // Start background music loading + m_assetManager->startBackgroundMusicLoading(); + + // Initialize a basic test render handler that shows loaded assets m_stateManager->registerRenderHandler(AppState::Loading, [this](RenderManager& renderer) { - // Simple test render - just clear screen and show a colored rectangle + // Simple test render - just clear screen and show loaded assets renderer.clear(20, 30, 40, 255); - SDL_FRect testRect = { 400, 300, 400, 200 }; - renderer.renderRect(testRect, 255, 100, 100, 255); + // Try to render background if loaded + SDL_Texture* background = m_assetManager->getTexture("background"); + if (background) { + SDL_FRect bgRect = { 0, 0, 1200, 1000 }; + renderer.renderTexture(background, nullptr, &bgRect); + } + + // Try to render logo if loaded + SDL_Texture* logo = m_assetManager->getTexture("logo"); + if (logo) { + SDL_FRect logoRect = { 300, 200, 600, 200 }; + renderer.renderTexture(logo, nullptr, &logoRect); + } + + // Show asset loading status + SDL_FRect statusRect = { 50, 50, 400, 30 }; + renderer.renderRect(statusRect, 0, 100, 200, 200); }); - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Game systems initialized"); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Game systems initialized with asset loading"); return true; } @@ -205,6 +255,7 @@ void ApplicationManager::render() { void ApplicationManager::cleanupManagers() { // Cleanup managers in reverse order m_stateManager.reset(); + m_assetManager.reset(); m_inputManager.reset(); m_renderManager.reset(); diff --git a/src/core/ApplicationManager.h b/src/core/ApplicationManager.h index b33fda2..b148fd1 100644 --- a/src/core/ApplicationManager.h +++ b/src/core/ApplicationManager.h @@ -41,6 +41,7 @@ public: // Access to managers (for now, will be replaced with dependency injection later) RenderManager* getRenderManager() const { return m_renderManager.get(); } InputManager* getInputManager() const { return m_inputManager.get(); } + AssetManager* getAssetManager() const { return m_assetManager.get(); } StateManager* getStateManager() const { return m_stateManager.get(); } private: @@ -61,6 +62,7 @@ private: // Core managers std::unique_ptr m_renderManager; std::unique_ptr m_inputManager; + std::unique_ptr m_assetManager; std::unique_ptr m_stateManager; // Application state diff --git a/src/core/AssetManager.cpp b/src/core/AssetManager.cpp new file mode 100644 index 0000000..b715ada --- /dev/null +++ b/src/core/AssetManager.cpp @@ -0,0 +1,384 @@ +#include "AssetManager.h" +#include "../graphics/Font.h" +#include "../audio/Audio.h" +#include "../audio/SoundEffect.h" +#include +#include +#include + +AssetManager::AssetManager() + : m_renderer(nullptr) + , m_audioSystem(nullptr) + , m_soundSystem(nullptr) + , m_defaultTexturePath("assets/images/") + , m_defaultFontPath("assets/fonts/") + , m_initialized(false) { +} + +AssetManager::~AssetManager() { + shutdown(); +} + +bool AssetManager::initialize(SDL_Renderer* renderer) { + if (m_initialized) { + logError("AssetManager already initialized"); + return false; + } + + if (!renderer) { + setError("Invalid renderer provided to AssetManager"); + return false; + } + + m_renderer = renderer; + + // Get references to singleton systems + m_audioSystem = &Audio::instance(); + m_soundSystem = &SoundEffectManager::instance(); + + m_initialized = true; + logInfo("AssetManager initialized successfully"); + return true; +} + +void AssetManager::shutdown() { + if (!m_initialized) { + return; + } + + logInfo("Shutting down AssetManager..."); + + // Clear loading tasks + clearLoadingTasks(); + + // Cleanup textures + for (auto& [id, texture] : m_textures) { + if (texture) { + SDL_DestroyTexture(texture); + logInfo("Destroyed texture: " + id); + } + } + m_textures.clear(); + + // Cleanup fonts (unique_ptr handles destruction) + m_fonts.clear(); + + // Reset state + m_renderer = nullptr; + m_audioSystem = nullptr; + m_soundSystem = nullptr; + m_initialized = false; + + logInfo("AssetManager shutdown complete"); +} + +SDL_Texture* AssetManager::loadTexture(const std::string& id, const std::string& filepath) { + if (!validateRenderer()) { + return nullptr; + } + + // Check if already loaded + auto it = m_textures.find(id); + if (it != m_textures.end()) { + logInfo("Texture already loaded: " + id); + return it->second; + } + + // Load new texture + SDL_Texture* texture = loadTextureFromFile(filepath); + if (!texture) { + setError("Failed to load texture: " + filepath); + return nullptr; + } + + m_textures[id] = texture; + logInfo("Loaded texture: " + id + " from " + filepath); + return texture; +} + +SDL_Texture* AssetManager::getTexture(const std::string& id) const { + auto it = m_textures.find(id); + return (it != m_textures.end()) ? it->second : nullptr; +} + +bool AssetManager::unloadTexture(const std::string& id) { + auto it = m_textures.find(id); + if (it == m_textures.end()) { + setError("Texture not found: " + id); + return false; + } + + if (it->second) { + SDL_DestroyTexture(it->second); + } + + m_textures.erase(it); + logInfo("Unloaded texture: " + id); + return true; +} + +bool AssetManager::loadFont(const std::string& id, const std::string& filepath, int baseSize) { + // Check if already loaded + auto it = m_fonts.find(id); + if (it != m_fonts.end()) { + logInfo("Font already loaded: " + id); + return true; + } + + // Create new font + auto font = std::make_unique(); + if (!font->init(filepath, baseSize)) { + setError("Failed to initialize font: " + filepath); + return false; + } + + m_fonts[id] = std::move(font); + logInfo("Loaded font: " + id + " from " + filepath + " (size: " + std::to_string(baseSize) + ")"); + return true; +} + +FontAtlas* AssetManager::getFont(const std::string& id) const { + auto it = m_fonts.find(id); + return (it != m_fonts.end()) ? it->second.get() : nullptr; +} + +bool AssetManager::unloadFont(const std::string& id) { + auto it = m_fonts.find(id); + if (it == m_fonts.end()) { + setError("Font not found: " + id); + return false; + } + + // Shutdown the font before removing + it->second->shutdown(); + m_fonts.erase(it); + logInfo("Unloaded font: " + id); + return true; +} + +bool AssetManager::loadMusicTrack(const std::string& filepath) { + if (!m_audioSystem) { + setError("Audio system not available"); + return false; + } + + if (!fileExists(filepath)) { + setError("Music file not found: " + filepath); + return false; + } + + try { + m_audioSystem->addTrackAsync(filepath); + logInfo("Added music track for loading: " + filepath); + return true; + } catch (const std::exception& e) { + setError("Failed to add music track: " + std::string(e.what())); + return false; + } +} + +bool AssetManager::loadSoundEffect(const std::string& id, const std::string& filepath) { + if (!m_soundSystem) { + setError("Sound effect system not available"); + return false; + } + + if (!fileExists(filepath)) { + setError("Sound effect file not found: " + filepath); + return false; + } + + if (m_soundSystem->loadSound(id, filepath)) { + logInfo("Loaded sound effect: " + id + " from " + filepath); + return true; + } else { + setError("Failed to load sound effect: " + id + " from " + filepath); + return false; + } +} + +bool AssetManager::loadSoundEffectWithFallback(const std::string& id, const std::string& baseName) { + if (!m_soundSystem) { + setError("Sound effect system not available"); + return false; + } + + // Try WAV first, then MP3 fallback (matching main.cpp pattern) + std::string wavPath = "assets/music/" + baseName + ".wav"; + std::string mp3Path = "assets/music/" + baseName + ".mp3"; + + // Check WAV first + if (fileExists(wavPath)) { + if (m_soundSystem->loadSound(id, wavPath)) { + logInfo("Loaded sound effect: " + id + " from " + wavPath + " (WAV)"); + return true; + } + } + + // Fallback to MP3 + if (fileExists(mp3Path)) { + if (m_soundSystem->loadSound(id, mp3Path)) { + logInfo("Loaded sound effect: " + id + " from " + mp3Path + " (MP3 fallback)"); + return true; + } + } + + setError("Failed to load sound effect: " + id + " (tried both WAV and MP3)"); + return false; +} + +void AssetManager::startBackgroundMusicLoading() { + if (!m_audioSystem) { + setError("Audio system not available"); + return; + } + + m_audioSystem->startBackgroundLoading(); + logInfo("Started background music loading"); +} + +bool AssetManager::isMusicLoadingComplete() const { + if (!m_audioSystem) { + return false; + } + + return m_audioSystem->isLoadingComplete(); +} + +int AssetManager::getLoadedMusicTrackCount() const { + if (!m_audioSystem) { + return 0; + } + + return m_audioSystem->getLoadedTrackCount(); +} + +void AssetManager::addLoadingTask(const LoadingTask& task) { + m_loadingTasks.push_back(task); + logInfo("Added loading task: " + task.id + " (" + task.filepath + ")"); +} + +void AssetManager::executeLoadingTasks(std::function progressCallback) { + if (m_loadingTasks.empty()) { + if (progressCallback) progressCallback(1.0f); + return; + } + + logInfo("Executing " + std::to_string(m_loadingTasks.size()) + " loading tasks..."); + + size_t totalTasks = m_loadingTasks.size(); + size_t completedTasks = 0; + + for (const auto& task : m_loadingTasks) { + bool success = false; + + switch (task.type) { + case LoadingTask::TEXTURE: + success = (loadTexture(task.id, task.filepath) != nullptr); + break; + + case LoadingTask::FONT: + success = loadFont(task.id, task.filepath, task.fontSize); + break; + + case LoadingTask::MUSIC: + success = loadMusicTrack(task.filepath); + break; + + case LoadingTask::SOUND_EFFECT: + success = loadSoundEffect(task.id, task.filepath); + break; + } + + if (!success) { + logError("Failed to load asset: " + task.id + " (" + task.filepath + ")"); + } + + completedTasks++; + + if (progressCallback) { + float progress = static_cast(completedTasks) / static_cast(totalTasks); + progressCallback(progress); + } + } + + logInfo("Completed " + std::to_string(completedTasks) + "/" + std::to_string(totalTasks) + " loading tasks"); +} + +void AssetManager::clearLoadingTasks() { + m_loadingTasks.clear(); + logInfo("Cleared loading tasks"); +} + +bool AssetManager::isResourceLoaded(const std::string& id) const { + return (m_textures.find(id) != m_textures.end()) || + (m_fonts.find(id) != m_fonts.end()); +} + +std::string AssetManager::getAssetPath(const std::string& relativePath) { + // Simple path construction - could be enhanced with proper path handling + if (relativePath.find("assets/") == 0) { + return relativePath; // Already has assets/ prefix + } + return "assets/" + relativePath; +} + +bool AssetManager::fileExists(const std::string& filepath) { + // Use SDL file I/O for consistency with main.cpp pattern + SDL_IOStream* file = SDL_IOFromFile(filepath.c_str(), "rb"); + if (file) { + SDL_CloseIO(file); + return true; + } + return false; +} + +SDL_Texture* AssetManager::loadTextureFromFile(const std::string& filepath) { + if (!validateRenderer()) { + return nullptr; + } + + // Load using SDL_LoadBMP (matching main.cpp pattern) + SDL_Surface* surface = SDL_LoadBMP(filepath.c_str()); + if (!surface) { + setError("Failed to load surface from: " + filepath + " - " + SDL_GetError()); + return nullptr; + } + + SDL_Texture* texture = SDL_CreateTextureFromSurface(m_renderer, surface); + SDL_DestroySurface(surface); + + if (!texture) { + setError("Failed to create texture from surface: " + filepath + " - " + SDL_GetError()); + return nullptr; + } + + return texture; +} + +bool AssetManager::validateRenderer() const { + if (!m_initialized) { + const_cast(this)->setError("AssetManager not initialized"); + return false; + } + + if (!m_renderer) { + const_cast(this)->setError("Invalid renderer"); + return false; + } + + return true; +} + +void AssetManager::setError(const std::string& error) { + m_lastError = error; + logError(error); +} + +void AssetManager::logInfo(const std::string& message) const { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str()); +} + +void AssetManager::logError(const std::string& message) const { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str()); +} diff --git a/src/core/AssetManager.h b/src/core/AssetManager.h new file mode 100644 index 0000000..db329d3 --- /dev/null +++ b/src/core/AssetManager.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Forward declarations +class FontAtlas; +class Audio; +class SoundEffectManager; + +/** + * AssetManager - Centralized resource management following SOLID principles + * + * Responsibilities: + * - Texture loading and management (BMP, PNG via SDL) + * - Font loading and caching (TTF via FontAtlas) + * - Audio resource coordination (MP3 via Audio, WAV via SoundEffectManager) + * - Resource lifecycle management (loading, caching, cleanup) + * - Error handling and fallback mechanisms + * + * Design Principles: + * - Single Responsibility: Only handles asset loading/management + * - Open/Closed: Easy to extend with new asset types + * - Dependency Inversion: Uses interfaces for audio systems + * - Interface Segregation: Separate methods for different asset types + */ +class AssetManager { +public: + AssetManager(); + ~AssetManager(); + + // Lifecycle management + bool initialize(SDL_Renderer* renderer); + void shutdown(); + + // Texture management + SDL_Texture* loadTexture(const std::string& id, const std::string& filepath); + SDL_Texture* getTexture(const std::string& id) const; + bool unloadTexture(const std::string& id); + void setDefaultTexturePath(const std::string& path) { m_defaultTexturePath = path; } + + // Font management + bool loadFont(const std::string& id, const std::string& filepath, int baseSize = 24); + FontAtlas* getFont(const std::string& id) const; + bool unloadFont(const std::string& id); + void setDefaultFontPath(const std::string& path) { m_defaultFontPath = path; } + + // Audio management (coordinates with existing Audio and SoundEffectManager) + bool loadMusicTrack(const std::string& filepath); + bool loadSoundEffect(const std::string& id, const std::string& filepath); + bool loadSoundEffectWithFallback(const std::string& id, const std::string& baseName); + void startBackgroundMusicLoading(); + bool isMusicLoadingComplete() const; + int getLoadedMusicTrackCount() const; + + // Batch loading operations + struct LoadingTask { + enum Type { TEXTURE, FONT, MUSIC, SOUND_EFFECT }; + Type type; + std::string id; + std::string filepath; + int fontSize = 24; // For fonts only + }; + + void addLoadingTask(const LoadingTask& task); + void executeLoadingTasks(std::function progressCallback = nullptr); + void clearLoadingTasks(); + + // Resource queries + size_t getTextureCount() const { return m_textures.size(); } + size_t getFontCount() const { return m_fonts.size(); } + bool isResourceLoaded(const std::string& id) const; + + // Error handling + std::string getLastError() const { return m_lastError; } + void clearLastError() { m_lastError.clear(); } + + // Asset path utilities + static std::string getAssetPath(const std::string& relativePath); + static bool fileExists(const std::string& filepath); + +private: + // Resource storage + std::unordered_map m_textures; + std::unordered_map> m_fonts; + std::vector m_loadingTasks; + + // System references + SDL_Renderer* m_renderer; + Audio* m_audioSystem; // Pointer to singleton + SoundEffectManager* m_soundSystem; // Pointer to singleton + + // Configuration + std::string m_defaultTexturePath; + std::string m_defaultFontPath; + std::string m_lastError; + bool m_initialized; + + // Helper methods + SDL_Texture* loadTextureFromFile(const std::string& filepath); + bool validateRenderer() const; + void setError(const std::string& error); + void logInfo(const std::string& message) const; + void logError(const std::string& message) const; +};