#include "options.hpp" #include // Para SDL_ScaleMode, SDL_LogCategory, SDL_LogError, SDL_LogInfo, SDL_LogWarn #include // Para clamp #include // Para size_t #include // Para ifstream, ofstream #include // Para string #include // Para vector #include "difficulty.hpp" // Para Code, init #include "external/fkyaml_node.hpp" // Para fkyaml::node #include "input.hpp" // Para Input #include "lang.hpp" // Para getText, Code #include "ui/logger.hpp" // Para info #include "utils.hpp" // Para boolToString, getFileName namespace Options { // --- Variables globales --- Window window; // Opciones de la ventana Settings settings; // Opciones del juego Video video; // Opciones de vídeo Audio audio; // Opciones de audio GamepadManager gamepad_manager; // Opciones de mando para cada jugador Keyboard keyboard; // Opciones para el teclado PendingChanges pending_changes; // Opciones que se aplican al cerrar std::vector postfx_presets = { // Lista de presets de PostFX {"CRT", 0.6F, 0.7F, 0.15F, 0.5F, 0.5F, 0.0F, 0.0F, 0.0F}, {"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F}, {"CURVED", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F}, {"SCANLINES", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F}, {"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F}, {"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F}, }; int current_postfx_preset = 0; // Índice del preset PostFX activo std::string postfx_file_path; // Ruta al fichero de presets PostFX // Establece el fichero de configuración void setConfigFile(const std::string& file_path) { settings.config_file = file_path; } // Establece el fichero de configuración de mandos void setControllersFile(const std::string& file_path) { settings.controllers_file = file_path; } // Establece la ruta del fichero de PostFX void setPostFXFile(const std::string& path) { postfx_file_path = path; } // Helper: extrae un campo float de un nodo YAML si existe, ignorando errores de conversión static void parseFloatField(const fkyaml::node& node, const std::string& key, float& target) { if (node.contains(key)) { try { target = node[key].get_value(); } catch (...) {} } } // Carga los presets de PostFX desde el fichero auto loadPostFXFromFile() -> bool { postfx_presets.clear(); std::ifstream file(postfx_file_path); if (!file.good()) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "PostFX file not found, creating default: %s", postfx_file_path.c_str()); return savePostFXToFile(); } std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); try { auto yaml = fkyaml::node::deserialize(content); if (yaml.contains("presets")) { const auto& presets = yaml["presets"]; for (const auto& p : presets) { PostFXPreset preset; if (p.contains("name")) { preset.name = p["name"].get_value(); } parseFloatField(p, "vignette", preset.vignette); parseFloatField(p, "scanlines", preset.scanlines); parseFloatField(p, "chroma", preset.chroma); 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); postfx_presets.push_back(preset); } } if (!postfx_presets.empty()) { current_postfx_preset = std::clamp( current_postfx_preset, 0, static_cast(postfx_presets.size()) - 1); } else { current_postfx_preset = 0; } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "PostFX file loaded: %zu preset(s)", postfx_presets.size()); return true; } catch (const fkyaml::exception& e) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Error parsing PostFX YAML: %s. Recreating defaults.", e.what()); return savePostFXToFile(); } } // Guarda los presets de PostFX por defecto al fichero auto savePostFXToFile() -> bool { if (postfx_file_path.empty()) { return false; } std::ofstream file(postfx_file_path); if (!file.is_open()) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: %s can't be opened for writing", postfx_file_path.c_str()); return false; } file << "# Coffee Crisis Arcade Edition - PostFX Presets\n"; file << "# Each preset defines the intensity of post-processing effects (0.0 to 1.0).\n"; file << "# vignette: screen darkening at the edges\n"; file << "# scanlines: horizontal scanline effect\n"; file << "# chroma: chromatic aberration (RGB color fringing)\n"; file << "# mask: phosphor dot mask (RGB subpixel pattern)\n"; file << "# gamma: gamma correction input 2.4 / output 2.2\n"; file << "# curvature: CRT barrel distortion\n"; file << "# bleeding: NTSC horizontal colour bleeding\n"; file << "# flicker: phosphor CRT flicker ~50 Hz (0.0 = off, 1.0 = max)\n"; file << "\n"; file << "presets:\n"; file << " - name: \"CRT\"\n"; file << " vignette: 0.6\n"; file << " scanlines: 0.7\n"; file << " chroma: 0.15\n"; file << " mask: 0.5\n"; file << " gamma: 0.5\n"; file << " curvature: 0.0\n"; file << " bleeding: 0.0\n"; file << " flicker: 0.0\n"; file << " - name: \"NTSC\"\n"; file << " vignette: 0.4\n"; file << " scanlines: 0.5\n"; file << " chroma: 0.2\n"; file << " mask: 0.3\n"; file << " gamma: 0.3\n"; file << " curvature: 0.0\n"; file << " bleeding: 0.6\n"; file << " flicker: 0.0\n"; file << " - name: \"CURVED\"\n"; file << " vignette: 0.5\n"; file << " scanlines: 0.6\n"; file << " chroma: 0.1\n"; file << " mask: 0.4\n"; file << " gamma: 0.4\n"; file << " curvature: 0.8\n"; file << " bleeding: 0.0\n"; file << " flicker: 0.0\n"; file << " - name: \"SCANLINES\"\n"; file << " vignette: 0.0\n"; file << " scanlines: 0.8\n"; file << " chroma: 0.0\n"; file << " mask: 0.0\n"; file << " gamma: 0.0\n"; file << " curvature: 0.0\n"; file << " bleeding: 0.0\n"; file << " flicker: 0.0\n"; file << " - name: \"SUBTLE\"\n"; file << " vignette: 0.3\n"; file << " scanlines: 0.4\n"; file << " chroma: 0.05\n"; file << " mask: 0.0\n"; file << " gamma: 0.2\n"; file << " curvature: 0.0\n"; file << " bleeding: 0.0\n"; file << " flicker: 0.0\n"; file << " - name: \"CRT LIVE\"\n"; file << " vignette: 0.5\n"; file << " scanlines: 0.6\n"; file << " chroma: 0.3\n"; file << " mask: 0.3\n"; file << " gamma: 0.4\n"; file << " curvature: 0.3\n"; file << " bleeding: 0.4\n"; file << " flicker: 0.8\n"; file.close(); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "PostFX file created with defaults: %s", postfx_file_path.c_str()); // Cargar los presets recién escritos postfx_presets.clear(); postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F, 0.5F, 0.5F, 0.0F, 0.0F, 0.0F}); postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F}); postfx_presets.push_back({"CURVED", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F}); postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F}); postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F}); postfx_presets.push_back({"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F}); current_postfx_preset = 0; return true; } // Inicializa las opciones del programa void init() { // Dificultades Difficulty::init(); // Opciones de control gamepad_manager.init(); setKeyboardToPlayer(Player::Id::PLAYER1); // Opciones de cambios pendientes pending_changes.new_language = settings.language; pending_changes.new_difficulty = settings.difficulty; pending_changes.has_pending_changes = false; } // --- Funciones helper de carga desde YAML --- void loadWindowFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("window")) { return; } const auto& win = yaml["window"]; if (win.contains("zoom")) { try { int val = win["zoom"].get_value(); window.zoom = (val > 0) ? val : window.zoom; } catch (...) {} } } void loadVideoFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("video")) { return; } const auto& vid = yaml["video"]; if (vid.contains("fullscreen")) { try { video.fullscreen = vid["fullscreen"].get_value(); } catch (...) {} } if (vid.contains("scale_mode")) { try { video.scale_mode = static_cast(vid["scale_mode"].get_value()); } catch (...) {} } if (vid.contains("vsync")) { try { video.vsync = vid["vsync"].get_value(); } catch (...) {} } if (vid.contains("integer_scale")) { try { video.integer_scale = vid["integer_scale"].get_value(); } catch (...) {} } if (vid.contains("postfx")) { try { video.postfx = vid["postfx"].get_value(); } catch (...) {} } // Nuevo formato: supersampling (bool) + supersampling_amount (int) // Backward compat: si solo existe supersampling como int, también funciona { bool ss_enabled = false; int ss_amount = 3; if (vid.contains("supersampling")) { try { const auto& node = vid["supersampling"]; if (node.is_boolean()) { ss_enabled = node.get_value(); } else { // Formato antiguo: int directamente int factor = node.get_value(); ss_enabled = factor >= 2; ss_amount = (factor >= 2) ? factor : 3; } } catch (...) {} } if (vid.contains("supersampling_amount")) { try { int amount = vid["supersampling_amount"].get_value(); if (amount >= 2) { ss_amount = amount; } } catch (...) {} } video.supersampling = ss_enabled ? ss_amount : 1; } if (vid.contains("postfx_preset")) { try { int preset = vid["postfx_preset"].get_value(); // No validamos contra postfx_presets.size() aquí porque postfx.yaml // aún no se ha cargado. El clamp se hace en loadPostFXFromFile(). if (preset >= 0) { current_postfx_preset = preset; } } catch (...) {} } } void loadAudioFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("audio")) { return; } const auto& aud = yaml["audio"]; if (aud.contains("enabled")) { try { audio.enabled = aud["enabled"].get_value(); } catch (...) {} } if (aud.contains("volume")) { try { audio.volume = std::clamp(aud["volume"].get_value(), 0, 100); } catch (...) {} } if (aud.contains("music")) { const auto& mus = aud["music"]; if (mus.contains("enabled")) { try { audio.music.enabled = mus["enabled"].get_value(); } catch (...) {} } if (mus.contains("volume")) { try { audio.music.volume = std::clamp(mus["volume"].get_value(), 0, 100); } catch (...) {} } } if (aud.contains("sound")) { const auto& snd = aud["sound"]; if (snd.contains("enabled")) { try { audio.sound.enabled = snd["enabled"].get_value(); } catch (...) {} } if (snd.contains("volume")) { try { audio.sound.volume = std::clamp(snd["volume"].get_value(), 0, 100); } catch (...) {} } } } void loadGameFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("game")) { return; } const auto& game = yaml["game"]; if (game.contains("language")) { try { auto lang = static_cast(game["language"].get_value()); if (lang == Lang::Code::ENGLISH || lang == Lang::Code::VALENCIAN || lang == Lang::Code::SPANISH) { settings.language = lang; } else { settings.language = Lang::Code::ENGLISH; } pending_changes.new_language = settings.language; } catch (...) {} } if (game.contains("difficulty")) { try { settings.difficulty = static_cast(game["difficulty"].get_value()); pending_changes.new_difficulty = settings.difficulty; } catch (...) {} } if (game.contains("autofire")) { try { settings.autofire = game["autofire"].get_value(); } catch (...) {} } if (game.contains("shutdown_enabled")) { try { settings.shutdown_enabled = game["shutdown_enabled"].get_value(); } catch (...) {} } if (game.contains("params_file")) { try { settings.params_file = game["params_file"].get_value(); } catch (...) {} } } void loadControllersFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("controllers")) { return; } const auto& controllers = yaml["controllers"]; size_t i = 0; for (const auto& ctrl : controllers) { if (i >= GamepadManager::size()) { break; } if (ctrl.contains("name")) { try { gamepad_manager[i].name = ctrl["name"].get_value(); } catch (...) {} } if (ctrl.contains("path")) { try { gamepad_manager[i].path = ctrl["path"].get_value(); } catch (...) {} } if (ctrl.contains("player")) { try { int player_int = ctrl["player"].get_value(); if (player_int == 1) { gamepad_manager[i].player_id = Player::Id::PLAYER1; } else if (player_int == 2) { gamepad_manager[i].player_id = Player::Id::PLAYER2; } } catch (...) {} } ++i; } } void loadKeyboardFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("keyboard")) { return; } const auto& kb = yaml["keyboard"]; if (kb.contains("player")) { try { keyboard.player_id = static_cast(kb["player"].get_value()); } catch (...) {} } } // Carga el fichero de configuración auto loadFromFile() -> bool { init(); std::ifstream file(settings.config_file); if (!file.is_open()) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Config file not found. Creating default settings."); saveToFile(); return true; } Logger::info("Reading file: " + getFileName(settings.config_file)); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); try { auto yaml = fkyaml::node::deserialize(content); loadWindowFromYaml(yaml); loadVideoFromYaml(yaml); loadAudioFromYaml(yaml); loadGameFromYaml(yaml); loadControllersFromYaml(yaml); loadKeyboardFromYaml(yaml); } catch (const fkyaml::exception& e) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Error parsing YAML config: %s. Using defaults.", e.what()); init(); saveToFile(); return true; } gamepad_manager.assignAndLinkGamepads(); return true; } // Guarda el fichero de configuración auto saveToFile() -> bool { std::ofstream file(settings.config_file); if (!file.good()) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: %s can't be opened", getFileName(settings.config_file).c_str()); return false; } Logger::info("Writing file: " + getFileName(settings.config_file)); applyPendingChanges(); file << "# Coffee Crisis Arcade Edition - Configuration File\n"; file << "# This file is automatically generated and managed by the game.\n"; file << "\n"; file << "version: " << settings.config_version << "\n"; file << "\n"; // WINDOW file << "# WINDOW\n"; file << "window:\n"; file << " zoom: " << window.zoom << "\n"; file << "\n"; // VIDEO file << "# VIDEO\n"; file << "video:\n"; file << " fullscreen: " << boolToString(video.fullscreen) << "\n"; file << " scale_mode: " << static_cast(video.scale_mode) << " # " << static_cast(SDL_ScaleMode::SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast(SDL_ScaleMode::SDL_SCALEMODE_LINEAR) << ": linear\n"; file << " vsync: " << boolToString(video.vsync) << "\n"; file << " integer_scale: " << boolToString(video.integer_scale) << "\n"; file << " postfx: " << boolToString(video.postfx) << "\n"; file << " postfx_preset: " << current_postfx_preset << "\n"; file << " supersampling: " << boolToString(video.supersampling > 1) << "\n"; file << " supersampling_amount: " << std::max(2, video.supersampling) << "\n"; file << "\n"; // AUDIO file << "# AUDIO (volume range: 0..100)\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"; file << "\n"; // GAME file << "# GAME\n"; file << "game:\n"; file << " language: " << static_cast(settings.language) << " # 0: spanish, 1: valencian, 2: english\n"; file << " difficulty: " << static_cast(settings.difficulty) << " # " << static_cast(Difficulty::Code::EASY) << ": easy, " << static_cast(Difficulty::Code::NORMAL) << ": normal, " << static_cast(Difficulty::Code::HARD) << ": hard\n"; file << " autofire: " << boolToString(settings.autofire) << "\n"; file << " shutdown_enabled: " << boolToString(settings.shutdown_enabled) << "\n"; file << " params_file: " << settings.params_file << "\n"; file << "\n"; // CONTROLLERS file << "# CONTROLLERS\n"; file << "controllers:\n"; gamepad_manager.saveToFile(file); file << "\n"; // KEYBOARD file << "# KEYBOARD\n"; file << "keyboard:\n"; file << " player: " << static_cast(keyboard.player_id) << "\n"; file.close(); return true; } // Asigna el teclado al jugador void setKeyboardToPlayer(Player::Id player_id) { keyboard.player_id = player_id; } // Intercambia el teclado de jugador void swapKeyboard() { keyboard.player_id = keyboard.player_id == Player::Id::PLAYER1 ? Player::Id::PLAYER2 : Player::Id::PLAYER1; } // Intercambia los jugadores asignados a los dos primeros mandos void swapControllers() { gamepad_manager.swapPlayers(); } // Averigua quien está usando el teclado auto getPlayerWhoUsesKeyboard() -> Player::Id { return keyboard.player_id; } // Aplica los cambios pendientes copiando los valores a sus variables void applyPendingChanges() { if (pending_changes.has_pending_changes) { settings.language = pending_changes.new_language; settings.difficulty = pending_changes.new_difficulty; pending_changes.has_pending_changes = false; } } void checkPendingChanges() { pending_changes.has_pending_changes = settings.language != pending_changes.new_language || settings.difficulty != pending_changes.new_difficulty; } // Buscar y asignar un mando disponible por nombre auto assignGamepadByName(const std::string& gamepad_name_to_find, Player::Id player_id) -> bool { auto found_gamepad = Input::get()->findAvailableGamepadByName(gamepad_name_to_find); if (found_gamepad) { return gamepad_manager.assignGamepadToPlayer(player_id, found_gamepad, found_gamepad->name); } return false; } // Obtener información de un gamepad específico auto getGamepadInfo(Player::Id player_id) -> std::string { try { const auto& gamepad = gamepad_manager.getGamepad(player_id); return "Player " + std::to_string(static_cast(player_id)) + ": " + (gamepad.name.empty() ? "No gamepad" : gamepad.name); } catch (const std::exception&) { return "Invalid player"; } } // Asigna los mandos físicos basándose en la configuración actual. void GamepadManager::assignAndLinkGamepads() { auto physical_gamepads = Input::get()->getGamepads(); std::array desired_paths; for (size_t i = 0; i < MAX_PLAYERS; ++i) { desired_paths[i] = gamepads_[i].path; gamepads_[i].instance = nullptr; } std::vector> assigned_instances; assignGamepadsByPath(desired_paths, physical_gamepads, assigned_instances); assignRemainingGamepads(physical_gamepads, assigned_instances); clearUnassignedGamepadSlots(); } // --- PRIMERA PASADA: Intenta asignar mandos basándose en la ruta guardada --- void GamepadManager::assignGamepadsByPath( const std::array& desired_paths, const std::vector>& physical_gamepads, // NOLINT(readability-named-parameter) std::vector>& assigned_instances) { for (size_t i = 0; i < MAX_PLAYERS; ++i) { const std::string& desired_path = desired_paths[i]; if (desired_path.empty()) { continue; } for (const auto& physical_gamepad : physical_gamepads) { if (physical_gamepad->path == desired_path && !isGamepadAssigned(physical_gamepad, assigned_instances)) { gamepads_[i].instance = physical_gamepad; gamepads_[i].name = physical_gamepad->name; assigned_instances.push_back(physical_gamepad); break; } } } } // --- SEGUNDA PASADA: Asigna los mandos físicos restantes a los jugadores libres --- void GamepadManager::assignRemainingGamepads( const std::vector>& physical_gamepads, // NOLINT(readability-named-parameter) std::vector>& assigned_instances) { for (size_t i = 0; i < MAX_PLAYERS; ++i) { if (gamepads_[i].instance != nullptr) { continue; } for (const auto& physical_gamepad : physical_gamepads) { if (!isGamepadAssigned(physical_gamepad, assigned_instances)) { gamepads_[i].instance = physical_gamepad; gamepads_[i].name = physical_gamepad->name; gamepads_[i].path = physical_gamepad->path; assigned_instances.push_back(physical_gamepad); break; } } } } // --- TERCERA PASADA: Limpia la información "fantasma" de los slots no asignados --- void GamepadManager::clearUnassignedGamepadSlots() { for (auto& gamepad_config : gamepads_) { if (gamepad_config.instance == nullptr) { gamepad_config.name = Lang::getText("[SERVICE_MENU] NO_CONTROLLER"); gamepad_config.path = ""; } } } auto GamepadManager::isGamepadAssigned( const std::shared_ptr& physical_gamepad, const std::vector>& assigned_instances) -> bool { // NOLINT(readability-named-parameter) return std::ranges::any_of(assigned_instances, [&physical_gamepad](const auto& assigned) -> auto { return assigned == physical_gamepad; }); } // Convierte un player id a texto segun Lang auto playerIdToString(Player::Id player_id) -> std::string { switch (player_id) { case Player::Id::PLAYER1: return Lang::getText("[SERVICE_MENU] PLAYER1"); case Player::Id::PLAYER2: return Lang::getText("[SERVICE_MENU] PLAYER2"); default: return ""; } } // Convierte un texto a player id segun Lang auto stringToPlayerId(const std::string& name) -> Player::Id { if (name == Lang::getText("[SERVICE_MENU] PLAYER1")) { return Player::Id::PLAYER1; } if (name == Lang::getText("[SERVICE_MENU] PLAYER2")) { return Player::Id::PLAYER2; } return Player::Id::NO_PLAYER; } } // namespace Options