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
@@ -107,6 +107,7 @@ set(APP_SOURCES
|
|||||||
source/utils/log.cpp
|
source/utils/log.cpp
|
||||||
source/utils/utils.cpp
|
source/utils/utils.cpp
|
||||||
source/game/debug_config.cpp
|
source/game/debug_config.cpp
|
||||||
|
source/game/gameplay_config.cpp
|
||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
source/game/options_apply.cpp
|
source/game/options_apply.cpp
|
||||||
source/game/ui/service_menu.cpp
|
source/game/ui/service_menu.cpp
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/debug_config.hpp"
|
#include "game/debug_config.hpp"
|
||||||
|
#include "game/gameplay_config.hpp"
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#include "game/editor/editor_config.hpp"
|
#include "game/editor/editor_config.hpp"
|
||||||
#endif
|
#endif
|
||||||
@@ -93,6 +94,11 @@ Director::Director() {
|
|||||||
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
||||||
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
|
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
|
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
|
||||||
// es crea amb presets per defecte (off, crt, ntsc).
|
// es crea amb presets per defecte (off, crt, ntsc).
|
||||||
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
||||||
|
|||||||
@@ -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_);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// 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};
|
||||||
|
};
|
||||||
Reference in new issue
Block a user