feat(config): afegeix GameplayConfig persistent amb versionatge defensiu
Singleton paral·lel a UiConfig però escrivible: viu a la carpeta de sistema (gameplay.yaml). Defaults compilats: friendly_fire, players_collide i whip_damage_pierces_walls a true. Davant qualsevol irregularitat (fitxer absent/buit, parse error, version absent o desconeguda, clau requerida mal tipada o absent), reseteja a defaults i reescriu el fitxer en lloc de preservar estat parcial. Cablejat al Director::start després de Options::loadFromFile. Encara sense consumidors — els flags es cablejaran a WhipAttack i al sistema de col·lisió de jugadors en commits posteriors.
This commit is contained in:
1 parent
e9f99a0542
commit
932b457725
4 files changed
+241
No files matched your search
@@ -0,0 +1,163 @@
|
||||
#include "game/gameplay_config.hpp"
|
||||
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#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<bool>();
|
||||
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<int>();
|
||||
} 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_);
|
||||
}
|
||||
Reference in new issue
Block a user