#include "game/options.hpp" #include #include #include #include #include #include "core/input/input.h" // for Input::Device::KEYBOARD, Input::Device::GAMECONTROLLER #include "core/locale/lang.h" // for Lang::Code, Lang::MAX_LANGUAGES #include "external/fkyaml_node.hpp" // for fkyaml::node #include "utils/utils.h" // for boolToString namespace Options { // --- Variables globales --- Window window; Video video; Audio audio; Loading loading; Settings settings; Gameplay gameplay; std::vector inputs; std::vector postfx_presets; std::string postfx_file_path; int current_postfx_preset = 0; std::vector crtpi_presets; std::string crtpi_file_path; int current_crtpi_preset = 0; // Lectura tolerant d'un camp YAML: assigna a `target` el valor del camp // si existeix i el tipus encaixa. Si la clau no hi és o el tipus YAML // no és compatible amb T, conserva el valor previ de `target` (default). // Retorna true si s'ha llegit, false si s'ha conservat el default. template auto tryGet(const fkyaml::node &node, const std::string &key, T &target) -> bool { if (!node.contains(key)) { return false; } try { target = node[key].get_value(); return true; } catch (...) { // @INTENTIONAL: valor YAML amb tipus incompatible per a T. La // política del loader és conservar el default existent en // `target` enlloc d'avortar la càrrega de la resta del fitxer. return false; } } // --- Helpers locals --- namespace { void parseBoolField(const fkyaml::node &node, const std::string &key, bool &target) { tryGet(node, key, target); } void parseIntField(const fkyaml::node &node, const std::string &key, int &target) { tryGet(node, key, target); } void parseStringField(const fkyaml::node &node, const std::string &key, std::string &target) { tryGet(node, key, target); } void loadWindowFromYaml(const fkyaml::node &yaml) { if (!yaml.contains("window")) { return; } const auto &win = yaml["window"]; parseIntField(win, "zoom", window.zoom); // El bound superior s'aplica més tard a `Screen::detectMaxZoom()` // un cop sabem la resolució real del display. if (window.zoom < 1) { window.zoom = Defaults::Window::ZOOM; } } void loadVideoFromYaml(const fkyaml::node &yaml) { if (!yaml.contains("video")) { return; } const auto &vid = yaml["video"]; parseBoolField(vid, "fullscreen", video.fullscreen); parseBoolField(vid, "vsync", video.vsync); parseBoolField(vid, "integer_scale", video.integer_scale); int scale_mode_int = static_cast(video.scale_mode); if (tryGet(vid, "scale_mode", scale_mode_int)) { video.scale_mode = static_cast(scale_mode_int); } if (vid.contains("gpu")) { const auto &gpu = vid["gpu"]; parseBoolField(gpu, "acceleration", video.gpu.acceleration); parseStringField(gpu, "preferred_driver", video.gpu.preferred_driver); } if (vid.contains("shader")) { const auto &sh = vid["shader"]; parseBoolField(sh, "enabled", video.shader.enabled); std::string shader_kind; if (tryGet(sh, "current_shader", shader_kind)) { video.shader.current_shader = (shader_kind == "crtpi" || shader_kind == "CRTPI") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX; } parseStringField(sh, "current_postfx_preset", video.shader.current_postfx_preset_name); parseStringField(sh, "current_crtpi_preset", video.shader.current_crtpi_preset_name); } } void loadAudioFromYaml(const fkyaml::node &yaml) { if (!yaml.contains("audio")) { return; } const auto &aud = yaml["audio"]; parseBoolField(aud, "enabled", audio.enabled); if (tryGet(aud, "volume", audio.volume)) { audio.volume = std::clamp(audio.volume, 0.0F, 1.0F); } if (aud.contains("music")) { const auto &mus = aud["music"]; parseBoolField(mus, "enabled", audio.music.enabled); if (tryGet(mus, "volume", audio.music.volume)) { audio.music.volume = std::clamp(audio.music.volume, 0.0F, 1.0F); } } if (aud.contains("sound")) { const auto &snd = aud["sound"]; parseBoolField(snd, "enabled", audio.sound.enabled); if (tryGet(snd, "volume", audio.sound.volume)) { audio.sound.volume = std::clamp(audio.sound.volume, 0.0F, 1.0F); } } } void loadLoadingFromYaml(const fkyaml::node &yaml) { if (!yaml.contains("loading")) { return; } const auto &ld = yaml["loading"]; parseBoolField(ld, "show", loading.show); parseBoolField(ld, "show_resource_name", loading.show_resource_name); parseBoolField(ld, "wait_for_input", loading.wait_for_input); } void loadSettingsFromYaml(const fkyaml::node &yaml) { if (!yaml.contains("settings")) { return; } const auto &st = yaml["settings"]; parseIntField(st, "difficulty", settings.difficulty); int language_int = static_cast(settings.language); parseIntField(st, "language", language_int); if (language_int < 0 || language_int >= Lang::MAX_LANGUAGES) { language_int = static_cast(Lang::Code::EN_UK); } settings.language = static_cast(language_int); parseIntField(st, "player_selected", settings.player_selected); parseStringField(st, "skin", settings.skin); } void loadGameplayFromYaml(const fkyaml::node &yaml) { if (!yaml.contains("gameplay")) { return; } const auto &gp = yaml["gameplay"]; parseBoolField(gp, "pause_countdown", gameplay.pause_countdown); } void loadInputsFromYaml(const fkyaml::node &yaml) { if (!yaml.contains("input") || inputs.size() < 2) { return; } const auto &ins = yaml["input"]; size_t i = 0; for (const auto &entry : ins) { if (i >= inputs.size()) { break; } int device_type_int = static_cast(inputs[i].device_type); if (tryGet(entry, "device_type", device_type_int)) { inputs[i].device_type = static_cast(device_type_int); } ++i; } } } // namespace // --- Funciones públiques --- void setConfigFile(const std::string &file_path) { settings.config_file = file_path; } void init() { // Reinicia structs a defaults (els member-initializers ho fan sols). window = Window{}; video = Video{}; audio = Audio{}; loading = Loading{}; gameplay = Gameplay{}; // Preserva config_file si ja s'ha establert abans. const std::string PREV_CONFIG_FILE = settings.config_file; settings = Settings{}; settings.config_file = PREV_CONFIG_FILE; #ifdef __EMSCRIPTEN__ // En Emscripten la ventana la gestiona el navegador window.zoom = 4; video.fullscreen = false; video.integer_scale = true; #endif // Dispositius d'entrada per defecte inputs.clear(); InputDevice kb; kb.id = 0; kb.name = "KEYBOARD"; kb.device_type = Input::Device::KEYBOARD; inputs.push_back(kb); InputDevice gc; gc.id = 0; gc.name = "GAME CONTROLLER"; gc.device_type = Input::Device::GAMECONTROLLER; inputs.push_back(gc); } auto loadFromFile() -> bool { init(); std::ifstream file(settings.config_file); if (!file.is_open()) { // Primera execució: crea el YAML amb defaults. return saveToFile(); } const std::string CONTENT((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); try { auto yaml = fkyaml::node::deserialize(CONTENT); int file_version = 0; tryGet(yaml, "config_version", file_version); if (file_version != Settings::CURRENT_CONFIG_VERSION) { std::cout << "Config version " << file_version << " != expected " << Settings::CURRENT_CONFIG_VERSION << ". Recreating defaults.\n"; init(); return saveToFile(); } loadWindowFromYaml(yaml); loadVideoFromYaml(yaml); loadAudioFromYaml(yaml); loadLoadingFromYaml(yaml); loadSettingsFromYaml(yaml); loadGameplayFromYaml(yaml); loadInputsFromYaml(yaml); } catch (const fkyaml::exception &e) { std::cout << "Error parsing YAML config: " << e.what() << ". Using defaults.\n"; init(); return saveToFile(); } return true; } auto saveToFile() -> bool { if (settings.config_file.empty()) { return false; } std::ofstream file(settings.config_file); if (!file.is_open()) { std::cout << "Error: " << settings.config_file << " can't be opened for writing\n"; return false; } file << "# Coffee Crisis - Configuration file\n"; file << "# Auto-generated, managed by the game.\n\n"; file << "config_version: " << settings.config_version << "\n\n"; // WINDOW // `max_zoom` no es guarda — es deriva del display a cada arranc via // `Screen::detectMaxZoom()`. file << "# WINDOW\n"; file << "window:\n"; file << " zoom: " << window.zoom << "\n\n"; // VIDEO file << "# VIDEO\n"; file << "video:\n"; file << " fullscreen: " << boolToString(video.fullscreen) << "\n"; file << " vsync: " << boolToString(video.vsync) << "\n"; file << " integer_scale: " << boolToString(video.integer_scale) << "\n"; file << " scale_mode: " << static_cast(video.scale_mode) << " # " << static_cast(SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast(SDL_SCALEMODE_LINEAR) << ": linear\n"; file << " gpu:\n"; file << " acceleration: " << boolToString(video.gpu.acceleration) << "\n"; file << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n"; file << " shader:\n"; file << " enabled: " << boolToString(video.shader.enabled) << "\n"; file << " current_shader: " << (video.shader.current_shader == Rendering::ShaderType::CRTPI ? "crtpi" : "postfx") << "\n"; file << " current_postfx_preset: \"" << video.shader.current_postfx_preset_name << "\"\n"; file << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset_name << "\"\n\n"; // AUDIO file << "# AUDIO (volume range: 0.0..1.0)\n"; file << "audio:\n"; file << " enabled: " << boolToString(audio.enabled) << "\n"; file << " volume: " << audio.volume << "\n"; file << " music:\n"; file << " enabled: " << boolToString(audio.music.enabled) << "\n"; file << " volume: " << audio.music.volume << "\n"; file << " sound:\n"; file << " enabled: " << boolToString(audio.sound.enabled) << "\n"; file << " volume: " << audio.sound.volume << "\n\n"; // LOADING file << "# LOADING SCREEN\n"; file << "loading:\n"; file << " show: " << boolToString(loading.show) << "\n"; file << " show_resource_name: " << boolToString(loading.show_resource_name) << "\n"; file << " wait_for_input: " << boolToString(loading.wait_for_input) << "\n\n"; // SETTINGS file << "# SETTINGS\n"; file << "settings:\n"; file << " difficulty: " << settings.difficulty << "\n"; file << " language: " << static_cast(settings.language) << "\n"; file << " player_selected: " << settings.player_selected << "\n"; file << " skin: \"" << settings.skin << "\"\n\n"; // GAMEPLAY file << "# GAMEPLAY\n"; file << "gameplay:\n"; file << " pause_countdown: " << boolToString(gameplay.pause_countdown) << "\n\n"; // INPUT file << "# INPUT DEVICES (device_type: " << static_cast(Input::Device::KEYBOARD) << "=KEYBOARD, " << static_cast(Input::Device::GAMECONTROLLER) << "=GAMECONTROLLER)\n"; file << "input:\n"; for (size_t i = 0; i < inputs.size(); ++i) { file << " - slot: " << i << "\n"; file << " device_type: " << static_cast(inputs[i].device_type) << "\n"; } file.close(); return true; } // ======================================================================== // Presets de shaders (postfx.yaml / crtpi.yaml) // // Els defaults viuen en una única font (defaultPostFXPresets / defaultCrtPiPresets). // Generem el YAML a partir d'ells el primer cop i els usem també com a // fallback si el YAML és absent o corrupte. Si algú toca els valors, ho fa // en un sol lloc. // ======================================================================== namespace { void parseFloatField(const fkyaml::node &node, const std::string &key, float &target) { tryGet(node, key, target); } auto defaultPostFXPresets() -> const std::vector & { static const std::vector DEFAULTS = { {.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F}, {.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma_min = 0.2F, .chroma_max = 0.2F, .mask = 0.4F, .gamma = 0.5F, .curvature = 0.0F, .bleeding = 0.6F}, {.name = "CURVED", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.1F, .mask = 0.5F, .gamma = 0.7F, .curvature = 0.8F}, {.name = "SCANLINES", .vignette = 0.0F, .scanlines = 0.8F}, {.name = "SUBTLE", .vignette = 0.3F, .scanlines = 0.4F, .chroma_min = 0.05F, .chroma_max = 0.05F, .mask = 0.0F, .gamma = 0.3F}, {.name = "CRT LIVE", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.3F, .mask = 0.3F, .gamma = 0.4F, .curvature = 0.3F, .bleeding = 0.4F, .flicker = 0.8F}, }; return DEFAULTS; } auto defaultCrtPiPresets() -> const std::vector & { static const std::vector DEFAULTS = { {.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}, }; return DEFAULTS; } void writePostFXDefaults(std::ostream &out) { out << "# Coffee Crisis - PostFX Shader Presets\n\n"; out << "presets:\n"; for (const auto &p : defaultPostFXPresets()) { out << " - name: \"" << p.name << "\"\n"; out << " vignette: " << p.vignette << "\n"; out << " scanlines: " << p.scanlines << "\n"; out << " chroma_min: " << p.chroma_min << "\n"; out << " chroma_max: " << p.chroma_max << "\n"; out << " mask: " << p.mask << "\n"; out << " gamma: " << p.gamma << "\n"; out << " curvature: " << p.curvature << "\n"; out << " bleeding: " << p.bleeding << "\n"; out << " flicker: " << p.flicker << "\n"; out << " scan_dark_ratio: " << p.scan_dark_ratio << "\n"; out << " scan_dark_floor: " << p.scan_dark_floor << "\n"; out << " scan_edge_soft: " << p.scan_edge_soft << "\n"; } } void writeCrtPiDefaults(std::ostream &out) { out << "# Coffee Crisis - CrtPi Shader Presets\n\n"; out << "presets:\n"; for (const auto &p : defaultCrtPiPresets()) { out << " - name: \"" << p.name << "\"\n"; out << " scanline_weight: " << p.scanline_weight << "\n"; out << " scanline_gap_brightness: " << p.scanline_gap_brightness << "\n"; out << " bloom_factor: " << p.bloom_factor << "\n"; out << " input_gamma: " << p.input_gamma << "\n"; out << " output_gamma: " << p.output_gamma << "\n"; out << " mask_brightness: " << p.mask_brightness << "\n"; out << " curvature_x: " << p.curvature_x << "\n"; out << " curvature_y: " << p.curvature_y << "\n"; out << " mask_type: " << p.mask_type << "\n"; out << " enable_scanlines: " << boolToString(p.enable_scanlines) << "\n"; out << " enable_multisample: " << boolToString(p.enable_multisample) << "\n"; out << " enable_gamma: " << boolToString(p.enable_gamma) << "\n"; out << " enable_curvature: " << boolToString(p.enable_curvature) << "\n"; out << " enable_sharper: " << boolToString(p.enable_sharper) << "\n"; } } } // namespace void setPostFXFile(const std::string &path) { postfx_file_path = path; } auto loadPostFXFromFile() -> bool { postfx_presets.clear(); current_postfx_preset = 0; std::ifstream file(postfx_file_path); if (!file.is_open()) { // No existeix: escriu el YAML a partir dels defaults i copia'ls a memòria. std::ofstream out(postfx_file_path); if (out.is_open()) { writePostFXDefaults(out); out.close(); } postfx_presets = defaultPostFXPresets(); return true; } const std::string CONTENT((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); try { auto yaml = fkyaml::node::deserialize(CONTENT); if (yaml.contains("presets")) { for (const auto &p : yaml["presets"]) { PostFXPreset preset; tryGet(p, "name", preset.name); parseFloatField(p, "vignette", preset.vignette); parseFloatField(p, "scanlines", preset.scanlines); // Compatibilitat: si només hi ha "chroma", l'usem per a min i max. float legacy_chroma = -1.0F; if (tryGet(p, "chroma", legacy_chroma)) { preset.chroma_min = legacy_chroma; preset.chroma_max = legacy_chroma; } parseFloatField(p, "chroma_min", preset.chroma_min); parseFloatField(p, "chroma_max", preset.chroma_max); parseFloatField(p, "mask", preset.mask); parseFloatField(p, "gamma", preset.gamma); parseFloatField(p, "curvature", preset.curvature); parseFloatField(p, "bleeding", preset.bleeding); parseFloatField(p, "flicker", preset.flicker); parseFloatField(p, "scan_dark_ratio", preset.scan_dark_ratio); parseFloatField(p, "scan_dark_floor", preset.scan_dark_floor); parseFloatField(p, "scan_edge_soft", preset.scan_edge_soft); postfx_presets.push_back(preset); } } std::cout << "PostFX loaded: " << postfx_presets.size() << " preset(s)\n"; } catch (const fkyaml::exception &e) { std::cout << "Error parsing PostFX YAML: " << e.what() << ". Using defaults.\n"; postfx_presets = defaultPostFXPresets(); return false; } if (postfx_presets.empty()) { postfx_presets = defaultPostFXPresets(); } return true; } void setCrtPiFile(const std::string &path) { crtpi_file_path = path; } auto loadCrtPiFromFile() -> bool { crtpi_presets.clear(); current_crtpi_preset = 0; std::ifstream file(crtpi_file_path); if (!file.is_open()) { std::ofstream out(crtpi_file_path); if (out.is_open()) { writeCrtPiDefaults(out); out.close(); } crtpi_presets = defaultCrtPiPresets(); return true; } const std::string CONTENT((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); try { auto yaml = fkyaml::node::deserialize(CONTENT); if (yaml.contains("presets")) { for (const auto &p : yaml["presets"]) { CrtPiPreset preset; tryGet(p, "name", preset.name); parseFloatField(p, "scanline_weight", preset.scanline_weight); parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness); parseFloatField(p, "bloom_factor", preset.bloom_factor); parseFloatField(p, "input_gamma", preset.input_gamma); parseFloatField(p, "output_gamma", preset.output_gamma); parseFloatField(p, "mask_brightness", preset.mask_brightness); parseFloatField(p, "curvature_x", preset.curvature_x); parseFloatField(p, "curvature_y", preset.curvature_y); tryGet(p, "mask_type", preset.mask_type); tryGet(p, "enable_scanlines", preset.enable_scanlines); tryGet(p, "enable_multisample", preset.enable_multisample); tryGet(p, "enable_gamma", preset.enable_gamma); tryGet(p, "enable_curvature", preset.enable_curvature); tryGet(p, "enable_sharper", preset.enable_sharper); crtpi_presets.push_back(preset); } } std::cout << "CrtPi loaded: " << crtpi_presets.size() << " preset(s)\n"; } catch (const fkyaml::exception &e) { std::cout << "Error parsing CrtPi YAML: " << e.what() << ". Using defaults.\n"; crtpi_presets = defaultCrtPiPresets(); return false; } if (crtpi_presets.empty()) { crtpi_presets = defaultCrtPiPresets(); } return true; } } // namespace Options