#include "game/options.hpp" #include #include #include #include #include #include "external/fkyaml_node.hpp" #include "utils/defines.hpp" namespace { auto scalingModeToString(Defaults::ScalingMode mode) -> const char* { switch (mode) { case Defaults::ScalingMode::DISABLED: return "disabled"; case Defaults::ScalingMode::STRETCH: return "stretch"; case Defaults::ScalingMode::LETTERBOX: return "letterbox"; case Defaults::ScalingMode::OVERSCAN: return "overscan"; case Defaults::ScalingMode::INTEGER: return "integer"; } return "letterbox"; } auto scalingModeFromString(const std::string& text) -> Defaults::ScalingMode { if (text == "disabled") { return Defaults::ScalingMode::DISABLED; } if (text == "stretch") { return Defaults::ScalingMode::STRETCH; } if (text == "overscan") { return Defaults::ScalingMode::OVERSCAN; } if (text == "integer") { return Defaults::ScalingMode::INTEGER; } return Defaults::ScalingMode::LETTERBOX; } auto filterToString(Defaults::TextureFilter filter) -> const char* { return filter == Defaults::TextureFilter::LINEAR ? "linear" : "nearest"; } auto filterFromString(const std::string& text) -> Defaults::TextureFilter { return text == "linear" ? Defaults::TextureFilter::LINEAR : Defaults::TextureFilter::NEAREST; } 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(); } void parseWindow(const fkyaml::node& yaml) { if (!yaml.contains("window")) { return; } const auto& node = yaml["window"]; if (node.contains("zoom")) { Options::window.zoom = node["zoom"].get_value(); } if (node.contains("fullscreen")) { Options::window.fullscreen = node["fullscreen"].get_value(); } } void parseGpu(const fkyaml::node& video_node) { if (!video_node.contains("gpu")) { return; } const auto& node = video_node["gpu"]; if (node.contains("acceleration")) { Options::video.gpu.acceleration = node["acceleration"].get_value(); } if (node.contains("preferred_driver")) { Options::video.gpu.preferred_driver = node["preferred_driver"].get_value(); } } void parseShader(const fkyaml::node& video_node) { if (!video_node.contains("shader")) { return; } const auto& node = video_node["shader"]; if (node.contains("enabled")) { Options::video.shader.enabled = node["enabled"].get_value(); } if (node.contains("current_shader")) { Options::video.shader.current_shader = node["current_shader"].get_value(); } if (node.contains("current_postfx_preset")) { Options::video.shader.current_postfx_preset = node["current_postfx_preset"].get_value(); } if (node.contains("current_crtpi_preset")) { Options::video.shader.current_crtpi_preset = node["current_crtpi_preset"].get_value(); } } void parseVideo(const fkyaml::node& yaml) { if (!yaml.contains("video")) { return; } const auto& node = yaml["video"]; if (node.contains("vsync")) { Options::video.vsync = node["vsync"].get_value(); } if (node.contains("scaling_mode")) { Options::video.scaling_mode = scalingModeFromString(node["scaling_mode"].get_value()); } if (node.contains("texture_filter")) { Options::video.texture_filter = filterFromString(node["texture_filter"].get_value()); } parseGpu(node); parseShader(node); } void parseMusic(const fkyaml::node& audio_node) { if (!audio_node.contains("music")) { return; } const auto& node = audio_node["music"]; if (node.contains("enabled")) { Options::audio.music.enabled = node["enabled"].get_value(); } if (node.contains("volume")) { Options::audio.music.volume = node["volume"].get_value(); } } void parseSound(const fkyaml::node& audio_node) { if (!audio_node.contains("sound")) { return; } const auto& node = audio_node["sound"]; if (node.contains("enabled")) { Options::audio.sound.enabled = node["enabled"].get_value(); } if (node.contains("volume")) { Options::audio.sound.volume = node["volume"].get_value(); } } void parseAudio(const fkyaml::node& yaml) { if (!yaml.contains("audio")) { return; } const auto& node = yaml["audio"]; if (node.contains("enabled")) { Options::audio.enabled = node["enabled"].get_value(); } if (node.contains("volume")) { Options::audio.volume = node["volume"].get_value(); } parseMusic(node); parseSound(node); } // Presets per defecte de PostFX, usats quan el fitxer postfx.yaml no existeix. auto defaultPostFXPresets() -> std::vector { return { {.name = "off"}, // passthrough (tot a 0) {.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.25F, .mask = 0.3F, .gamma = 0.6F, .curvature = 0.0F, .bleeding = 0.4F, .flicker = 0.1F}, }; } void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) { if (node.contains("name")) { preset.name = node["name"].get_value(); } if (node.contains("vignette")) { preset.vignette = node["vignette"].get_value(); } if (node.contains("scanlines")) { preset.scanlines = node["scanlines"].get_value(); } if (node.contains("chroma")) { preset.chroma = node["chroma"].get_value(); } if (node.contains("mask")) { preset.mask = node["mask"].get_value(); } if (node.contains("gamma")) { preset.gamma = node["gamma"].get_value(); } if (node.contains("curvature")) { preset.curvature = node["curvature"].get_value(); } if (node.contains("bleeding")) { preset.bleeding = node["bleeding"].get_value(); } if (node.contains("flicker")) { preset.flicker = node["flicker"].get_value(); } } 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"; } // Presets per defecte de CrtPi (tuning bàsic del shader original CRT-Pi). auto defaultCrtPiPresets() -> std::vector { return { {.name = "default"}, {.name = "sharper", .scanline_weight = 8.0F, .scanline_gap_brightness = 0.10F, .bloom_factor = 3.0F, .enable_sharper = true}, {.name = "curved", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .curvature_x = 0.05F, .curvature_y = 0.10F, .enable_curvature = true}, }; } void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) { if (node.contains("name")) { preset.name = node["name"].get_value(); } if (node.contains("scanline_weight")) { preset.scanline_weight = node["scanline_weight"].get_value(); } if (node.contains("scanline_gap_brightness")) { preset.scanline_gap_brightness = node["scanline_gap_brightness"].get_value(); } if (node.contains("bloom_factor")) { preset.bloom_factor = node["bloom_factor"].get_value(); } if (node.contains("input_gamma")) { preset.input_gamma = node["input_gamma"].get_value(); } if (node.contains("output_gamma")) { preset.output_gamma = node["output_gamma"].get_value(); } if (node.contains("mask_brightness")) { preset.mask_brightness = node["mask_brightness"].get_value(); } if (node.contains("curvature_x")) { preset.curvature_x = node["curvature_x"].get_value(); } if (node.contains("curvature_y")) { preset.curvature_y = node["curvature_y"].get_value(); } if (node.contains("mask_type")) { preset.mask_type = node["mask_type"].get_value(); } if (node.contains("enable_scanlines")) { preset.enable_scanlines = node["enable_scanlines"].get_value(); } if (node.contains("enable_multisample")) { preset.enable_multisample = node["enable_multisample"].get_value(); } if (node.contains("enable_gamma")) { preset.enable_gamma = node["enable_gamma"].get_value(); } if (node.contains("enable_curvature")) { preset.enable_curvature = node["enable_curvature"].get_value(); } if (node.contains("enable_sharper")) { preset.enable_sharper = node["enable_sharper"].get_value(); } } 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"; } } // namespace namespace Options { void setConfigFile(const std::string& path) { config_file_path = path; } auto loadFromFile() -> bool { if (config_file_path.empty()) { return false; } const std::string CONTENT = readFileOrEmpty(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. if (file_version != Texts::VERSION) { return saveToFile(); } config_version = file_version; parseWindow(yaml); parseVideo(yaml); parseAudio(yaml); return true; } catch (const std::exception& e) { std::cerr << "Options: error parsejant config.yaml: " << e.what() << '\n'; std::cerr << "Options: reescrivint el fitxer amb defaults.\n"; return saveToFile(); } } auto saveToFile() -> bool { if (config_file_path.empty()) { return false; } config_version = Texts::VERSION; std::ostringstream out; out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n"; out << "version: \"" << config_version << "\"\n\n"; out << "window:\n"; out << " zoom: " << window.zoom << "\n"; out << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n\n"; out << "video:\n"; out << " vsync: " << (video.vsync ? "true" : "false") << "\n"; out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n"; out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n"; out << " gpu:\n"; out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n"; out << " preferred_driver: \"" << 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 << "audio:\n"; out << " enabled: " << (audio.enabled ? "true" : "false") << "\n"; out << " volume: " << audio.volume << "\n"; out << " music:\n"; out << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n"; out << " volume: " << audio.music.volume << "\n"; out << " sound:\n"; out << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n"; out << " volume: " << audio.sound.volume << "\n"; return writeFile(config_file_path, out.str()); } auto buildScreenConfig() -> Screen::Config { Screen::Config config; config.zoom = window.zoom; config.fullscreen = window.fullscreen; config.vsync = video.vsync; config.scaling_mode = video.scaling_mode; config.texture_filter = video.texture_filter; config.gpu_acceleration = video.gpu.acceleration; config.preferred_driver = video.gpu.preferred_driver; return config; } void setPostFXFile(const std::string& path) { postfx_file_path = path; } auto loadPostFXFromFile() -> bool { if (postfx_file_path.empty()) { return false; } const std::string CONTENT = readFileOrEmpty(postfx_file_path); if (CONTENT.empty()) { postfx_presets = defaultPostFXPresets(); return savePostFXToFile(); } try { auto yaml = fkyaml::node::deserialize(CONTENT); if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { postfx_presets = defaultPostFXPresets(); return savePostFXToFile(); } postfx_presets.clear(); for (const auto& preset_node : yaml["presets"]) { PostFXPreset preset; parsePostFXPreset(preset_node, preset); postfx_presets.push_back(preset); } if (postfx_presets.empty()) { postfx_presets = defaultPostFXPresets(); return savePostFXToFile(); } return true; } catch (const std::exception& e) { std::cerr << "Options: error parsejant postfx.yaml: " << e.what() << '\n'; std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n"; postfx_presets = defaultPostFXPresets(); return savePostFXToFile(); } } auto savePostFXToFile() -> bool { if (postfx_file_path.empty()) { return false; } std::ostringstream out; out << "# Presets PostFX per a AEE 2026. Editable a mà.\n"; out << "# Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.\n"; out << "presets:\n"; for (const auto& preset : postfx_presets) { writePostFXPreset(out, preset); } return writeFile(postfx_file_path, out.str()); } void setCrtPiFile(const std::string& path) { crtpi_file_path = path; } auto loadCrtPiFromFile() -> bool { if (crtpi_file_path.empty()) { return false; } const std::string CONTENT = readFileOrEmpty(crtpi_file_path); if (CONTENT.empty()) { crtpi_presets = defaultCrtPiPresets(); return saveCrtPiToFile(); } try { auto yaml = fkyaml::node::deserialize(CONTENT); if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { crtpi_presets = defaultCrtPiPresets(); return saveCrtPiToFile(); } crtpi_presets.clear(); for (const auto& preset_node : yaml["presets"]) { CrtPiPreset preset; parseCrtPiPreset(preset_node, preset); crtpi_presets.push_back(preset); } if (crtpi_presets.empty()) { crtpi_presets = defaultCrtPiPresets(); return saveCrtPiToFile(); } return true; } catch (const std::exception& e) { std::cerr << "Options: error parsejant crtpi.yaml: " << e.what() << '\n'; std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n"; crtpi_presets = defaultCrtPiPresets(); return saveCrtPiToFile(); } } auto saveCrtPiToFile() -> bool { if (crtpi_file_path.empty()) { return false; } std::ostringstream out; out << "# Presets CRT-Pi per a AEE 2026. Editable a mà.\n"; out << "# Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.\n"; out << "presets:\n"; for (const auto& preset : crtpi_presets) { writeCrtPiPreset(out, preset); } return writeFile(crtpi_file_path, out.str()); } auto buildActiveShader() -> Rendering::ShaderType { if (video.shader.current_shader == "crtpi") { return Rendering::ShaderType::CRTPI; } return Rendering::ShaderType::POSTFX; } 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 {}; } 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::cerr << "Options: preset CrtPi \"" << 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 (!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::cerr << "Options: preset PostFX \"" << 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, }; } } // namespace Options