From 932b4577256c1312f72fa0a765862ebc67054930 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 13:17:28 +0200 Subject: [PATCH] feat(config): afegeix GameplayConfig persistent amb versionatge defensiu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CMakeLists.txt | 1 + source/core/system/director.cpp | 6 ++ source/game/gameplay_config.cpp | 163 ++++++++++++++++++++++++++++++++ source/game/gameplay_config.hpp | 71 ++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 source/game/gameplay_config.cpp create mode 100644 source/game/gameplay_config.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 537e2be..602c69f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ set(APP_SOURCES source/utils/log.cpp source/utils/utils.cpp source/game/debug_config.cpp + source/game/gameplay_config.cpp source/game/options.cpp source/game/options_apply.cpp source/game/ui/service_menu.cpp diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 29f5fe8..e0a3c3d 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -29,6 +29,7 @@ #include "core/system/event_buffer.hpp" #include "core/ui/ui_config.hpp" #include "game/debug_config.hpp" +#include "game/gameplay_config.hpp" #ifdef _DEBUG #include "game/editor/editor_config.hpp" #endif @@ -93,6 +94,11 @@ Director::Director() { // el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`). GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml"); + // Tweaks de jugabilitat (látigo travessa sarcs, friendly fire, col·lisió + // entre jugadors). Fitxer escrivible des del menú de servei (F9). Si + // no existeix, GameplayConfig::load el crea amb els defaults. + GameplayConfig::get().load(system_folder_ + "gameplay.yaml"); + // Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix // es crea amb presets per defecte (off, crt, ntsc). Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE); diff --git a/source/game/gameplay_config.cpp b/source/game/gameplay_config.cpp new file mode 100644 index 0000000..1a4ea07 --- /dev/null +++ b/source/game/gameplay_config.cpp @@ -0,0 +1,163 @@ +#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_); +} diff --git a/source/game/gameplay_config.hpp b/source/game/gameplay_config.hpp new file mode 100644 index 0000000..1608937 --- /dev/null +++ b/source/game/gameplay_config.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include + +// Tweaks de jugabilitat persistents per usuari. Carregat de +// `gameplay.yaml` dins la carpeta de sistema (vegeu +// `Director::createSystemFolder` per la ubicació real per plataforma). +// +// Diferent d'UiConfig (data/config/ui.yaml, només lectura): aquest fitxer +// és escrivible i el ServiceMenu en modifica els valors. Els defaults +// compilats són els que veuen els nous usuaris fins que toquen el menú. +// +// Política d'integritat: davant qualsevol irregularitat (fitxer absent, +// parse error, `version` absent o diferent, clau requerida absent o de +// tipus incorrecte), `load()` reseteja a defaults i reescriu el fitxer. +// No es preserven fitxers parcialment vàlids — així evitem dragar +// versions antigues incoherents quan l'esquema evolucioni. +class GameplayConfig { + public: + // Esquema actual del fitxer. Incrementar quan canviï l'estructura + // (renomenar/treure claus o canviar semàntica). Els fitxers d'usuari + // amb una `version` distinta es reescriuen amb els defaults nous. + // Afegir noves claus opcionals no requereix bump si els defaults + // compilats són equivalents al comportament anterior. + static constexpr int CURRENT_VERSION = 1; + + static auto get() -> GameplayConfig&; + + GameplayConfig(const GameplayConfig&) = delete; + GameplayConfig(GameplayConfig&&) = delete; + auto operator=(const GameplayConfig&) -> GameplayConfig& = delete; + auto operator=(GameplayConfig&&) -> GameplayConfig& = delete; + + // Llig el fitxer i guarda la ruta per a `save()`. Si no existeix, + // l'escriu amb els defaults compilats. Idempotent: pot cridar-se + // múltiples vegades (l'última crida fixa el path actiu). + void load(const std::string& file_path); + + // Persisteix l'estat actual al path passat a `load()`. Cridat pel + // ServiceMenu a cada canvi de toggle. No-op si `load()` no s'ha + // cridat encara (sense path no podem escriure enlloc útil). + void save() const; + + // Tweaks. Defaults: tots true. + // + // - whip_damage_pierces_walls: el dany del látigo travessa sarcòfags + // (NO el mur extern del playfield, que retalla igualment per + // evitar que el rect surti de la sala visible). + // - friendly_fire: un latigazo pot impactar l'altre jugador. + // - players_collide: els jugadors col·lisionen físicament entre si + // en lloc de travessar-se. + [[nodiscard]] auto whipDamagePiercesWalls() const -> bool { return whip_damage_pierces_walls_; } + [[nodiscard]] auto friendlyFire() const -> bool { return friendly_fire_; } + [[nodiscard]] auto playersCollide() const -> bool { return players_collide_; } + + void setWhipDamagePiercesWalls(bool v) { whip_damage_pierces_walls_ = v; } + void setFriendlyFire(bool v) { friendly_fire_ = v; } + void setPlayersCollide(bool v) { players_collide_ = v; } + + private: + GameplayConfig() = default; + ~GameplayConfig() = default; + + // Ruta absoluta al fitxer persistit. Buida fins que algú crida + // `load()` per primera vegada; mentre estigui buida, `save()` és no-op. + std::string file_path_; + + bool whip_damage_pierces_walls_{true}; + bool friendly_fire_{true}; + bool players_collide_{true}; +};