#include "game/options.hpp" #include #include #include #include #include #include #include #include #include "external/fkyaml_node.hpp" #include "utils/defines.hpp" 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 g_postfx_presets{}; std::vector 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. constexpr std::array SCALING_MODE_NAMES = { std::pair{Defaults::ScalingMode::LETTERBOX, std::string_view{"letterbox"}}, std::pair{Defaults::ScalingMode::DISABLED, std::string_view{"disabled"}}, std::pair{Defaults::ScalingMode::STRETCH, std::string_view{"stretch"}}, std::pair{Defaults::ScalingMode::OVERSCAN, std::string_view{"overscan"}}, std::pair{Defaults::ScalingMode::INTEGER, std::string_view{"integer"}}, }; constexpr std::array TEXTURE_FILTER_NAMES = { std::pair{Defaults::TextureFilter::NEAREST, std::string_view{"nearest"}}, std::pair{Defaults::TextureFilter::LINEAR, std::string_view{"linear"}}, }; constexpr std::array RENDER_INFO_POSITION_NAMES = { std::pair{Defaults::RenderInfoPosition::OFF, std::string_view{"off"}}, std::pair{Defaults::RenderInfoPosition::TOP, std::string_view{"top"}}, std::pair{Defaults::RenderInfoPosition::BOTTOM, std::string_view{"bottom"}}, }; // Helpers generics: el fallback és la primera entrada de la taula. template auto enumToString(const std::array, N>& table, Enum value) -> const char* { for (const auto& [e, name] : table) { if (e == value) { return name.data(); } } return table.front().second.data(); } template auto enumFromString(const std::array, N>& table, const std::string& text) -> Enum { for (const auto& [e, name] : table) { if (name == text) { return e; } } return table.front().first; } auto readFileOrEmpty(const std::string& path) -> std::string { std::ifstream file(path); if (!file) { return {}; } std::stringstream buffer; buffer << file.rdbuf(); return buffer.str(); } auto writeFile(const std::string& path, const std::string& content) -> bool { std::ofstream file(path, std::ios::binary); if (!file) { return false; } file.write(content.data(), static_cast(content.size())); return file.good(); } // Helper trivial per a llegir un camp optional del YAML: si existeix, // sobreescriu `dst`; si no, el deixa com estava (valor per defecte). // Centralitza el patró `if (node.contains("x")) { x = node["x"].get_value(); }`. template void readField(const fkyaml::node& node, const char* key, T& dst) { if (node.contains(key)) { dst = node[key].get_value(); } } // Còpia `` a `.bak` per protegir ediciones manuals de // l'usuari quan estem a punt de reescriure el fitxer amb defaults per // error de parseig o canvi de versió. Si el fitxer no existeix (primera // execució) o no es pot obrir, no fa res: no hi ha res a salvar. void backupFile(const std::string& path) { if (path.empty()) { return; } std::ifstream src(path, std::ios::binary); if (!src) { return; } std::ofstream dst(path + ".bak", std::ios::binary); if (!dst) { return; } dst << src.rdbuf(); std::cout << "Options: còpia de seguretat a " << path << ".bak\n"; } void parseWindow(const fkyaml::node& yaml) { if (!yaml.contains("window")) { return; } const auto& node = yaml["window"]; 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", 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", 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", g_video.vsync); if (node.contains("scaling_mode")) { g_video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value()); } if (node.contains("texture_filter")) { g_video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value()); } if (node.contains("render_info_position")) { g_video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value()); } readField(node, "render_info_show_time", g_video.render_info_show_time); parseGpu(node); parseShader(node); } void parseMusic(const fkyaml::node& audio_node) { if (!audio_node.contains("music")) { return; } const auto& node = audio_node["music"]; 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", 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", g_audio.enabled); readField(node, "volume", g_audio.volume); parseMusic(node); parseSound(node); } // Presets per defecte de PostFX, portats directament de projecte_2026. auto defaultPostFXPresets() -> std::vector { return { {.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma = 0.15F, .mask = 0.6F, .gamma = 0.8F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F}, {.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma = 0.2F, .mask = 0.4F, .gamma = 0.5F, .curvature = 0.0F, .bleeding = 0.6F, .flicker = 0.0F}, {.name = "Curved", .vignette = 0.5F, .scanlines = 0.6F, .chroma = 0.1F, .mask = 0.5F, .gamma = 0.7F, .curvature = 0.8F, .bleeding = 0.0F, .flicker = 0.0F}, {.name = "Scanlines", .vignette = 0.0F, .scanlines = 0.8F, .chroma = 0.0F, .mask = 0.0F, .gamma = 0.0F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F}, {.name = "Subtle", .vignette = 0.3F, .scanlines = 0.4F, .chroma = 0.05F, .mask = 0.0F, .gamma = 0.3F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F}, {.name = "CRT Live", .vignette = 0.5F, .scanlines = 0.6F, .chroma = 0.3F, .mask = 0.3F, .gamma = 0.4F, .curvature = 0.3F, .bleeding = 0.4F, .flicker = 0.8F}, }; } void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) { readField(node, "name", preset.name); readField(node, "vignette", preset.vignette); readField(node, "scanlines", preset.scanlines); readField(node, "chroma", preset.chroma); readField(node, "mask", preset.mask); readField(node, "gamma", preset.gamma); readField(node, "curvature", preset.curvature); readField(node, "bleeding", preset.bleeding); readField(node, "flicker", preset.flicker); readField(node, "scan_dark_ratio", preset.scan_dark_ratio); readField(node, "scan_dark_floor", preset.scan_dark_floor); readField(node, "scan_edge_soft", preset.scan_edge_soft); } void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) { out << " - name: \"" << preset.name << "\"\n"; out << " vignette: " << preset.vignette << "\n"; out << " scanlines: " << preset.scanlines << "\n"; out << " chroma: " << preset.chroma << "\n"; out << " mask: " << preset.mask << "\n"; out << " gamma: " << preset.gamma << "\n"; out << " curvature: " << preset.curvature << "\n"; out << " bleeding: " << preset.bleeding << "\n"; out << " flicker: " << preset.flicker << "\n"; out << " scan_dark_ratio: " << preset.scan_dark_ratio << "\n"; out << " scan_dark_floor: " << preset.scan_dark_floor << "\n"; out << " scan_edge_soft: " << preset.scan_edge_soft << "\n"; } // Presets per defecte del shader CRT-Pi, portats de projecte_2026. auto defaultCrtPiPresets() -> std::vector { return { {.name = "Default", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = false, .enable_sharper = false}, {.name = "Curved", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = true, .enable_sharper = false}, {.name = "Sharp", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = true, .enable_curvature = false, .enable_sharper = true}, {.name = "Minimal", .scanline_weight = 8.0F, .scanline_gap_brightness = 0.05F, .bloom_factor = 2.0F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 1.00F, .curvature_x = 0.0F, .curvature_y = 0.0F, .mask_type = 0, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = false, .enable_curvature = false, .enable_sharper = false}, }; } void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) { readField(node, "name", preset.name); readField(node, "scanline_weight", preset.scanline_weight); readField(node, "scanline_gap_brightness", preset.scanline_gap_brightness); readField(node, "bloom_factor", preset.bloom_factor); readField(node, "input_gamma", preset.input_gamma); readField(node, "output_gamma", preset.output_gamma); readField(node, "mask_brightness", preset.mask_brightness); readField(node, "curvature_x", preset.curvature_x); readField(node, "curvature_y", preset.curvature_y); readField(node, "mask_type", preset.mask_type); readField(node, "enable_scanlines", preset.enable_scanlines); readField(node, "enable_multisample", preset.enable_multisample); readField(node, "enable_gamma", preset.enable_gamma); readField(node, "enable_curvature", preset.enable_curvature); readField(node, "enable_sharper", preset.enable_sharper); } void writeCrtPiPreset(std::ostringstream& out, const Options::CrtPiPreset& preset) { out << " - name: \"" << preset.name << "\"\n"; out << " scanline_weight: " << preset.scanline_weight << "\n"; out << " scanline_gap_brightness: " << preset.scanline_gap_brightness << "\n"; out << " bloom_factor: " << preset.bloom_factor << "\n"; out << " input_gamma: " << preset.input_gamma << "\n"; out << " output_gamma: " << preset.output_gamma << "\n"; out << " mask_brightness: " << preset.mask_brightness << "\n"; out << " curvature_x: " << preset.curvature_x << "\n"; out << " curvature_y: " << preset.curvature_y << "\n"; out << " mask_type: " << preset.mask_type << "\n"; out << " enable_scanlines: " << (preset.enable_scanlines ? "true" : "false") << "\n"; out << " enable_multisample: " << (preset.enable_multisample ? "true" : "false") << "\n"; out << " enable_gamma: " << (preset.enable_gamma ? "true" : "false") << "\n"; out << " enable_curvature: " << (preset.enable_curvature ? "true" : "false") << "\n"; out << " enable_sharper: " << (preset.enable_sharper ? "true" : "false") << "\n"; } // Pipeline genèric de càrrega de presets: si el fitxer no existeix o és // invàlid (no deserialitza, manca `presets:`, no és seqüència, queda // buit), es restauren els defaults via `defaults_fn` i es reescriu via // `save_fn`. Unifica PostFX i CrtPi — abans eren dues còpies del mateix // codi que només diferien en el tipus concret de preset. template auto loadPresetsFromFile( const std::string& path, std::vector& presets, std::vector (*defaults_fn)(), void (*parse_fn)(const fkyaml::node&, T&), bool (*save_fn)(), const char* kind_label) -> bool { if (path.empty()) { return false; } const std::string CONTENT = readFileOrEmpty(path); if (CONTENT.empty()) { // Primera execució (no existia o era buit): no cal backup. presets = defaults_fn(); return save_fn(); } try { auto yaml = fkyaml::node::deserialize(CONTENT); if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { std::cout << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n"; backupFile(path); presets = defaults_fn(); return save_fn(); } presets.clear(); for (const auto& preset_node : yaml["presets"]) { T preset; parse_fn(preset_node, preset); presets.push_back(preset); } if (presets.empty()) { std::cout << "Options: " << kind_label << " sense presets; reescrivint amb defaults.\n"; backupFile(path); presets = defaults_fn(); return save_fn(); } return true; } catch (const std::exception& e) { std::cout << "Options: error parsejant " << kind_label << ": " << e.what() << '\n'; std::cout << "Options: reescrivint el fitxer amb presets per defecte.\n"; backupFile(path); presets = defaults_fn(); return save_fn(); } } // Pipeline genèric d'escriptura: capçalera de comentari + `presets:` + // una entrada per preset via `write_fn`. `config_key_note` és la segona // línia de comentari que recorda amb quin camp de config.yaml han de // coincidir els noms. template auto savePresetsToFile( const std::string& path, const std::vector& presets, void (*write_fn)(std::ostringstream&, const T&), const char* header_comment, const char* config_key_note) -> bool { if (path.empty()) { return false; } std::ostringstream out; out << "# " << header_comment << "\n"; out << "# " << config_key_note << "\n"; out << "presets:\n"; for (const auto& preset : presets) { write_fn(out, preset); } return writeFile(path, out.str()); } } // 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& { return g_postfx_presets; } auto crtpiPresets() -> const std::vector& { 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 setAudioEnabled(bool value) { g_audio.enabled = value; saveToFile(); } void setAudioVolume(float value) { g_audio.volume = std::clamp(value, 0.0F, 1.0F); saveToFile(); } void setAudioMusicEnabled(bool value) { g_audio.music.enabled = value; saveToFile(); } void setAudioMusicVolume(float value) { g_audio.music.volume = std::clamp(value, 0.0F, 1.0F); saveToFile(); } void setAudioSoundEnabled(bool value) { g_audio.sound.enabled = value; saveToFile(); } void setAudioSoundVolume(float value) { g_audio.sound.volume = std::clamp(value, 0.0F, 1.0F); saveToFile(); } void setConfigFile(const std::string& path) { g_config_file_path = path; } auto loadFromFile() -> bool { if (g_config_file_path.empty()) { return false; } const std::string CONTENT = readFileOrEmpty(g_config_file_path); if (CONTENT.empty()) { // Primera execució: creem el fitxer amb els valors per defecte. return saveToFile(); } try { auto yaml = fkyaml::node::deserialize(CONTENT); std::string file_version; if (yaml.contains("version")) { file_version = yaml["version"].get_value(); } // Sense migració per ara: si la versió no coincideix, reescrivim // amb els defaults de la versió nova. El fitxer original queda // a `.bak` per si l'usuari volia preservar tuning manual. if (file_version != Texts::VERSION) { std::cout << "Options: config.yaml versió \"" << file_version << "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n"; backupFile(g_config_file_path); return saveToFile(); } g_config_version = file_version; if (yaml.contains("locale")) { g_locale = yaml["locale"].get_value(); } parseWindow(yaml); parseVideo(yaml); parseAudio(yaml); return true; } 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(g_config_file_path); return saveToFile(); } } auto saveToFile() -> bool { if (g_config_file_path.empty()) { return false; } g_config_version = Texts::VERSION; std::ostringstream out; out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n"; out << "version: \"" << g_config_version << "\"\n"; out << "locale: \"" << g_locale << "\"\n\n"; out << "window:\n"; out << " zoom: " << g_window.zoom << "\n"; out << " fullscreen: " << (g_window.fullscreen ? "true" : "false") << "\n\n"; out << "video:\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: " << (g_video.gpu.acceleration ? "true" : "false") << "\n"; out << " preferred_driver: \"" << g_video.gpu.preferred_driver << "\"\n"; out << " shader:\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: " << (g_audio.enabled ? "true" : "false") << "\n"; out << " volume: " << g_audio.volume << "\n"; out << " music:\n"; out << " enabled: " << (g_audio.music.enabled ? "true" : "false") << "\n"; out << " volume: " << g_audio.music.volume << "\n"; out << " sound:\n"; out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n"; out << " volume: " << g_audio.sound.volume << "\n"; return writeFile(g_config_file_path, out.str()); } auto buildScreenConfig() -> Screen::Config { return { .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 = 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) { g_postfx_file_path = path; } auto loadPostFXFromFile() -> bool { return loadPresetsFromFile( g_postfx_file_path, g_postfx_presets, &defaultPostFXPresets, &parsePostFXPreset, &savePostFXToFile, "postfx.yaml"); } auto savePostFXToFile() -> bool { return savePresetsToFile( 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) { g_crtpi_file_path = path; } auto loadCrtPiFromFile() -> bool { return loadPresetsFromFile( g_crtpi_file_path, g_crtpi_presets, &defaultCrtPiPresets, &parseCrtPiPreset, &saveCrtPiToFile, "crtpi.yaml"); } auto saveCrtPiToFile() -> bool { return savePresetsToFile( 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 (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 = (g_video.shader.current_shader == "crtpi") ? g_video.shader.current_crtpi_preset : g_video.shader.current_postfx_preset; return { .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, }; } 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 (!g_video.shader.enabled) { return {}; } 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 { .scanline_weight = IT->scanline_weight, .scanline_gap_brightness = IT->scanline_gap_brightness, .bloom_factor = IT->bloom_factor, .input_gamma = IT->input_gamma, .output_gamma = IT->output_gamma, .mask_brightness = IT->mask_brightness, .curvature_x = IT->curvature_x, .curvature_y = IT->curvature_y, .mask_type = IT->mask_type, .enable_scanlines = IT->enable_scanlines, .enable_multisample = IT->enable_multisample, .enable_gamma = IT->enable_gamma, .enable_curvature = IT->enable_curvature, .enable_sharper = IT->enable_sharper, }; } auto buildPostFXParams() -> Rendering::PostFXParams { // Si el shader està deshabilitat, retornem params a zero → passthrough. if (!g_video.shader.enabled) { return {}; } 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 { .vignette = IT->vignette, .scanlines = IT->scanlines, .chroma = IT->chroma, .mask = IT->mask, .gamma = IT->gamma, .curvature = IT->curvature, .bleeding = IT->bleeding, .flicker = IT->flicker, .scan_dark_ratio = IT->scan_dark_ratio, .scan_dark_floor = IT->scan_dark_floor, .scan_edge_soft = IT->scan_edge_soft, }; } } // namespace Options