#include "core/resources/resource_cache.hpp" #include #include // Para find_if #include // Para exit, size_t #include // Para ifstream, istreambuf_iterator #include // Para basic_ostream, operator<<, endl, cout #include // Para runtime_error #include #include "core/audio/jail_audio.hpp" // Para Ja::deleteMusic, Ja::deleteSound, JA_Loa... #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/text.hpp" // Para Text, loadTextFile #include "core/resources/resource_helper.hpp" // Para Helper #include "core/resources/resource_list.hpp" // Para List, List::Type #include "game/defaults.hpp" // Para Defaults namespace #include "game/gameplay/room.hpp" // Para RoomData, loadRoomFile, loadRoomTileFile #include "game/gameplay/room_loader.hpp" // Para RoomLoader::loadFromString #include "game/options.hpp" // Para Options, OptionsGame, options #include "utils/defines.hpp" // Para WINDOW_CAPTION #include "utils/utils.hpp" // Para getFileName, printWithDots, PaletteColor #include "version.h" // Para Version::GIT_HASH namespace Ja { struct Music; struct Sound; } // namespace Ja namespace Resource { // [SINGLETON] Hay que definir las variables estáticas, desde el .h sólo la hemos declarado Cache* Cache::cache = nullptr; // [SINGLETON] Crearemos el objeto cache con esta función estática void Cache::init() { Cache::cache = new Cache(); } // [SINGLETON] Destruiremos el objeto cache con esta función estática void Cache::destroy() { delete Cache::cache; } // [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él auto Cache::get() -> Cache* { return Cache::cache; } // Constructor — no dispara la carga. Director llama a beginLoad() + loadStep() // desde iterate() para que el bucle SDL3 esté vivo durante la carga. Cache::Cache() : loading_text_(Screen::get()->getText()) { } // Vacia todos los vectores de recursos void Cache::clear() { clearSounds(); clearMusics(); surfaces_.clear(); palettes_.clear(); text_files_.clear(); texts_.clear(); animations_.clear(); rooms_.clear(); } // Carga todos los recursos de golpe (usado solo por reload() en hot-reload de debug) void Cache::load() { calculateTotal(); Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); std::cout << "\n** LOADING RESOURCES" << '\n'; loadSounds(); loadMusics(); loadSurfaces(); loadPalettes(); loadTextFiles(); loadAnimations(); loadRooms(); createText(); std::cout << "\n** RESOURCES LOADED" << '\n'; } // Prepara el loader incremental. Director lo llama una vez tras Cache::init(). void Cache::beginLoad() { calculateTotal(); Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); std::cout << "\n** LOADING RESOURCES (incremental)" << '\n'; stage_ = LoadStage::SOUNDS; stage_index_ = 0; } auto Cache::isLoadDone() const -> bool { return stage_ == LoadStage::DONE; } // Helper per a una etapa que itera una llista de recursos. // Imprimeix la capçalera i neteja el vector al primer cop; després carrega // un element per crida fins exhaurir la llista, moment en què passa a `next`. void Cache::stepEachInList(List::Type type, const char* header, const std::function& clear_fn, LoadStage next, const std::function& load_fn) { auto list = List::get()->getListByType(type); if (stage_index_ == 0) { std::cout << "\n>> " << header << '\n'; clear_fn(); } if (stage_index_ >= list.size()) { stage_ = next; stage_index_ = 0; return; } load_fn(stage_index_++); } // Carga assets hasta agotar el presupuesto de tiempo o completar todas las etapas. // Devuelve true cuando ya no queda nada por cargar. auto Cache::loadStep(int budget_ms) -> bool { if (stage_ == LoadStage::DONE) { return true; } const Uint64 START_NS = SDL_GetTicksNS(); const Uint64 BUDGET_NS = static_cast(budget_ms) * 1'000'000ULL; while (stage_ != LoadStage::DONE) { switch (stage_) { case LoadStage::SOUNDS: stepEachInList(List::Type::SOUND, "SOUND FILES", [this] { sounds_.clear(); }, LoadStage::MUSICS, [this](size_t i) { loadOneSound(i); }); break; case LoadStage::MUSICS: stepEachInList(List::Type::MUSIC, "MUSIC FILES", [this] { musics_.clear(); }, LoadStage::SURFACES, [this](size_t i) { loadOneMusic(i); }); break; case LoadStage::SURFACES: stepEachInList(List::Type::BITMAP, "SURFACES", [this] { surfaces_.clear(); }, LoadStage::SURFACES_POST, [this](size_t i) { loadOneSurface(i); }); break; case LoadStage::SURFACES_POST: finalizeSurfaces(); stage_ = LoadStage::PALETTES; stage_index_ = 0; break; case LoadStage::PALETTES: stepEachInList(List::Type::PALETTE, "PALETTES", [this] { palettes_.clear(); }, LoadStage::TEXT_FILES, [this](size_t i) { loadOnePalette(i); }); break; case LoadStage::TEXT_FILES: stepEachInList(List::Type::FONT, "TEXT FILES", [this] { text_files_.clear(); }, LoadStage::ANIMATIONS, [this](size_t i) { loadOneTextFile(i); }); break; case LoadStage::ANIMATIONS: stepEachInList(List::Type::ANIMATION, "ANIMATIONS", [this] { animations_.clear(); }, LoadStage::ROOMS, [this](size_t i) { loadOneAnimation(i); }); break; case LoadStage::ROOMS: stepEachInList(List::Type::ROOM, "ROOMS", [this] { rooms_.clear(); }, LoadStage::TEXTS, [this](size_t i) { loadOneRoom(i); }); break; case LoadStage::TEXTS: stepTexts(); break; case LoadStage::DONE: break; } if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) { break; } } return stage_ == LoadStage::DONE; } void Cache::stepTexts() { // createText itera sobre una lista fija de 5 fuentes constexpr size_t TEXT_COUNT = 5; if (stage_index_ == 0) { std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n'; texts_.clear(); } if (stage_index_ >= TEXT_COUNT) { stage_ = LoadStage::DONE; stage_index_ = 0; std::cout << "\n** RESOURCES LOADED" << '\n'; return; } createOneText(stage_index_++); } // Recarga todos los recursos (síncrono, solo para hot-reload de debug) void Cache::reload() { clear(); load(); } // Obtiene el sonido a partir de un nombre auto Cache::getSound(const std::string& name) -> Ja::Sound* { auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> bool { return s.name == name; }); if (it != sounds_.end()) { return it->sound; } std::cerr << "Error: Sonido no encontrado " << name << '\n'; throw std::runtime_error("Sonido no encontrado: " + name); } // Obtiene la música a partir de un nombre auto Cache::getMusic(const std::string& name) -> Ja::Music* { auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> bool { return m.name == name; }); if (it != musics_.end()) { return it->music; } std::cerr << "Error: Música no encontrada " << name << '\n'; throw std::runtime_error("Música no encontrada: " + name); } // Obtiene la surface a partir de un nombre auto Cache::getSurface(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(surfaces_, [&name](const auto& t) -> bool { return t.name == name; }); if (it != surfaces_.end()) { return it->surface; } std::cerr << "Error: Imagen no encontrada " << name << '\n'; throw std::runtime_error("Imagen no encontrada: " + name); } // Obtiene la paleta a partir de un nombre auto Cache::getPalette(const std::string& name) -> Palette { auto it = std::ranges::find_if(palettes_, [&name](const auto& t) -> bool { return t.name == name; }); if (it != palettes_.end()) { return it->palette; } std::cerr << "Error: Paleta no encontrada " << name << '\n'; throw std::runtime_error("Paleta no encontrada: " + name); } // Obtiene el fichero de texto a partir de un nombre auto Cache::getTextFile(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> bool { return t.name == name; }); if (it != text_files_.end()) { return it->text_file; } std::cerr << "Error: TextFile no encontrado " << name << '\n'; throw std::runtime_error("TextFile no encontrado: " + name); } // Obtiene el objeto de texto a partir de un nombre auto Cache::getText(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(texts_, [&name](const auto& t) -> bool { return t.name == name; }); if (it != texts_.end()) { return it->text; } std::cerr << "Error: Text no encontrado " << name << '\n'; throw std::runtime_error("Texto no encontrado: " + name); } // Obtiene los datos de animación parseados a partir de un nombre auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { auto it = std::ranges::find_if(animations_, [&name](const auto& a) -> bool { return a.name == name; }); if (it != animations_.end()) { return *it; } std::cerr << "Error: Animación no encontrada " << name << '\n'; throw std::runtime_error("Animación no encontrada: " + name); } // Obtiene la habitación a partir de un nombre auto Cache::getRoom(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; }); if (it != rooms_.end()) { return it->room; } std::cerr << "Error: Habitación no encontrada " << name << '\n'; throw std::runtime_error("Habitación no encontrada: " + name); } #ifdef _DEBUG // Recarga una habitación desde disco (para el editor de mapas) // Lee directamente del filesystem (no del resource pack) para obtener los cambios del editor void Cache::reloadRoom(const std::string& name) { auto file_path = List::get()->get(name); if (file_path.empty()) { std::cerr << "reloadRoom: Cannot resolve path for " << name << '\n'; return; } // Leer directamente del filesystem (evita el resource pack que tiene datos antiguos) std::ifstream file(file_path); if (!file.is_open()) { std::cerr << "reloadRoom: Cannot open " << file_path << '\n'; return; } std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); // Parsear y actualizar el cache auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; }); if (it != rooms_.end()) { *(it->room) = RoomLoader::loadFromString(content, name); std::cout << "reloadRoom: " << name << " reloaded from filesystem\n"; } } #endif // Obtiene todas las habitaciones auto Cache::getRooms() -> std::vector& { return rooms_; } // Helper para registrar errores de carga con formato consistente. // El rethrow es responsabilitat del catch que crida la funció. void Cache::logLoadError(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"; } // Lista fija de text objects. Compartida entre createText() y createOneText(i). namespace { struct TextObjectInfo { std::string key; // Identificador del recurso std::string texture_file; // Nombre del archivo de textura std::string text_file; // Nombre del archivo de texto }; auto getTextObjectInfos() -> const std::vector& { static const std::vector INFO = { {.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"}, {.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"}, {.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"}, {.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"}, {.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}}; return INFO; } } // namespace // --- Helpers incrementales (un asset por llamada) --- void Cache::loadOneSound(size_t index) { auto list = List::get()->getListByType(List::Type::SOUND); const auto& l = list[index]; try { auto name = getFileName(l); setCurrentLoading(name); Ja::Sound* sound = nullptr; auto audio_data = Helper::loadFile(l); if (!audio_data.empty()) { sound = Ja::loadSound(audio_data.data(), static_cast(audio_data.size())); } 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) { logLoadError("SOUND", l, e); throw; } } void Cache::loadOneMusic(size_t index) { auto list = List::get()->getListByType(List::Type::MUSIC); const auto& l = list[index]; try { auto name = getFileName(l); setCurrentLoading(name); Ja::Music* music = nullptr; auto audio_data = Helper::loadFile(l); if (!audio_data.empty()) { music = Ja::loadMusic(audio_data.data(), static_cast(audio_data.size())); } 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(); } catch (const std::exception& e) { logLoadError("MUSIC", l, e); throw; } } void Cache::loadOneSurface(size_t index) { auto list = List::get()->getListByType(List::Type::BITMAP); const auto& l = list[index]; try { auto name = getFileName(l); setCurrentLoading(name); surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared(l)}); surfaces_.back().surface->setTransparentColor(0); updateLoadingProgress(); } catch (const std::exception& e) { logLoadError("BITMAP", l, e); throw; } } void Cache::finalizeSurfaces() { // Reconfigura el color transparente de algunas surfaces getSurface("loading_screen_color.gif")->setTransparentColor(); getSurface("ending1.gif")->setTransparentColor(); getSurface("ending2.gif")->setTransparentColor(); getSurface("ending3.gif")->setTransparentColor(); getSurface("ending4.gif")->setTransparentColor(); getSurface("ending5.gif")->setTransparentColor(); getSurface("standard.gif")->setTransparentColor(16); } void Cache::loadOnePalette(size_t index) { auto list = List::get()->getListByType(List::Type::PALETTE); const auto& l = list[index]; try { auto name = getFileName(l); setCurrentLoading(name); palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)}); updateLoadingProgress(); } catch (const std::exception& e) { logLoadError("PALETTE", l, e); throw; } } void Cache::loadOneTextFile(size_t index) { auto list = List::get()->getListByType(List::Type::FONT); const auto& l = list[index]; try { auto name = getFileName(l); setCurrentLoading(name); text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)}); updateLoadingProgress(); } catch (const std::exception& e) { logLoadError("FONT", l, e); throw; } } void Cache::loadOneAnimation(size_t index) { auto list = List::get()->getListByType(List::Type::ANIMATION); const auto& l = list[index]; try { auto name = getFileName(l); setCurrentLoading(name); auto yaml_bytes = Helper::loadFile(l); 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) { logLoadError("ANIMATION", l, e); throw; } } void Cache::loadOneRoom(size_t index) { auto list = List::get()->getListByType(List::Type::ROOM); const auto& l = list[index]; try { auto name = getFileName(l); setCurrentLoading(name); rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared(Room::loadYAML(l))}); printWithDots("Room : ", name, "[ LOADED ]"); updateLoadingProgress(); } catch (const std::exception& e) { logLoadError("ROOM", l, e); throw; } } void Cache::createOneText(size_t index) { const auto& infos = getTextObjectInfos(); const auto& res_info = infos[index]; texts_.emplace_back(TextResource{ .name = res_info.key, .text = std::make_shared(getSurface(res_info.texture_file), getTextFile(res_info.text_file))}); printWithDots("Text : ", res_info.key, "[ DONE ]"); } // --- Bucles completos (solo usados por reload() síncrono) --- void Cache::loadSounds() { std::cout << "\n>> SOUND FILES" << '\n'; auto list = List::get()->getListByType(List::Type::SOUND); sounds_.clear(); for (size_t i = 0; i < list.size(); ++i) { loadOneSound(i); } } void Cache::loadMusics() { std::cout << "\n>> MUSIC FILES" << '\n'; auto list = List::get()->getListByType(List::Type::MUSIC); musics_.clear(); for (size_t i = 0; i < list.size(); ++i) { loadOneMusic(i); } } void Cache::loadSurfaces() { std::cout << "\n>> SURFACES" << '\n'; auto list = List::get()->getListByType(List::Type::BITMAP); surfaces_.clear(); for (size_t i = 0; i < list.size(); ++i) { loadOneSurface(i); } finalizeSurfaces(); } void Cache::loadPalettes() { std::cout << "\n>> PALETTES" << '\n'; auto list = List::get()->getListByType(List::Type::PALETTE); palettes_.clear(); for (size_t i = 0; i < list.size(); ++i) { loadOnePalette(i); } } void Cache::loadTextFiles() { std::cout << "\n>> TEXT FILES" << '\n'; auto list = List::get()->getListByType(List::Type::FONT); text_files_.clear(); for (size_t i = 0; i < list.size(); ++i) { loadOneTextFile(i); } } void Cache::loadAnimations() { std::cout << "\n>> ANIMATIONS" << '\n'; auto list = List::get()->getListByType(List::Type::ANIMATION); animations_.clear(); for (size_t i = 0; i < list.size(); ++i) { loadOneAnimation(i); } } void Cache::loadRooms() { std::cout << "\n>> ROOMS" << '\n'; auto list = List::get()->getListByType(List::Type::ROOM); rooms_.clear(); for (size_t i = 0; i < list.size(); ++i) { loadOneRoom(i); } } void Cache::createText() { std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n'; texts_.clear(); const auto& infos = getTextObjectInfos(); for (size_t i = 0; i < infos.size(); ++i) { createOneText(i); } } // Vacía el vector de sonidos void Cache::clearSounds() { // Itera sobre el vector y libera los recursos asociados a cada Ja::Sound for (auto& sound : sounds_) { if (sound.sound != nullptr) { Ja::deleteSound(sound.sound); sound.sound = nullptr; } } sounds_.clear(); // Limpia el vector después de liberar todos los recursos } // Vacía el vector de musicas void Cache::clearMusics() { // Itera sobre el vector y libera los recursos asociados a cada Ja::Music for (auto& music : musics_) { if (music.music != nullptr) { Ja::deleteMusic(music.music); music.music = nullptr; } } musics_.clear(); // Limpia el vector después de liberar todos los recursos } // Calcula el numero de recursos para cargar void Cache::calculateTotal() { std::vector asset_types = { List::Type::SOUND, List::Type::MUSIC, List::Type::BITMAP, List::Type::PALETTE, List::Type::FONT, List::Type::ANIMATION, List::Type::ROOM}; int total = 0; for (const auto& asset_type : asset_types) { auto list = List::get()->getListByType(asset_type); total += list.size(); } count_ = ResourceCount{.total = total, .loaded = 0}; } // Muestra el progreso de carga void Cache::renderProgress() { Screen::get()->start(); Screen::get()->clearSurface(static_cast(PaletteColor::BLACK)); // Si show=false: pantalla negra y salir if (!Options::loading.show) { Screen::get()->render(); return; } constexpr float X_PADDING = 60.0F; constexpr float Y_PADDING = 10.0F; constexpr float BAR_HEIGHT = 5.0F; const float BAR_POSITION = Options::game.height - BAR_HEIGHT - Y_PADDING; auto surface = Screen::get()->getRendererSurface(); const auto LOADING_TEXT_COLOR = static_cast(PaletteColor::BRIGHT_WHITE); const auto BAR_COLOR = static_cast(PaletteColor::WHITE); const int TEXT_HEIGHT = loading_text_->getCharacterSize(); const int CENTER_X = Options::game.width / 2; const int CENTER_Y = Options::game.height / 2; // Draw APP_NAME centered above center const std::string APP_NAME = spaceBetweenLetters(Version::APP_NAME); loading_text_->writeColored( CENTER_X - (loading_text_->length(APP_NAME) / 2), CENTER_Y - TEXT_HEIGHT, APP_NAME, LOADING_TEXT_COLOR); // Draw VERSION centered below center const std::string VERSION_TEXT = "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")"; loading_text_->writeColored( CENTER_X - (loading_text_->length(VERSION_TEXT) / 2), CENTER_Y + TEXT_HEIGHT, VERSION_TEXT, LOADING_TEXT_COLOR); // Draw progress bar border const float WIRED_BAR_WIDTH = Options::game.width - (X_PADDING * 2); SDL_FRect rect_wired = {.x = X_PADDING, .y = BAR_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT}; surface->drawRectBorder(&rect_wired, BAR_COLOR); // Draw progress bar fill const float FULL_BAR_WIDTH = WIRED_BAR_WIDTH * count_.getPercentage(); SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT}; surface->fillRect(&rect_full, BAR_COLOR); // Mostra el nom del recurs (o missatge d'espera si ja ha acabat i wait_for_input=true) const bool WAITING_FOR_INPUT = isLoadDone() && Options::loading.wait_for_input; const std::string OVER_BAR_TEXT = WAITING_FOR_INPUT ? "PRESS ANY KEY TO CONTINUE" : current_loading_name_; if ((Options::loading.show_resource_name || WAITING_FOR_INPUT) && !OVER_BAR_TEXT.empty()) { const float TEXT_Y = BAR_POSITION - static_cast(TEXT_HEIGHT) - 2.0F; loading_text_->writeColored( CENTER_X - (loading_text_->length(OVER_BAR_TEXT) / 2), static_cast(TEXT_Y), OVER_BAR_TEXT, LOADING_TEXT_COLOR); } Screen::get()->render(); } // Guarda el nombre del recurso que se está a punto de cargar. El repintado // lo hace el BootLoader (una vez por frame) — aquí solo se actualiza el estado. void Cache::setCurrentLoading(const std::string& name) { current_loading_name_ = name; } // Incrementa el contador de recursos cargados void Cache::updateLoadingProgress() { count_.add(1); } } // namespace Resource