#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 // 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; } // 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("shaders")) { try { video.shaders = vid["shaders"].get_value(); } 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 << " shaders: " << boolToString(video.shaders) << "\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