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:
JailDesignerandClaude Opus 4.7 committed 2026-04-22 08:37:59 +02:00
1 parent 32dcf264c4
commit a3b03b87be
5 files changed
+256 -160

No files matched your search

+157 -91
View File
@@ -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
View File
@@ -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;