From 1fed8f63bd10aa95ee9c8f9b3c5fbefb5a131591 Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 21 Nov 2025 13:34:20 +0100 Subject: [PATCH] eliminat el checkFiles inicial que a jaildoc li molestava --- .claude/settings.local.json | 5 +- CLAUDE.md | 93 ++++++++++---- run_headless.sh | 51 ++++++++ source/core/resources/resource_cache.cpp | 149 +++++++++++++++-------- source/core/resources/resource_cache.hpp | 3 + source/core/resources/resource_list.cpp | 55 --------- source/core/resources/resource_list.hpp | 2 - source/core/system/director.cpp | 15 +-- source/core/system/director.hpp | 2 +- 9 files changed, 233 insertions(+), 142 deletions(-) create mode 100755 run_headless.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 39d4cd4..37909cf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,10 @@ "Bash(find:*)", "Bash(for file in source/game/scenes/title.cpp source/game/scenes/game.cpp source/game/entities/player.cpp source/game/options.cpp source/core/rendering/screen.cpp)", "Bash(do echo \"=== $file ===\")", - "Bash(done)" + "Bash(done)", + "Bash(Xvfb:*)", + "Bash(chmod:*)", + "Bash(timeout:*)" ], "deny": [], "ask": [] diff --git a/CLAUDE.md b/CLAUDE.md index 948d4cc..cbc3a3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,46 @@ cmake --build build --clean-first **Important:** The build directory is `/Users/sergio/Gitea/jaildoctors_dilemma/build` and the output executable is placed in the project root directory. +### Testing in Headless Environment (SSH/Remote Server) + +**IMPORTANT:** When working on a remote server via SSH without a physical display, the game MUST be run using Xvfb (X Virtual Framebuffer) to avoid SDL3 display initialization errors. + +#### Required Setup (One-time) +```bash +# Install Xvfb +sudo apt-get install xvfb +``` + +#### Running the Game in Headless Mode + +**Option 1: Using the wrapper script (RECOMMENDED)** +```bash +./run_headless.sh +``` + +**Option 2: Using xvfb-run directly** +```bash +xvfb-run -a ./jaildoctors_dilemma +``` + +**Option 3: Custom display configuration** +```bash +xvfb-run -a -s "-screen 0 1280x720x24" ./jaildoctors_dilemma +``` + +#### Why This Is Critical + +- **SDL3 requires a display:** The game uses SDL3 which requires X11/Wayland display +- **No code changes needed:** Xvfb simulates a virtual display without modifying the codebase +- **Full logging:** Console output and logs work normally, essential for debugging resource loading +- **Testing resource loading:** When modifying asset loading code, running with xvfb-run allows seeing all initialization logs + +**ALWAYS use xvfb-run when testing on the remote server, especially when:** +- Modifying resource loading code +- Testing asset initialization +- Debugging startup issues +- Verifying configuration changes + ### Static Analysis Tools (Linters) This project uses two complementary static analysis tools for code quality: @@ -306,8 +346,8 @@ class Screen { - `Screen` - Rendering, window management, palette/shader effects - `Input` - Keyboard & gamepad input binding and checking - `Audio` - Music and sound effect playback -- `Resource` - Asset loading, caching, streaming -- `Asset` - Asset path registry and verification +- `Resource::Cache` - Asset loading, caching, streaming (with error handling) +- `Resource::List` - Asset path registry from assets.yaml (O(1) lookups) - `Director` - Main application controller - `Cheevos` - Achievement state management - `Debug` - Debug information overlay @@ -413,12 +453,14 @@ struct AnimationData { main() ↓ Director::Director() [Initialization] - ├─ Options::init() - Load game configuration - ├─ Asset::init() - Register asset paths - ├─ Screen::init() - Create window, SDL renderer + ├─ Resource::List::init() - Initialize asset registry singleton + ├─ Director::setFileList() - Load assets.yaml configuration (no verification) + ├─ Options::loadFromFile() - Load game configuration ├─ Audio::init() - Initialize SDL audio + ├─ Screen::init() - Create window, SDL renderer ├─ Input::init() - Bind keyboard/gamepad controls - ├─ Resource::init() - Load all game resources + ├─ Resource::Cache::init() - Load ALL game resources (with verification) + │ └─ Throws exception if any required asset is missing └─ Cheevos::init() - Load achievement state Director::run() [Main loop] @@ -433,11 +475,11 @@ Director::run() [Main loop] Director::~Director() [Cleanup] ├─ Options::saveToFile() - Save game settings - ├─ Resource::destroy() + ├─ Resource::Cache::destroy() ├─ Audio::destroy() ├─ Input::destroy() ├─ Screen::destroy() - └─ Asset::destroy() + └─ Resource::List::destroy() ``` ### 4.2 Game Scene Flow (Core Gameplay Loop) @@ -468,24 +510,27 @@ Game::run() { ``` Director::setFileList() - ├─ Asset::loadFromFile(config_path, PREFIX, system_folder_) - │ ├─ Read config/assets.yaml - Parse text configuration file - │ ├─ Parse each line: TYPE|PATH|OPTIONS - Extract asset information - │ ├─ Replace variables (${PREFIX}, ${SYSTEM_FOLDER}) - │ └─ Store in unordered_map (O(1) lookup) - Fast asset path retrieval - └─ Asset::check() - Verify required files exist + └─ Asset::loadFromFile(config_path, PREFIX, system_folder_) + ├─ Read config/assets.yaml - Parse text configuration file + ├─ Parse YAML structure: assets grouped by category + ├─ Replace variables (${PREFIX}, ${SYSTEM_FOLDER}) + └─ Store in unordered_map (O(1) lookup) - Fast asset path retrieval Game Scene initialization - └─ Resource::init() - Loads all resources - ├─ loadSounds() - WAV files - ├─ loadMusics() - OGG files - ├─ loadSurfaces() - GIF/PNG images - ├─ loadAnimations() - .ani animation definitions - ├─ loadTileMaps() - .room tilemap data - └─ loadRooms() - Room metadata + └─ Resource::Cache::init() - Loads all resources + ├─ loadSounds() - WAV files (with error handling) + ├─ loadMusics() - OGG files (with error handling) + ├─ loadSurfaces() - GIF/PNG images (with error handling) + ├─ loadPalettes() - PAL palette files (with error handling) + ├─ loadTextFiles() - Font definition files (with error handling) + ├─ loadAnimations() - YAML animation definitions (with error handling) + └─ loadRooms() - Room YAML files (with error handling) + + Note: Asset verification happens during actual loading. + If a required file is missing, Cache::load() throws detailed exception. During gameplay - └─ Resource::get*(name) - Return cached resource + └─ Resource::Cache::get*(name) - Return cached resource ``` ### 4.4 Input Flow @@ -653,8 +698,8 @@ Achievements trigger notifications on unlock. | `Screen` | Rendering, window, palette management | Singleton | | `Input` | Keyboard & gamepad input | Singleton | | `Audio` | Music and SFX playback | Singleton | -| `Resource` | Asset caching and loading | Singleton | -| `Asset` | Asset path registry from config/assets.yaml, O(1) lookups, variable substitution | Singleton | +| `Resource::Cache` | Asset caching and loading (with detailed error messages) | Singleton | +| `Resource::List` | Asset path registry from config/assets.yaml, O(1) lookups, variable substitution | Singleton | | `Debug` | Debug overlay information | Singleton | | `globalEvents` | Global SDL event handling (quit, device reset, mouse) | Namespace | diff --git a/run_headless.sh b/run_headless.sh new file mode 100755 index 0000000..75419cf --- /dev/null +++ b/run_headless.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Script para ejecutar JailDoctor's Dilemma en modo headless (sin pantalla) +# Usa Xvfb (X Virtual Framebuffer) para simular un display + +# Configuración +GAME_EXECUTABLE="./jaildoctors_dilemma" +DISPLAY_SIZE="1280x720x24" # Resolución y profundidad de color + +# Colores para output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== JailDoctor's Dilemma - Modo Headless ===${NC}" +echo "" + +# Verificar que Xvfb está instalado +if ! command -v xvfb-run &> /dev/null; then + echo -e "${YELLOW}ERROR: Xvfb no está instalado${NC}" + echo "Instálalo con: sudo apt-get install xvfb" + exit 1 +fi + +# Verificar que el ejecutable existe +if [ ! -f "$GAME_EXECUTABLE" ]; then + echo -e "${YELLOW}ERROR: No se encuentra el ejecutable $GAME_EXECUTABLE${NC}" + echo "Compila el juego primero con: cmake --build build" + exit 1 +fi + +echo "Iniciando juego con Xvfb..." +echo "Display virtual: $DISPLAY_SIZE" +echo "" +echo -e "${YELLOW}Nota: El juego está corriendo. Presiona Ctrl+C para detenerlo.${NC}" +echo "" + +# Ejecutar el juego con Xvfb +# Opciones: +# -a: Selecciona automáticamente un número de display disponible +# -s: Configura el servidor X (resolución y profundidad) +xvfb-run -a -s "-screen 0 ${DISPLAY_SIZE}" "$GAME_EXECUTABLE" "$@" + +EXIT_CODE=$? +echo "" +if [ $EXIT_CODE -eq 0 ]; then + echo -e "${GREEN}Juego finalizado correctamente${NC}" +else + echo -e "${YELLOW}Juego finalizado con código de salida: $EXIT_CODE${NC}" +fi + +exit $EXIT_CODE diff --git a/source/core/resources/resource_cache.cpp b/source/core/resources/resource_cache.cpp index 85074eb..6ed0faa 100644 --- a/source/core/resources/resource_cache.cpp +++ b/source/core/resources/resource_cache.cpp @@ -176,6 +176,15 @@ auto Cache::getRooms() -> std::vector& { return rooms_; } +// Helper para lanzar errores de carga con formato consistente +[[noreturn]] void Cache::throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) { + std::cerr << "\n[ ERROR ] Failed to load " << asset_type << ": " << getFileName(file_path) << '\n'; + std::cerr << "[ ERROR ] Path: " << file_path << '\n'; + std::cerr << "[ ERROR ] Reason: " << e.what() << '\n'; + std::cerr << "[ ERROR ] Check config/assets.yaml configuration\n"; + throw; +} + // Carga los sonidos void Cache::loadSounds() { std::cout << "\n>> SOUND FILES" << '\n'; @@ -183,23 +192,31 @@ void Cache::loadSounds() { sounds_.clear(); for (const auto& l : list) { - auto name = getFileName(l); - JA_Sound_t* sound = nullptr; + try { + auto name = getFileName(l); + JA_Sound_t* sound = nullptr; - // Try loading from resource pack first - auto audio_data = Helper::loadFile(l); - if (!audio_data.empty()) { - sound = JA_LoadSound(audio_data.data(), static_cast(audio_data.size())); + // Try loading from resource pack first + auto audio_data = Helper::loadFile(l); + if (!audio_data.empty()) { + sound = JA_LoadSound(audio_data.data(), static_cast(audio_data.size())); + } + + // Fallback to file path if memory loading failed + if (sound == nullptr) { + sound = JA_LoadSound(l.c_str()); + } + + if (sound == nullptr) { + throw std::runtime_error("Failed to decode audio file"); + } + + sounds_.emplace_back(SoundResource{.name = name, .sound = sound}); + printWithDots("Sound : ", name, "[ LOADED ]"); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("SOUND", l, e); } - - // Fallback to file path if memory loading failed - if (sound == nullptr) { - sound = JA_LoadSound(l.c_str()); - } - - sounds_.emplace_back(SoundResource{.name = name, .sound = sound}); - printWithDots("Sound : ", name, "[ LOADED ]"); - updateLoadingProgress(); } } @@ -210,23 +227,31 @@ void Cache::loadMusics() { musics_.clear(); for (const auto& l : list) { - auto name = getFileName(l); - JA_Music_t* music = nullptr; + try { + auto name = getFileName(l); + JA_Music_t* music = nullptr; - // Try loading from resource pack first - auto audio_data = Helper::loadFile(l); - if (!audio_data.empty()) { - music = JA_LoadMusic(audio_data.data(), static_cast(audio_data.size())); + // Try loading from resource pack first + auto audio_data = Helper::loadFile(l); + if (!audio_data.empty()) { + music = JA_LoadMusic(audio_data.data(), static_cast(audio_data.size())); + } + + // Fallback to file path if memory loading failed + if (music == nullptr) { + music = JA_LoadMusic(l.c_str()); + } + + if (music == nullptr) { + throw std::runtime_error("Failed to decode music file"); + } + + musics_.emplace_back(MusicResource{.name = name, .music = music}); + printWithDots("Music : ", name, "[ LOADED ]"); + updateLoadingProgress(1); + } catch (const std::exception& e) { + throwLoadError("MUSIC", l, e); } - - // Fallback to file path if memory loading failed - if (music == nullptr) { - music = JA_LoadMusic(l.c_str()); - } - - musics_.emplace_back(MusicResource{.name = name, .music = music}); - printWithDots("Music : ", name, "[ LOADED ]"); - updateLoadingProgress(1); } } @@ -237,10 +262,14 @@ void Cache::loadSurfaces() { surfaces_.clear(); for (const auto& l : list) { - auto name = getFileName(l); - surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared(l)}); - surfaces_.back().surface->setTransparentColor(0); - updateLoadingProgress(); + try { + auto name = getFileName(l); + surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared(l)}); + surfaces_.back().surface->setTransparentColor(0); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("BITMAP", l, e); + } } // Reconfigura el color transparente de algunas surfaces @@ -260,9 +289,13 @@ void Cache::loadPalettes() { palettes_.clear(); for (const auto& l : list) { - auto name = getFileName(l); - palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)}); - updateLoadingProgress(); + try { + auto name = getFileName(l); + palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)}); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("PALETTE", l, e); + } } } @@ -273,9 +306,13 @@ void Cache::loadTextFiles() { text_files_.clear(); for (const auto& l : list) { - auto name = getFileName(l); - text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)}); - updateLoadingProgress(); + try { + auto name = getFileName(l); + text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)}); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("FONT", l, e); + } } } @@ -286,14 +323,22 @@ void Cache::loadAnimations() { animations_.clear(); for (const auto& l : list) { - auto name = getFileName(l); + try { + auto name = getFileName(l); - // Cargar bytes del archivo YAML sin parsear (carga lazy) - auto yaml_bytes = Helper::loadFile(l); + // Cargar bytes del archivo YAML sin parsear (carga lazy) + auto yaml_bytes = Helper::loadFile(l); - animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes}); - printWithDots("Animation : ", name, "[ LOADED ]"); - updateLoadingProgress(); + if (yaml_bytes.empty()) { + throw std::runtime_error("File is empty or could not be loaded"); + } + + animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes}); + printWithDots("Animation : ", name, "[ LOADED ]"); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("ANIMATION", l, e); + } } } @@ -304,10 +349,14 @@ void Cache::loadRooms() { rooms_.clear(); for (const auto& l : list) { - auto name = getFileName(l); - rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared(Room::loadYAML(l))}); - printWithDots("Room : ", name, "[ LOADED ]"); - updateLoadingProgress(); + try { + auto name = getFileName(l); + rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared(Room::loadYAML(l))}); + printWithDots("Room : ", name, "[ LOADED ]"); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("ROOM", l, e); + } } } diff --git a/source/core/resources/resource_cache.hpp b/source/core/resources/resource_cache.hpp index 4872bf5..1176868 100644 --- a/source/core/resources/resource_cache.hpp +++ b/source/core/resources/resource_cache.hpp @@ -66,6 +66,9 @@ class Cache { static void checkEvents(); void updateLoadingProgress(int steps = 5); + // Helper para mensajes de error de carga + [[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e); + // Constructor y destructor Cache(); ~Cache() = default; diff --git a/source/core/resources/resource_list.cpp b/source/core/resources/resource_list.cpp index cfa455b..645b41c 100644 --- a/source/core/resources/resource_list.cpp +++ b/source/core/resources/resource_list.cpp @@ -200,61 +200,6 @@ auto List::exists(const std::string& filename) const -> bool { return file_list_.find(filename) != file_list_.end(); } -// Comprueba que existen todos los elementos -auto List::check() const -> bool { - bool success = true; - - std::cout << "\n** CHECKING FILES" << '\n'; - - // Agrupar por tipo para mostrar organizado - std::unordered_map> by_type; - - for (const auto& [filename, item] : file_list_) { - if (item.required) { - by_type[item.type].push_back(&item); - } - } - - // Verificar por tipo - for (int type = 0; type < static_cast(Type::SIZE); ++type) { - Type asset_type = static_cast(type); - - if (by_type.find(asset_type) != by_type.end()) { - std::cout << "\n>> " << getTypeName(asset_type) << " FILES" << '\n'; - - bool type_success = true; - for (const auto* item : by_type[asset_type]) { - if (!checkFile(item->file)) { - success = false; - type_success = false; - } - } - - if (type_success) { - std::cout << " All files are OK." << '\n'; - } - } - } - - // Resultado - std::cout << (success ? "\n** CHECKING FILES COMPLETED.\n" : "\n** CHECKING FILES FAILED.\n") << '\n'; - - return success; -} - -// Comprueba que existe un fichero -auto List::checkFile(const std::string& path) -> bool { - std::ifstream file(path); - bool success = file.good(); - file.close(); - - if (!success) { - printWithDots("Checking file : ", getFileName(path), "[ ERROR ]"); - } - - return success; -} - // Parsea string a Type auto List::parseAssetType(const std::string& type_str) -> Type { if (type_str == "DATA") { diff --git a/source/core/resources/resource_list.hpp b/source/core/resources/resource_list.hpp index 0845e1c..8988fa0 100644 --- a/source/core/resources/resource_list.hpp +++ b/source/core/resources/resource_list.hpp @@ -37,7 +37,6 @@ class List { void loadFromString(const std::string& config_content, const std::string& prefix = "", const std::string& system_folder = ""); // Para cargar desde pack (release) [[nodiscard]] auto get(const std::string& filename) const -> std::string; // Obtiene la ruta completa [[nodiscard]] auto loadData(const std::string& filename) const -> std::vector; // Carga datos del archivo - [[nodiscard]] auto check() const -> bool; [[nodiscard]] auto getListByType(Type type) const -> std::vector; [[nodiscard]] auto exists(const std::string& filename) const -> bool; // Verifica si un asset existe @@ -59,7 +58,6 @@ class List { std::string executable_path_; // Ruta del ejecutable // --- Métodos internos --- - [[nodiscard]] static auto checkFile(const std::string& path) -> bool; // Verifica si un archivo existe [[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo [[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; // Convierte string a tipo void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index ee35a74..7927f29 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -107,10 +107,9 @@ Director::Director(std::vector const& args) { // 1. Initialize Asset system from filesystem Resource::List::init(executable_path_); - // 2. Load and verify assets from disk - if (!setFileList()) { - exit(EXIT_FAILURE); - } + // 2. Load asset configuration from disk + // Note: Asset verification happens during Resource::Cache::load() + setFileList(); // 3. Initialize resource pack system (optional, with fallback) std::cout << "Initializing resource pack (development mode): " << pack_path << '\n'; @@ -259,8 +258,8 @@ void Director::createSystemFolder(const std::string& folder) { } } -// Crea el indice de ficheros -auto Director::setFileList() -> bool { +// Carga la configuración de assets desde assets.yaml +void Director::setFileList() { // Determinar el prefijo de ruta según la plataforma #ifdef MACOS_BUNDLE const std::string PREFIX = "/../Resources"; @@ -272,10 +271,8 @@ auto Director::setFileList() -> bool { std::string config_path = executable_path_ + PREFIX + "/config/assets.yaml"; // Cargar todos los assets desde el archivo de configuración + // La verificación de existencia de archivos se realiza durante Resource::Cache::load() Resource::List::get()->loadFromFile(config_path, PREFIX, system_folder_); - - // Verificar que todos los assets requeridos existen - return Resource::List::get()->check(); } // Ejecuta la seccion de juego con el logo diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index 4944e9b..d1b681b 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -19,7 +19,7 @@ class Director { // --- Funciones --- void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos - auto setFileList() -> bool; // Crea el indice de ficheros + void setFileList(); // Carga la configuración de assets desde assets.yaml static void runLogo(); // Ejecuta la seccion de juego con el logo static void runLoadingScreen(); // Ejecuta la seccion de juego de la pantalla de carga static void runTitle(); // Ejecuta la seccion de juego con el titulo y los menus