refactor(game): COUP-03 + CONV-02 encapsulate Options state behind setters
L'estat mutable d'Options (window, video, audio, locale, presets, paths) es mou a un anonymous namespace dins options.cpp. L'API pública del namespace passa a ser: - Getters const-ref: window(), video(), audio(), locale(), configVersion(), postfxPresets(), crtpiPresets(). - Setters específics que muten i persisteixen via saveToFile(): setWindowZoom, setWindowFullscreen, setVideoVSync, setVideoScalingMode, setVideoRenderInfoPosition, setVideoRenderInfoShowTime, setShaderEnabled, setActiveShaderName, setPostFXPreset, setCrtPiPreset. Els setters NO notifiquen els subsistemes: el caller (global_inputs.cpp) crida explícitament Screen::setWindowZoom, reapplyShader, etc. La part "observers" del audit es deixa WONTFIX justificat — notificar des d'Options invertiria la direcció Director→Screen/Audio i Options aprenria del layout dels subsistemes. CONV-02 tanca al pas: la part Options dels "globals sense observers" queda resolta amb la nova encapsulació. Map::* (l'altra meitat de CONV-02) ja s'havia tancat amb PATCH-07. Smoke tests 13/13 OK; log Game start → Bye! sota xvfb-run + SDL_VIDEODRIVER=dummy + SIGINT idèntic al baseline de main. clang-tidy i cppcheck nets sobre els fitxers modificats. Refs: docs/audit-game.md (COUP-03, CONV-02, INCONS-01). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
32dcf264c4
commit
a3b03b87be
5 files changed
+256
-160
No files matched your search
+14
-4
@@ -3,7 +3,7 @@
|
||||
Rama: `audit/game`.
|
||||
Cobertura: `source/game/{entities,map,scenes,ui}` + `source/game/options.{hpp,cpp}` (12 fitxers, ~1.486 línies).
|
||||
|
||||
Estado global: **28 DONE, 2 DONE (parcial), 5 WONTFIX, 4 DEFERRED** sobre 39 ítems (el top 5 compta dins dels seus IDs individuals). Cada canvi té un commit aïllat i s'ha provat amb `SDL_VIDEODRIVER=dummy` + `SIGINT` (el log de `Game start` a `Bye!` és idèntic entre commits; la branca `config.yaml` de backup verificada manualment amb un fitxer corrupte). Els 4 DEFERRED (COUP-02, COUP-03, CONV-02 resta, PATCH-01) van a la futura branca `refactor/options-class`.
|
||||
Estado global: **28 DONE, 2 DONE (parcial), 5 WONTFIX, 4 DEFERRED** sobre 39 ítems (el top 5 compta dins dels seus IDs individuals). Cada canvi té un commit aïllat i s'ha provat amb `SDL_VIDEODRIVER=dummy` + `SIGINT` (el log de `Game start` a `Bye!` és idèntic entre commits; la branca `config.yaml` de backup verificada manualment amb un fitxer corrupte). Els 4 DEFERRED (COUP-02, COUP-03, CONV-02 resta, PATCH-01) es tanquen a la branca `refactor/options-class` (registre de commits al final del fitxer).
|
||||
|
||||
Estados posibles:
|
||||
- `PENDING` — identificat, sense tocar
|
||||
@@ -69,14 +69,14 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer
|
||||
|----|--------------|----------|-----------|-------|-------|
|
||||
| COUP-01 | `core/input/global_inputs.cpp:15` | `core/` inclou `game/ui/notification_presets.hpp` | Crítico | DONE | `notification_presets.hpp` mogut a `core/rendering/` (al costat del `Notifier` que consumeix). `global_inputs.cpp` i el comentari de `notifier.hpp` actualitzats |
|
||||
| COUP-02 | `player.cpp` + `Map::PLAYFIELD_*`, `PITCH_*`, `GRID_*`, `collides` | `Player` coneix la geometria interna del mapa | Importante | DEFERRED | Player només llegeix globals de `Map::`, no muta res. Injectar `MapInfo&` té sentit quan hi haja múltiples mapes simultanis o un sistema tipus ECS. Mentre hi haja un Map actiu global, l'acoblament és acceptable. Es refacta amb COUP-03 a `refactor/options-class` |
|
||||
| COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | DEFERRED | Encapsular tots els globals darrere setters + observers per Screen/Audio/RenderInfo és un refactor ample (equivalent a COUP-01 de l'audit d'àudio, que va anar a una branca pròpia). Es tractarà a `refactor/options-class` perquè toca tots els callers de core/input. Es fa en branca separada |
|
||||
| COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | DONE | Estat mutable mogut a anonymous namespace dins `options.cpp`; API pública són getters const-ref (`window()`, `video()`, `audio()`, `locale()`, `postfxPresets()`, `crtpiPresets()`) + setters específics (`setWindowZoom`, `setShaderEnabled`, `setActiveShaderName`, ...) que muten i persisteixen via `saveToFile()`. Els setters NO notifiquen els subsistemes: el caller (`global_inputs.cpp`) crida després `Screen::setWindowZoom`, `reapplyShader()`, etc. La part "observers" del audit es deixa WONTFIX (veure CONV-02): notificar des d'Options invertiria la direcció `Director→Screen/Audio` i Options aprenria del layout dels subsistemes |
|
||||
| COUP-04 | `tilemap.cpp:117-138` | Clip+blit directe al canvas ARGB que podria viure a `Surface` | Menor | WONTFIX | Únic caller en tot el projecte. Promoure a `Surface::blitInto` ara afegiria una API genèrica per a un ús únic (YAGNI). Si un segon client apareix, es mourea en aquell moment |
|
||||
|
||||
## 5. Inconsistències internes
|
||||
|
||||
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes |
|
||||
|----|--------------|----------|-----------|-------|-------|
|
||||
| INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | DONE (parcial) | Resolt "classe all-static" amb PATCH-07 (Map → namespace). Queden 3 estils diferenciats per responsabilitat: `namespace` per a estat compartit (Options, Map), classes per a entitats amb estat (Player, Tilemap), `namespace+constexpr` per a alias/presets (UiColor, Notification). La distinció ara és intencional, no fortuïta |
|
||||
| INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | DONE (parcial) | Resolt "classe all-static" amb PATCH-07 (Map → namespace). Queden 3 estils diferenciats per responsabilitat: `namespace` per a estat compartit (Options, Map), classes per a entitats amb estat (Player, Tilemap), `namespace+constexpr` per a alias/presets (UiColor, Notification). La distinció ara és intencional, no fortuïta. NOTA (post `refactor/options-class`): "encapsular" en COUP-03 vol dir ocultar l'estat mutable darrere de l'API del namespace (anonymous namespace + getters/setters), no convertir Options a classe — el patró `namespace per a estat compartit` queda preservat |
|
||||
| INCONS-02 | `options.cpp:372,477,530,579,607` | `std::cerr` en comptes de `std::cout` (convenció del projecte) | Importante | DONE | Totes les ocurrències (10, incloent les afegides a PATCH-04) substituïdes per `std::cout`. Test manual: amb config.yaml trencat el missatge de backup va per stdout |
|
||||
| INCONS-03 | `options.cpp` vs `tilemap.cpp:77-78` | Diferent política en YAML absent: `contains()` vs accés directe que llança | Menor | WONTFIX | Les dues polítiques són correctes per al seu cas: `Tilemap`/`Map::loadFrom` tenen fitxers de definició (asset obligatori → llançar); `Options` parseja preferències de l'usuari (camp absent → manté default). No és inconsistència sinó dos contractes diferents |
|
||||
| INCONS-04 | `options.cpp`, `player.cpp` | Locals ALL-CAPS (`CONTENT`, `IT`, `DT`, `OLD_X`) barrejats amb `snake_case` | Menor | WONTFIX | El `.clang-tidy` del projecte exigeix `LocalConstantCase = UPPER_CASE` i `VariableCase = lower_case` — els ALL-CAPS són per a `const`/`constexpr` i els snake_case per a mutables. La barreja es correcta i està dictada pel linter |
|
||||
@@ -97,6 +97,16 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer
|
||||
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes |
|
||||
|----|--------------|----------|-----------|-------|-------|
|
||||
| CONV-01 | `player.*`, `map.*`, `ui/*`, `game_scene.cpp` | Speed, caixa de col·lisió, geometria, índexs paleta, rutes d'assets, nom del track: tot cablejat | Crítico | DONE | Tapat per HC-01/02/03/04/05/06: speed a Defaults::Player, col·lisió a player.yaml, geometria a pyramid.yaml, índexs paleta a Defaults::UI, rutes a Defaults::Pyramid + Config |
|
||||
| CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | DONE (parcial) | Map resolt amb PATCH-07 (namespace). La part d'Options/observers va amb COUP-03 (DEFERRED a `refactor/options-class`) |
|
||||
| CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | DONE | Map resolt amb PATCH-07 (namespace). Options resolt amb COUP-03: estat mutable encapsulat darrere de l'API del namespace (anonymous ns + getters/setters). La part "observers" (notificacions automàtiques a Screen/Audio) queda WONTFIX justificat: invertiria la direcció de dependència `Director→Screen/Audio` que el projecte manté intencionadament |
|
||||
| CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | DONE | Resolt amb INCONS-02 |
|
||||
| CONV-04 | `player.cpp:27` | `SDL_GetKeyboardState` directe en `Player::update` (fuga de SDL al gameplay) | Menor | WONTFIX | `SDL_GetKeyboardState` és l'accés correcte per a state-based polling a 60Hz; `EventBuffer` és per a esdeveniments discrets. La fuga és cosmètica — abstraure-la no aportaria valor |
|
||||
|
||||
---
|
||||
|
||||
## Registro de commits — `refactor/options-class`
|
||||
|
||||
Branca per a tancar els 4 ítems DEFERRED de `audit/game`. Base: `main` (32dcf26).
|
||||
|
||||
- `<pendent>` — COUP-03 + CONV-02: estat mutable d'`Options` mogut a anonymous ns + API de getters/setters; `global_inputs.cpp` i `director.cpp` adaptats. CONV-02 (part Options) tanca al pas. La part "observers" del audit es deixa WONTFIX (invertiria `Director→Screen/Audio`)
|
||||
- `<pendent>` — COUP-02: `MapInfo` plana injectada al `Player` (només Player; momia/roca/loadlevel resten amb `Map::*` global, com diu la nota original del audit)
|
||||
- `<pendent>` — PATCH-01: `tryMove(dx, dy, collisionFn)` a `MovingSprite` amb hook virtual `collisionBox()`; `Player::update` deixa de fer `update + revertir + reaplicar`
|
||||
@@ -42,83 +42,86 @@ namespace GlobalInputs {
|
||||
}
|
||||
|
||||
void onDecZoom() {
|
||||
if (Options::window.fullscreen) { return; }
|
||||
if (Options::window.zoom <= MIN_ZOOM) { return; }
|
||||
Options::window.zoom--;
|
||||
Screen::get().setWindowZoom(Options::window.zoom);
|
||||
Options::saveToFile();
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom));
|
||||
if (Options::window().fullscreen) { return; }
|
||||
if (Options::window().zoom <= MIN_ZOOM) { return; }
|
||||
const int NEW_ZOOM = Options::window().zoom - 1;
|
||||
Options::setWindowZoom(NEW_ZOOM);
|
||||
Screen::get().setWindowZoom(NEW_ZOOM);
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
|
||||
}
|
||||
|
||||
void onIncZoom() {
|
||||
if (Options::window.fullscreen) { return; }
|
||||
if (Options::window.zoom >= MAX_ZOOM) { return; }
|
||||
Options::window.zoom++;
|
||||
Screen::get().setWindowZoom(Options::window.zoom);
|
||||
Options::saveToFile();
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom));
|
||||
if (Options::window().fullscreen) { return; }
|
||||
if (Options::window().zoom >= MAX_ZOOM) { return; }
|
||||
const int NEW_ZOOM = Options::window().zoom + 1;
|
||||
Options::setWindowZoom(NEW_ZOOM);
|
||||
Screen::get().setWindowZoom(NEW_ZOOM);
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
|
||||
}
|
||||
|
||||
void onFullscreen() {
|
||||
Options::window.fullscreen = !Options::window.fullscreen;
|
||||
Screen::get().setFullscreen(Options::window.fullscreen);
|
||||
Options::saveToFile();
|
||||
notifyLocale(Options::window.fullscreen ? "input.fullscreen_on" : "input.fullscreen_off");
|
||||
const bool NEW_FS = !Options::window().fullscreen;
|
||||
Options::setWindowFullscreen(NEW_FS);
|
||||
Screen::get().setFullscreen(NEW_FS);
|
||||
notifyLocale(NEW_FS ? "input.fullscreen_on" : "input.fullscreen_off");
|
||||
}
|
||||
|
||||
void onToggleVSync() {
|
||||
Options::video.vsync = !Options::video.vsync;
|
||||
Screen::get().setVSync(Options::video.vsync);
|
||||
Options::saveToFile();
|
||||
notifyLocale(Options::video.vsync ? "input.vsync_on" : "input.vsync_off");
|
||||
const bool NEW_VSYNC = !Options::video().vsync;
|
||||
Options::setVideoVSync(NEW_VSYNC);
|
||||
Screen::get().setVSync(NEW_VSYNC);
|
||||
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
||||
}
|
||||
|
||||
void onToggleShader() {
|
||||
Options::video.shader.enabled = !Options::video.shader.enabled;
|
||||
const bool NEW_ENABLED = !Options::video().shader.enabled;
|
||||
Options::setShaderEnabled(NEW_ENABLED);
|
||||
reapplyShader();
|
||||
Options::saveToFile();
|
||||
notifyLocale(Options::video.shader.enabled ? "input.shader_on" : "input.shader_off");
|
||||
notifyLocale(NEW_ENABLED ? "input.shader_on" : "input.shader_off");
|
||||
}
|
||||
|
||||
void onNextShader() {
|
||||
Options::video.shader.current_shader = (Options::video.shader.current_shader == "crtpi") ? "postfx" : "crtpi";
|
||||
const std::string NEW_NAME = (Options::video().shader.current_shader == "crtpi") ? "postfx" : "crtpi";
|
||||
Options::setActiveShaderName(NEW_NAME);
|
||||
reapplyShader();
|
||||
Options::saveToFile();
|
||||
Notification::notify(Locale::get().text("input.shader") + ": " + Options::video.shader.current_shader);
|
||||
Notification::notify(Locale::get().text("input.shader") + ": " + NEW_NAME);
|
||||
}
|
||||
|
||||
// Busca l'entrada actual al vector de presets i passa a la següent
|
||||
// (wrap-around). Si no en troba cap s'evita el canvi silenciosament.
|
||||
void onNextPreset() {
|
||||
const bool IS_CRTPI = Options::video.shader.current_shader == "crtpi";
|
||||
const bool IS_CRTPI = Options::video().shader.current_shader == "crtpi";
|
||||
std::string new_preset;
|
||||
if (IS_CRTPI) {
|
||||
if (Options::crtpi_presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(Options::crtpi_presets,
|
||||
[](const auto& preset) { return preset.name == Options::video.shader.current_crtpi_preset; });
|
||||
std::size_t idx = (it == Options::crtpi_presets.end()) ? 0 : std::distance(Options::crtpi_presets.begin(), it);
|
||||
idx = (idx + 1) % Options::crtpi_presets.size();
|
||||
Options::video.shader.current_crtpi_preset = Options::crtpi_presets[idx].name;
|
||||
const auto& presets = Options::crtpiPresets();
|
||||
if (presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(presets,
|
||||
[](const auto& preset) { return preset.name == Options::video().shader.current_crtpi_preset; });
|
||||
std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
|
||||
idx = (idx + 1) % presets.size();
|
||||
new_preset = presets[idx].name;
|
||||
Options::setCrtPiPreset(new_preset);
|
||||
} else {
|
||||
if (Options::postfx_presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(Options::postfx_presets,
|
||||
[](const auto& preset) { return preset.name == Options::video.shader.current_postfx_preset; });
|
||||
std::size_t idx = (it == Options::postfx_presets.end()) ? 0 : std::distance(Options::postfx_presets.begin(), it);
|
||||
idx = (idx + 1) % Options::postfx_presets.size();
|
||||
Options::video.shader.current_postfx_preset = Options::postfx_presets[idx].name;
|
||||
const auto& presets = Options::postfxPresets();
|
||||
if (presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(presets,
|
||||
[](const auto& preset) { return preset.name == Options::video().shader.current_postfx_preset; });
|
||||
std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
|
||||
idx = (idx + 1) % presets.size();
|
||||
new_preset = presets[idx].name;
|
||||
Options::setPostFXPreset(new_preset);
|
||||
}
|
||||
reapplyShader();
|
||||
Options::saveToFile();
|
||||
const std::string& name = IS_CRTPI ? Options::video.shader.current_crtpi_preset : Options::video.shader.current_postfx_preset;
|
||||
Notification::notify(Locale::get().text("input.preset") + ": " + name);
|
||||
Notification::notify(Locale::get().text("input.preset") + ": " + new_preset);
|
||||
}
|
||||
|
||||
void onNextScalingMode() {
|
||||
using Mode = Defaults::ScalingMode;
|
||||
Options::video.scaling_mode = static_cast<Mode>((static_cast<int>(Options::video.scaling_mode) + 1) % 5);
|
||||
Screen::get().setScalingMode(Options::video.scaling_mode);
|
||||
Options::saveToFile();
|
||||
const auto NEW_MODE = static_cast<Mode>((static_cast<int>(Options::video().scaling_mode) + 1) % 5);
|
||||
Options::setVideoScalingMode(NEW_MODE);
|
||||
Screen::get().setScalingMode(NEW_MODE);
|
||||
const char* mode_key = nullptr;
|
||||
switch (Options::video.scaling_mode) {
|
||||
switch (NEW_MODE) {
|
||||
case Mode::DISABLED:
|
||||
mode_key = "input.scaling_disabled";
|
||||
break;
|
||||
@@ -139,14 +142,12 @@ namespace GlobalInputs {
|
||||
}
|
||||
|
||||
void onCycleRenderInfo() {
|
||||
const int NEXT = (static_cast<int>(Options::video.render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT;
|
||||
Options::video.render_info_position = static_cast<Defaults::RenderInfoPosition>(NEXT);
|
||||
Options::saveToFile();
|
||||
const int NEXT = (static_cast<int>(Options::video().render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT;
|
||||
Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT));
|
||||
}
|
||||
|
||||
void onToggleRenderTime() {
|
||||
Options::video.render_info_show_time = !Options::video.render_info_show_time;
|
||||
Options::saveToFile();
|
||||
Options::setVideoRenderInfoShowTime(!Options::video().render_info_show_time);
|
||||
}
|
||||
|
||||
void onExit() {
|
||||
|
||||
@@ -61,7 +61,7 @@ Director::Director() {
|
||||
|
||||
// Traduccions per a tots els textos visibles. Si el fitxer no existeix
|
||||
// o hi ha un error de parse, Locale::text() caurà a retornar la clau.
|
||||
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale + ".yaml");
|
||||
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
|
||||
|
||||
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
|
||||
KeyConfig::get().load("data/input/keys.yaml");
|
||||
|
||||
+157
-91
@@ -14,6 +14,21 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// --- Estat mutable (privat al TU) ---
|
||||
// Encapsulat ací per impedir mutacions directes des de fora del namespace
|
||||
// Options:: (decisió COUP-03 / docs/audit-game.md). Els callers llegeixen
|
||||
// via getters const-ref i muten via setters específics que persisteixen.
|
||||
std::string g_config_version;
|
||||
std::string g_config_file_path;
|
||||
std::string g_postfx_file_path;
|
||||
std::string g_crtpi_file_path;
|
||||
std::string g_locale{Defaults::I18n::LOCALE};
|
||||
Options::Window g_window{};
|
||||
Options::Video g_video{};
|
||||
Options::Audio g_audio{};
|
||||
std::vector<Options::PostFXPreset> g_postfx_presets{};
|
||||
std::vector<Options::CrtPiPreset> g_crtpi_presets{};
|
||||
|
||||
// Taules enum ↔ string per a la serialització a YAML. La primera entrada
|
||||
// és el fallback quan l'enum no té nom conegut o el text no casa amb
|
||||
// cap entrada coneguda — per això `LETTERBOX`/`NEAREST`/`OFF` encapçalen.
|
||||
@@ -93,40 +108,40 @@ namespace {
|
||||
void parseWindow(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("window")) { return; }
|
||||
const auto& node = yaml["window"];
|
||||
readField(node, "zoom", Options::window.zoom);
|
||||
readField(node, "fullscreen", Options::window.fullscreen);
|
||||
readField(node, "zoom", g_window.zoom);
|
||||
readField(node, "fullscreen", g_window.fullscreen);
|
||||
}
|
||||
|
||||
void parseGpu(const fkyaml::node& video_node) {
|
||||
if (!video_node.contains("gpu")) { return; }
|
||||
const auto& node = video_node["gpu"];
|
||||
readField(node, "acceleration", Options::video.gpu.acceleration);
|
||||
readField(node, "preferred_driver", Options::video.gpu.preferred_driver);
|
||||
readField(node, "acceleration", g_video.gpu.acceleration);
|
||||
readField(node, "preferred_driver", g_video.gpu.preferred_driver);
|
||||
}
|
||||
|
||||
void parseShader(const fkyaml::node& video_node) {
|
||||
if (!video_node.contains("shader")) { return; }
|
||||
const auto& node = video_node["shader"];
|
||||
readField(node, "enabled", Options::video.shader.enabled);
|
||||
readField(node, "current_shader", Options::video.shader.current_shader);
|
||||
readField(node, "current_postfx_preset", Options::video.shader.current_postfx_preset);
|
||||
readField(node, "current_crtpi_preset", Options::video.shader.current_crtpi_preset);
|
||||
readField(node, "enabled", g_video.shader.enabled);
|
||||
readField(node, "current_shader", g_video.shader.current_shader);
|
||||
readField(node, "current_postfx_preset", g_video.shader.current_postfx_preset);
|
||||
readField(node, "current_crtpi_preset", g_video.shader.current_crtpi_preset);
|
||||
}
|
||||
|
||||
void parseVideo(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("video")) { return; }
|
||||
const auto& node = yaml["video"];
|
||||
readField(node, "vsync", Options::video.vsync);
|
||||
readField(node, "vsync", g_video.vsync);
|
||||
if (node.contains("scaling_mode")) {
|
||||
Options::video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value<std::string>());
|
||||
g_video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value<std::string>());
|
||||
}
|
||||
if (node.contains("texture_filter")) {
|
||||
Options::video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value<std::string>());
|
||||
g_video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value<std::string>());
|
||||
}
|
||||
if (node.contains("render_info_position")) {
|
||||
Options::video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>());
|
||||
g_video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>());
|
||||
}
|
||||
readField(node, "render_info_show_time", Options::video.render_info_show_time);
|
||||
readField(node, "render_info_show_time", g_video.render_info_show_time);
|
||||
parseGpu(node);
|
||||
parseShader(node);
|
||||
}
|
||||
@@ -134,22 +149,22 @@ namespace {
|
||||
void parseMusic(const fkyaml::node& audio_node) {
|
||||
if (!audio_node.contains("music")) { return; }
|
||||
const auto& node = audio_node["music"];
|
||||
readField(node, "enabled", Options::audio.music.enabled);
|
||||
readField(node, "volume", Options::audio.music.volume);
|
||||
readField(node, "enabled", g_audio.music.enabled);
|
||||
readField(node, "volume", g_audio.music.volume);
|
||||
}
|
||||
|
||||
void parseSound(const fkyaml::node& audio_node) {
|
||||
if (!audio_node.contains("sound")) { return; }
|
||||
const auto& node = audio_node["sound"];
|
||||
readField(node, "enabled", Options::audio.sound.enabled);
|
||||
readField(node, "volume", Options::audio.sound.volume);
|
||||
readField(node, "enabled", g_audio.sound.enabled);
|
||||
readField(node, "volume", g_audio.sound.volume);
|
||||
}
|
||||
|
||||
void parseAudio(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("audio")) { return; }
|
||||
const auto& node = yaml["audio"];
|
||||
readField(node, "enabled", Options::audio.enabled);
|
||||
readField(node, "volume", Options::audio.volume);
|
||||
readField(node, "enabled", g_audio.enabled);
|
||||
readField(node, "volume", g_audio.volume);
|
||||
parseMusic(node);
|
||||
parseSound(node);
|
||||
}
|
||||
@@ -423,14 +438,65 @@ namespace {
|
||||
|
||||
namespace Options {
|
||||
|
||||
// --- Getters de l'estat ---
|
||||
auto window() -> const Window& { return g_window; }
|
||||
auto video() -> const Video& { return g_video; }
|
||||
auto audio() -> const Audio& { return g_audio; }
|
||||
auto locale() -> const std::string& { return g_locale; }
|
||||
auto configVersion() -> const std::string& { return g_config_version; }
|
||||
auto postfxPresets() -> const std::vector<PostFXPreset>& { return g_postfx_presets; }
|
||||
auto crtpiPresets() -> const std::vector<CrtPiPreset>& { return g_crtpi_presets; }
|
||||
|
||||
// --- Setters: muten + persisteixen ---
|
||||
void setWindowZoom(int value) {
|
||||
g_window.zoom = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setWindowFullscreen(bool value) {
|
||||
g_window.fullscreen = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setVideoVSync(bool value) {
|
||||
g_video.vsync = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setVideoScalingMode(Defaults::ScalingMode value) {
|
||||
g_video.scaling_mode = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setVideoRenderInfoPosition(Defaults::RenderInfoPosition value) {
|
||||
g_video.render_info_position = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setVideoRenderInfoShowTime(bool value) {
|
||||
g_video.render_info_show_time = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setShaderEnabled(bool value) {
|
||||
g_video.shader.enabled = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setActiveShaderName(const std::string& value) {
|
||||
g_video.shader.current_shader = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setPostFXPreset(const std::string& value) {
|
||||
g_video.shader.current_postfx_preset = value;
|
||||
saveToFile();
|
||||
}
|
||||
void setCrtPiPreset(const std::string& value) {
|
||||
g_video.shader.current_crtpi_preset = value;
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
void setConfigFile(const std::string& path) {
|
||||
config_file_path = path;
|
||||
g_config_file_path = path;
|
||||
}
|
||||
|
||||
auto loadFromFile() -> bool {
|
||||
if (config_file_path.empty()) { return false; }
|
||||
if (g_config_file_path.empty()) { return false; }
|
||||
|
||||
const std::string CONTENT = readFileOrEmpty(config_file_path);
|
||||
const std::string CONTENT = readFileOrEmpty(g_config_file_path);
|
||||
if (CONTENT.empty()) {
|
||||
// Primera execució: creem el fitxer amb els valors per defecte.
|
||||
return saveToFile();
|
||||
@@ -449,12 +515,12 @@ namespace Options {
|
||||
if (file_version != Texts::VERSION) {
|
||||
std::cout << "Options: config.yaml versió \"" << file_version
|
||||
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
|
||||
backupFile(config_file_path);
|
||||
backupFile(g_config_file_path);
|
||||
return saveToFile();
|
||||
}
|
||||
config_version = file_version;
|
||||
g_config_version = file_version;
|
||||
|
||||
if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value<std::string>(); }
|
||||
if (yaml.contains("locale")) { g_locale = yaml["locale"].get_value<std::string>(); }
|
||||
parseWindow(yaml);
|
||||
parseVideo(yaml);
|
||||
parseAudio(yaml);
|
||||
@@ -462,84 +528,84 @@ namespace Options {
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
||||
std::cout << "Options: reescrivint el fitxer amb defaults.\n";
|
||||
backupFile(config_file_path);
|
||||
backupFile(g_config_file_path);
|
||||
return saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
auto saveToFile() -> bool {
|
||||
if (config_file_path.empty()) { return false; }
|
||||
if (g_config_file_path.empty()) { return false; }
|
||||
|
||||
config_version = Texts::VERSION;
|
||||
g_config_version = Texts::VERSION;
|
||||
|
||||
std::ostringstream out;
|
||||
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
|
||||
out << "version: \"" << config_version << "\"\n";
|
||||
out << "locale: \"" << locale << "\"\n\n";
|
||||
out << "version: \"" << g_config_version << "\"\n";
|
||||
out << "locale: \"" << g_locale << "\"\n\n";
|
||||
|
||||
out << "window:\n";
|
||||
out << " zoom: " << window.zoom << "\n";
|
||||
out << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n\n";
|
||||
out << " zoom: " << g_window.zoom << "\n";
|
||||
out << " fullscreen: " << (g_window.fullscreen ? "true" : "false") << "\n\n";
|
||||
|
||||
out << "video:\n";
|
||||
out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
||||
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, video.scaling_mode) << "\"\n";
|
||||
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, video.texture_filter) << "\"\n";
|
||||
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, video.render_info_position) << "\"\n";
|
||||
out << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n";
|
||||
out << " vsync: " << (g_video.vsync ? "true" : "false") << "\n";
|
||||
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, g_video.scaling_mode) << "\"\n";
|
||||
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, g_video.texture_filter) << "\"\n";
|
||||
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, g_video.render_info_position) << "\"\n";
|
||||
out << " render_info_show_time: " << (g_video.render_info_show_time ? "true" : "false") << "\n";
|
||||
out << " gpu:\n";
|
||||
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
|
||||
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
|
||||
out << " acceleration: " << (g_video.gpu.acceleration ? "true" : "false") << "\n";
|
||||
out << " preferred_driver: \"" << g_video.gpu.preferred_driver << "\"\n";
|
||||
out << " shader:\n";
|
||||
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
|
||||
out << " current_shader: \"" << video.shader.current_shader << "\"\n";
|
||||
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n";
|
||||
out << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset << "\"\n\n";
|
||||
out << " enabled: " << (g_video.shader.enabled ? "true" : "false") << "\n";
|
||||
out << " current_shader: \"" << g_video.shader.current_shader << "\"\n";
|
||||
out << " current_postfx_preset: \"" << g_video.shader.current_postfx_preset << "\"\n";
|
||||
out << " current_crtpi_preset: \"" << g_video.shader.current_crtpi_preset << "\"\n\n";
|
||||
|
||||
out << "audio:\n";
|
||||
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << audio.volume << "\n";
|
||||
out << " enabled: " << (g_audio.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << g_audio.volume << "\n";
|
||||
out << " music:\n";
|
||||
out << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << audio.music.volume << "\n";
|
||||
out << " enabled: " << (g_audio.music.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << g_audio.music.volume << "\n";
|
||||
out << " sound:\n";
|
||||
out << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << audio.sound.volume << "\n";
|
||||
out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << g_audio.sound.volume << "\n";
|
||||
|
||||
return writeFile(config_file_path, out.str());
|
||||
return writeFile(g_config_file_path, out.str());
|
||||
}
|
||||
|
||||
auto buildScreenConfig() -> Screen::Config {
|
||||
return {
|
||||
.zoom = window.zoom,
|
||||
.fullscreen = window.fullscreen,
|
||||
.vsync = video.vsync,
|
||||
.scaling_mode = video.scaling_mode,
|
||||
.texture_filter = video.texture_filter,
|
||||
.gpu_acceleration = video.gpu.acceleration,
|
||||
.preferred_driver = video.gpu.preferred_driver,
|
||||
.zoom = g_window.zoom,
|
||||
.fullscreen = g_window.fullscreen,
|
||||
.vsync = g_video.vsync,
|
||||
.scaling_mode = g_video.scaling_mode,
|
||||
.texture_filter = g_video.texture_filter,
|
||||
.gpu_acceleration = g_video.gpu.acceleration,
|
||||
.preferred_driver = g_video.gpu.preferred_driver,
|
||||
};
|
||||
}
|
||||
|
||||
auto buildAudioConfig() -> ::Audio::Config {
|
||||
return {
|
||||
.enabled = audio.enabled,
|
||||
.volume = audio.volume,
|
||||
.music_enabled = audio.music.enabled,
|
||||
.music_volume = audio.music.volume,
|
||||
.sound_enabled = audio.sound.enabled,
|
||||
.sound_volume = audio.sound.volume,
|
||||
.enabled = g_audio.enabled,
|
||||
.volume = g_audio.volume,
|
||||
.music_enabled = g_audio.music.enabled,
|
||||
.music_volume = g_audio.music.volume,
|
||||
.sound_enabled = g_audio.sound.enabled,
|
||||
.sound_volume = g_audio.sound.volume,
|
||||
};
|
||||
}
|
||||
|
||||
void setPostFXFile(const std::string& path) {
|
||||
postfx_file_path = path;
|
||||
g_postfx_file_path = path;
|
||||
}
|
||||
|
||||
auto loadPostFXFromFile() -> bool {
|
||||
return loadPresetsFromFile<PostFXPreset>(
|
||||
postfx_file_path,
|
||||
postfx_presets,
|
||||
g_postfx_file_path,
|
||||
g_postfx_presets,
|
||||
&defaultPostFXPresets,
|
||||
&parsePostFXPreset,
|
||||
&savePostFXToFile,
|
||||
@@ -548,21 +614,21 @@ namespace Options {
|
||||
|
||||
auto savePostFXToFile() -> bool {
|
||||
return savePresetsToFile<PostFXPreset>(
|
||||
postfx_file_path,
|
||||
postfx_presets,
|
||||
g_postfx_file_path,
|
||||
g_postfx_presets,
|
||||
&writePostFXPreset,
|
||||
"Presets PostFX per a AEE 2026. Editable a mà.",
|
||||
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
|
||||
}
|
||||
|
||||
void setCrtPiFile(const std::string& path) {
|
||||
crtpi_file_path = path;
|
||||
g_crtpi_file_path = path;
|
||||
}
|
||||
|
||||
auto loadCrtPiFromFile() -> bool {
|
||||
return loadPresetsFromFile<CrtPiPreset>(
|
||||
crtpi_file_path,
|
||||
crtpi_presets,
|
||||
g_crtpi_file_path,
|
||||
g_crtpi_presets,
|
||||
&defaultCrtPiPresets,
|
||||
&parseCrtPiPreset,
|
||||
&saveCrtPiToFile,
|
||||
@@ -571,29 +637,29 @@ namespace Options {
|
||||
|
||||
auto saveCrtPiToFile() -> bool {
|
||||
return savePresetsToFile<CrtPiPreset>(
|
||||
crtpi_file_path,
|
||||
crtpi_presets,
|
||||
g_crtpi_file_path,
|
||||
g_crtpi_presets,
|
||||
&writeCrtPiPreset,
|
||||
"Presets CRT-Pi per a AEE 2026. Editable a mà.",
|
||||
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
|
||||
}
|
||||
|
||||
auto buildActiveShader() -> Rendering::ShaderType {
|
||||
if (video.shader.current_shader == "crtpi") {
|
||||
if (g_video.shader.current_shader == "crtpi") {
|
||||
return Rendering::ShaderType::CRTPI;
|
||||
}
|
||||
return Rendering::ShaderType::POSTFX;
|
||||
}
|
||||
|
||||
auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame {
|
||||
const std::string& active_preset = (video.shader.current_shader == "crtpi")
|
||||
? video.shader.current_crtpi_preset
|
||||
: video.shader.current_postfx_preset;
|
||||
const std::string& active_preset = (g_video.shader.current_shader == "crtpi")
|
||||
? g_video.shader.current_crtpi_preset
|
||||
: g_video.shader.current_postfx_preset;
|
||||
return {
|
||||
.target_position = video.render_info_position,
|
||||
.show_time = video.render_info_show_time,
|
||||
.shader_enabled = video.shader.enabled,
|
||||
.shader_name = video.shader.current_shader,
|
||||
.target_position = g_video.render_info_position,
|
||||
.show_time = g_video.render_info_show_time,
|
||||
.shader_enabled = g_video.shader.enabled,
|
||||
.shader_name = g_video.shader.current_shader,
|
||||
.shader_preset = active_preset,
|
||||
.text_color = text_color,
|
||||
.shadow_color = shadow_color,
|
||||
@@ -603,12 +669,12 @@ namespace Options {
|
||||
auto buildCrtPiParams() -> Rendering::CrtPiParams {
|
||||
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
|
||||
// PostFX passthrough, però sent nets per si algú consulta aquests params).
|
||||
if (!video.shader.enabled) { return {}; }
|
||||
if (!g_video.shader.enabled) { return {}; }
|
||||
|
||||
const auto IT = std::ranges::find_if(crtpi_presets,
|
||||
[](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; });
|
||||
if (IT == crtpi_presets.end()) {
|
||||
std::cout << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
||||
const auto IT = std::ranges::find_if(g_crtpi_presets,
|
||||
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
|
||||
if (IT == g_crtpi_presets.end()) {
|
||||
std::cout << "Options: preset CrtPi \"" << g_video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
@@ -631,12 +697,12 @@ namespace Options {
|
||||
|
||||
auto buildPostFXParams() -> Rendering::PostFXParams {
|
||||
// Si el shader està deshabilitat, retornem params a zero → passthrough.
|
||||
if (!video.shader.enabled) { return {}; }
|
||||
if (!g_video.shader.enabled) { return {}; }
|
||||
|
||||
const auto IT = std::ranges::find_if(postfx_presets,
|
||||
[](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; });
|
||||
if (IT == postfx_presets.end()) {
|
||||
std::cout << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
||||
const auto IT = std::ranges::find_if(g_postfx_presets,
|
||||
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
|
||||
if (IT == g_postfx_presets.end()) {
|
||||
std::cout << "Options: preset PostFX \"" << g_video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
|
||||
+32
-13
@@ -13,6 +13,15 @@
|
||||
// del config.yaml. Els subsistemes (Screen, Audio, ...) reben structs tipats
|
||||
// a través dels build*Config(), de manera que no queden acoblats a aquest
|
||||
// namespace ni al format concret del fitxer.
|
||||
//
|
||||
// L'estat mutable viu en un anonymous namespace dins d'`options.cpp`: els
|
||||
// callers el llegeixen via getters const-ref (`window()`, `video()`, ...) i
|
||||
// el muten via setters específics que persisteixen a config.yaml. Els
|
||||
// setters NO notifiquen els subsistemes (Screen/Audio): el caller (típic.
|
||||
// `core/input/global_inputs.cpp`) crida després el setter del subsistema.
|
||||
// Aquesta asimetria preserva la direcció `Director → Screen/Audio` i evita
|
||||
// que Options aprenga del layout dels subsistemes (decisió WONTFIX d'aquest
|
||||
// refactor: veure docs/audit-game.md → COUP-03).
|
||||
namespace Options {
|
||||
|
||||
struct Window {
|
||||
@@ -95,20 +104,30 @@ namespace Options {
|
||||
Sound sound{};
|
||||
};
|
||||
|
||||
// --- Estat global (inline vars per a que una sola definició viva a tot
|
||||
// el programa sense necessitar un .cpp extra per a cada una) ---
|
||||
inline std::string config_version{};
|
||||
inline std::string config_file_path{};
|
||||
inline std::string postfx_file_path{};
|
||||
inline std::string crtpi_file_path{};
|
||||
inline std::string locale{Defaults::I18n::LOCALE};
|
||||
inline Window window{};
|
||||
inline Video video{};
|
||||
inline Audio audio{};
|
||||
inline std::vector<PostFXPreset> postfx_presets{};
|
||||
inline std::vector<CrtPiPreset> crtpi_presets{};
|
||||
// --- Getters de l'estat (read-only) ---
|
||||
[[nodiscard]] auto window() -> const Window&;
|
||||
[[nodiscard]] auto video() -> const Video&;
|
||||
[[nodiscard]] auto audio() -> const Audio&;
|
||||
[[nodiscard]] auto locale() -> const std::string&;
|
||||
[[nodiscard]] auto configVersion() -> const std::string&;
|
||||
[[nodiscard]] auto postfxPresets() -> const std::vector<PostFXPreset>&;
|
||||
[[nodiscard]] auto crtpiPresets() -> const std::vector<CrtPiPreset>&;
|
||||
|
||||
// --- API pública ---
|
||||
// --- Setters: muten + persisteixen a config.yaml ---
|
||||
// No notifiquen els subsistemes; el caller ha de propagar el canvi al
|
||||
// subsistema corresponent (p.ex. Screen::setWindowZoom) per separat.
|
||||
void setWindowZoom(int value);
|
||||
void setWindowFullscreen(bool value);
|
||||
void setVideoVSync(bool value);
|
||||
void setVideoScalingMode(Defaults::ScalingMode value);
|
||||
void setVideoRenderInfoPosition(Defaults::RenderInfoPosition value);
|
||||
void setVideoRenderInfoShowTime(bool value);
|
||||
void setShaderEnabled(bool value);
|
||||
void setActiveShaderName(const std::string& value); // "postfx" | "crtpi"
|
||||
void setPostFXPreset(const std::string& value);
|
||||
void setCrtPiPreset(const std::string& value);
|
||||
|
||||
// --- API pública: càrrega/desament de fitxers ---
|
||||
void setConfigFile(const std::string& path);
|
||||
auto loadFromFile() -> bool;
|
||||
auto saveToFile() -> bool;
|
||||
|
||||
Reference in new issue
Block a user