#include "game/gameplay_config.hpp" #include #include #include #include #include #include "external/fkyaml_node.hpp" #include "utils/log.hpp" namespace { auto readFile(const std::string& path) -> std::string { std::ifstream file(path); if (!file) { return {}; } std::stringstream buffer; buffer << file.rdbuf(); return buffer.str(); } // Llig una subclau booleana obligatòria (`section.key`). Retorna // false si la secció o la clau no hi són o el tipus no és bool — // qualsevol d'aquests casos és una irregularitat que dispara // reescriptura del fitxer. auto readRequiredBool(const fkyaml::node& root, const char* section, const char* key, bool& out) -> bool { if (!root.contains(section)) { return false; } const auto& sec = root[section]; if (!sec.contains(key)) { return false; } try { out = sec[key].get_value(); return true; } catch (const std::exception&) { return false; } } // Escriu el fitxer amb els valors actuals + comentaris d'ajuda. // Format estable perquè l'usuari pot editar-lo a mà. void writeFile(const std::string& path, int version, bool whip_pierces, bool friendly_fire, bool players_collide) { std::ofstream file(path); if (!file) { Log::error("gameplay.yaml", "no s'ha pogut escriure {}", path); return; } file << "# gameplay.yaml — tweaks de jugabilitat persistents.\n"; file << "# Edita des del menú de servei (F9 → Tweaks) o a mà.\n"; file << "# Si esborres el fitxer (o aquesta capçalera de versió), es regenerarà\n"; file << "# amb els defaults a la propera arrencada.\n"; file << "\n"; file << "version: " << version << "\n"; file << "\n"; file << "whip:\n"; file << " damage_pierces_walls: " << (whip_pierces ? "true" : "false") << "\n"; file << "\n"; file << "combat:\n"; file << " friendly_fire: " << (friendly_fire ? "true" : "false") << "\n"; file << "\n"; file << "players:\n"; file << " collide_with_each_other: " << (players_collide ? "true" : "false") << "\n"; } } // namespace auto GameplayConfig::get() -> GameplayConfig& { static GameplayConfig instance_; return instance_; } void GameplayConfig::load(const std::string& file_path) { file_path_ = file_path; std::error_code error; const bool EXISTS = std::filesystem::exists(file_path, error); const auto CONTENT = EXISTS ? readFile(file_path) : std::string{}; // Tots els valors van a temporaris; només ens els quedem si la // validació completa passa. Així una validació parcial (p. ex. dues // claus bones i una de dolenta) no deixa l'estat a mig camí. bool whip_pierces = whip_damage_pierces_walls_; bool ff = friendly_fire_; bool players_collide = players_collide_; auto rewrite_with_defaults = [&](const char* reason) { // Resetem els camps als defaults compilats abans de reescriure. // Necessari si load() es crida diverses vegades amb fitxers // distints; el primer pot haver mutat l'estat. whip_damage_pierces_walls_ = true; friendly_fire_ = true; players_collide_ = true; writeFile(file_path, CURRENT_VERSION, whip_damage_pierces_walls_, friendly_fire_, players_collide_); Log::info("gameplay.yaml", "{} — reescrit a {} amb defaults", reason, file_path); }; if (!EXISTS) { rewrite_with_defaults("fitxer absent"); return; } if (CONTENT.empty()) { rewrite_with_defaults("fitxer buit"); return; } fkyaml::node yaml; try { yaml = fkyaml::node::deserialize(CONTENT); } catch (const std::exception& e) { rewrite_with_defaults((std::string("parse error: ") + e.what()).c_str()); return; } // `version` ha de ser un enter present i igual a CURRENT_VERSION. // Qualsevol altra cosa (absent, tipus no enter, valor diferent) és // una irregularitat: el fitxer ve d'una versió incompatible o ha // estat manipulat de manera inesperada. if (!yaml.contains("version")) { rewrite_with_defaults("version absent"); return; } int version = 0; try { version = yaml["version"].get_value(); } catch (const std::exception&) { rewrite_with_defaults("version no és enter"); return; } if (version != CURRENT_VERSION) { rewrite_with_defaults("version desconeguda"); return; } if (!readRequiredBool(yaml, "whip", "damage_pierces_walls", whip_pierces) || !readRequiredBool(yaml, "combat", "friendly_fire", ff) || !readRequiredBool(yaml, "players", "collide_with_each_other", players_collide)) { rewrite_with_defaults("clau requerida absent o de tipus incorrecte"); return; } // Tot validat: ens quedem els valors llegits. whip_damage_pierces_walls_ = whip_pierces; friendly_fire_ = ff; players_collide_ = players_collide; } void GameplayConfig::save() const { if (file_path_.empty()) { // Sense path establert: cap consumidor s'ha registrat encara. // Pot passar en tests que no carreguen el fitxer; silenciem el cas. return; } writeFile(file_path_, CURRENT_VERSION, whip_damage_pierces_walls_, friendly_fire_, players_collide_); }