#include "core/resources/skin_manager.hpp" #include #include #include #include "core/resources/resource_helper.h" #include "core/resources/resource_loader.h" #include "external/fkyaml_node.hpp" SkinManager *SkinManager::instance = nullptr; void SkinManager::init(const std::string &executable_path, const std::string &asset_prefix) { if (instance == nullptr) { instance = new SkinManager(executable_path, asset_prefix); instance->scan(); } } void SkinManager::destroy() { delete instance; instance = nullptr; } auto SkinManager::get() -> SkinManager * { return instance; } SkinManager::SkinManager(std::string executable_path, std::string asset_prefix) : executable_path_(std::move(executable_path)), asset_prefix_(std::move(asset_prefix)) { } void SkinManager::scan() { skins_.clear(); // ResourceLoader enumera tant entries del pack com fitxers de // `data/` en mode fallback. Les claus retornades són relatives a // `data/` (ex: "skins/classic/skin.yaml"). Buscar manifests // amb aquest patró és més robust que `std::filesystem` perquè // funciona igual a release (pack) que a dev (filesystem). const auto ALL = ResourceLoader::getInstance().getAvailableResources(); const std::string PREFIX = "skins/"; const std::string SUFFIX = "/skin.yaml"; for (const auto &key : ALL) { if (!key.starts_with(PREFIX) || !key.ends_with(SUFFIX)) { continue; } const std::string ID = key.substr(PREFIX.size(), key.size() - PREFIX.size() - SUFFIX.size()); if (ID.find('/') != std::string::npos) { continue; } // només top-level dins skins/ // ResourceHelper espera un path que contingui "data/" — el construïm // amb la convenció habitual del projecte. const std::string FULL_PATH = executable_path_ + asset_prefix_ + "/data/" + key; auto bytes = ResourceHelper::loadFile(FULL_PATH); if (bytes.empty()) { std::cerr << "SkinManager: cannot read manifest " << FULL_PATH << '\n'; continue; } SkinInfo info; info.id = ID; info.display_name = ID; info.gfx_dir = "gfx"; try { const std::string CONTENT(reinterpret_cast(bytes.data()), bytes.size()); auto yaml = fkyaml::node::deserialize(CONTENT); if (yaml.contains("display_name")) { info.display_name = yaml["display_name"].get_value(); } if (yaml.contains("gfx_dir")) { info.gfx_dir = yaml["gfx_dir"].get_value(); } } catch (const fkyaml::exception &e) { std::cerr << "SkinManager: bad manifest " << FULL_PATH << ": " << e.what() << '\n'; continue; } skins_.push_back(std::move(info)); } // Ordre alfabètic estable: garanteix que next/prev són deterministes. std::ranges::sort(skins_, [](const SkinInfo &a, const SkinInfo &b) { return a.id < b.id; }); } auto SkinManager::exists(const std::string &id) const -> bool { return std::ranges::any_of(skins_, [&](const SkinInfo &s) { return s.id == id; }); } auto SkinManager::currentDisplayName() const -> std::string { return displayNameOf(current_id_); } auto SkinManager::displayNameOf(const std::string &id) const -> std::string { const auto IT = std::ranges::find_if(skins_, [&](const SkinInfo &s) { return s.id == id; }); return (IT != skins_.end()) ? IT->display_name : id; } void SkinManager::setCurrent(const std::string &id) { if (exists(id)) { current_id_ = id; } } auto SkinManager::gfxDirOf(const std::string &id) const -> std::string { const auto IT = std::ranges::find_if(skins_, [&](const SkinInfo &s) { return s.id == id; }); return (IT != skins_.end()) ? IT->gfx_dir : std::string("gfx"); } auto SkinManager::gfxPath(const std::string &basename) const -> std::string { return asset_prefix_ + "/data/skins/" + current_id_ + "/" + gfxDirOf(current_id_) + "/" + basename; } auto SkinManager::nextSkinId() const -> std::string { if (skins_.empty()) { return current_id_; } for (size_t i = 0; i < skins_.size(); ++i) { if (skins_[i].id == current_id_) { return skins_[(i + 1) % skins_.size()].id; } } return skins_.front().id; } auto SkinManager::prevSkinId() const -> std::string { if (skins_.empty()) { return current_id_; } for (size_t i = 0; i < skins_.size(); ++i) { if (skins_[i].id == current_id_) { return skins_[(i + skins_.size() - 1) % skins_.size()].id; } } return skins_.front().id; }