#include "core/resources/resource.h" #include #include #include "core/audio/jail_audio.hpp" #include "core/rendering/text.h" #include "core/rendering/texture.h" #include "core/resources/asset.h" #include "core/resources/resource_helper.h" #include "core/resources/skin_manager.hpp" #include "game/options.hpp" #include "game/ui/menu.h" // Nota: Asset::get() e Input::get() se consultan en preloadAll y al construir // los menús; no se guardan punteros en el objeto Resource. Resource *Resource::instance = nullptr; static auto basename(const std::string &path) -> std::string { return path.substr(path.find_last_of("\\/") + 1); } static auto stem(const std::string &path) -> std::string { std::string b = basename(path); size_t dot = b.find_last_of('.'); if (dot == std::string::npos) { return b; } return b.substr(0, dot); } void Resource::init(SDL_Renderer *renderer) { if (instance == nullptr) { instance = new Resource(renderer); instance->preloadAll(); } } void Resource::destroy() { delete instance; instance = nullptr; } auto Resource::get() -> Resource * { return instance; } Resource::Resource(SDL_Renderer *renderer) : renderer_(renderer) {} Resource::~Resource() { for (auto &[name, m] : menus_) { delete m; } menus_.clear(); for (auto &[name, t] : texts_) { delete t; } texts_.clear(); for (auto &[name, t] : textures_) { delete t; } textures_.clear(); for (auto &[name, s] : sounds_) { Ja::deleteSound(s); } sounds_.clear(); for (auto &[name, m] : musics_) { Ja::deleteMusic(m); } musics_.clear(); } void Resource::preloadAll() { preloadResources(); preloadFonts(); preloadMenus(); } // Pass 1: texturas, sonidos, músicas y datos (animaciones / demo / menús) void Resource::preloadResources() { const auto &items = Asset::get()->getAll(); for (const auto &it : items) { if (!ResourceHelper::shouldUseResourcePack(it.file) && it.type != Asset::Type::LANG) { // Ficheros absolutos (config.txt, score.bin, systemFolder) — no se precargan continue; } auto bytes = ResourceHelper::loadFile(it.file); if (bytes.empty()) { continue; } const std::string BASE_NAME = basename(it.file); switch (it.type) { case Asset::Type::BITMAP: { auto *tex = new Texture(renderer_, bytes); textures_[BASE_NAME] = tex; break; } case Asset::Type::SOUND: { Ja::Sound *s = Ja::loadSound(bytes.data(), (uint32_t)bytes.size()); if (s != nullptr) { sounds_[BASE_NAME] = s; } break; } case Asset::Type::MUSIC: { Ja::Music *m = Ja::loadMusic(bytes.data(), (Uint32)bytes.size()); if (m != nullptr) { musics_[BASE_NAME] = m; } break; } case Asset::Type::DATA: loadDataAsset(BASE_NAME, bytes); break; case Asset::Type::FONT: // Fonts: se emparejan en pass 2 case Asset::Type::LANG: // Lenguaje: lo sigue leyendo la clase Lang via ResourceHelper default: break; } } } // Despacha un asset Asset::Type::DATA en función de la extensión / nombre void Resource::loadDataAsset(const std::string &bname, const std::vector &bytes) { if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".ani") { std::string content(reinterpret_cast(bytes.data()), bytes.size()); std::stringstream ss(content); std::vector lines; std::string line; while (std::getline(ss, line)) { // Normalitza CRLF perquè loadFromVector compari línies amb literals // ("[animation]", "[/animation]") sense \r residual. if (!line.empty() && line.back() == '\r') { line.pop_back(); } lines.push_back(line); } animation_lines_[bname] = std::move(lines); } else if (bname == "demo.bin") { demo_bytes_ = bytes; } // Menús (.men): se construyen en pass 2 porque dependen de textos y sonidos } // Pass 2a: construye Text por cada par basename.png + basename.txt void Resource::preloadFonts() { const auto &items = Asset::get()->getAll(); std::unordered_map> font_pngs; std::unordered_map> font_txts; for (const auto &it : items) { if (it.type != Asset::Type::FONT) { continue; } auto bytes = ResourceHelper::loadFile(it.file); if (bytes.empty()) { continue; } const std::string S = stem(it.file); const std::string BASE_NAME = basename(it.file); if (BASE_NAME.size() >= 4 && BASE_NAME.substr(BASE_NAME.size() - 4) == ".png") { font_pngs[S] = std::move(bytes); } else if (BASE_NAME.size() >= 4 && BASE_NAME.substr(BASE_NAME.size() - 4) == ".txt") { font_txts[S] = std::move(bytes); } } for (const auto &[s, png] : font_pngs) { auto it_txt = font_txts.find(s); if (it_txt == font_txts.end()) { continue; } Text *t = new Text(png, it_txt->second, renderer_); texts_[s] = t; } } // Pass 2b: construye los Menu (dependen de Text+sonidos cargados antes) // // NOTA: Menu::loadFromBytes aún llama internamente a asset->get() y Text/ // Ja::loadSound por path. Funciona en modo fallback; en pack estricto requiere // que Menu se adapte a cargar desde ResourceHelper. Migración pendiente. void Resource::preloadMenus() { const auto &items = Asset::get()->getAll(); for (const auto &it : items) { if (it.type != Asset::Type::DATA) { continue; } const std::string BASE_NAME = basename(it.file); if (BASE_NAME.size() < 4 || BASE_NAME.substr(BASE_NAME.size() - 4) != ".men") { continue; } auto bytes = ResourceHelper::loadFile(it.file); if (bytes.empty()) { continue; } Menu *m = new Menu(renderer_, ""); m->loadFromBytes(bytes, BASE_NAME); const std::string S = stem(it.file); menus_[S] = m; } } auto Resource::getTexture(const std::string &name) -> Texture * { auto it = textures_.find(name); if (it == textures_.end()) { std::cerr << "Resource::getTexture: missing " << name << '\n'; return nullptr; } return it->second; } auto Resource::getSound(const std::string &name) -> Ja::Sound * { auto it = sounds_.find(name); if (it == sounds_.end()) { std::cerr << "Resource::getSound: missing " << name << '\n'; return nullptr; } return it->second; } auto Resource::getMusic(const std::string &name) -> Ja::Music * { auto it = musics_.find(name); if (it == musics_.end()) { std::cerr << "Resource::getMusic: missing " << name << '\n'; return nullptr; } return it->second; } auto Resource::getAnimationLines(const std::string &name) -> std::vector & { auto it = animation_lines_.find(name); if (it == animation_lines_.end()) { static std::vector empty_; std::cerr << "Resource::getAnimationLines: missing " << name << '\n'; return empty_; } return it->second; } auto Resource::getText(const std::string &name) -> Text * { auto it = texts_.find(name); if (it == texts_.end()) { std::cerr << "Resource::getText: missing " << name << '\n'; return nullptr; } return it->second; } auto Resource::getMenu(const std::string &name) -> Menu * { auto it = menus_.find(name); if (it == menus_.end()) { std::cerr << "Resource::getMenu: missing " << name << '\n'; return nullptr; } return it->second; } void Resource::reloadForSkin(const std::string &skin_id) { if (SkinManager::get() == nullptr || !SkinManager::get()->exists(skin_id)) { return; } SkinManager::get()->setCurrent(skin_id); Asset::get()->onSkinChanged(); // Recarrega cada textura cachejada des del nou path mantenint pointer // identity perquè els Sprites no s'invaliden. for (auto &[bname, tex] : textures_) { const std::string NEW_PATH = Asset::get()->get(bname); if (!NEW_PATH.empty()) { tex->reLoadFromPath(NEW_PATH); } } // Re-parseja els fitxers .ani de la nova skin. for (auto &[bname, lines] : animation_lines_) { const std::string NEW_PATH = Asset::get()->get(bname); if (NEW_PATH.empty()) { continue; } auto bytes = ResourceHelper::loadFile(NEW_PATH); if (bytes.empty()) { continue; } std::string content(reinterpret_cast(bytes.data()), bytes.size()); std::stringstream ss(content); std::vector new_lines; std::string line; while (std::getline(ss, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } new_lines.push_back(line); } lines = std::move(new_lines); } Options::settings.skin = skin_id; Options::saveToFile(); }