#include "options.hpp" #include // Para SDL_ScaleMode, SDL_GamepadButton, SDL_LogCategory, SDL_LogInfo, SDL_LogError, SDL_LogWarn #include // Para clamp, max #include // Para size_t #include // Para basic_ostream, operator<<, basic_ostream::operator<<, basic_ofstream, basic_istream, basic_ifstream, ifstream, ofstream #include // Para function #include // Para map, operator==, _Rb_tree_const_iterator #include // Para std::ranges::any_of #include // Para istringstream #include // Para invalid_argument, out_of_range #include // Para char_traits, stoi, operator==, operator<<, allocator, string, basic_string, operator<=>, getline #include // Para swap, pair #include // Para vector #include "difficulty.hpp" // Para Code, init #include "input.hpp" // Para InputDevice #include "lang.hpp" // Para Code #include "utils.hpp" // Para boolToString, stringToBool, 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 // Declaraciones auto set(const std::string& var, const std::string& value) -> bool; // 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; } // Carga el fichero de configuración auto loadFromFile() -> bool { // 1. Inicializa las opciones con valores por defecto. init(); std::ifstream file(settings.config_file); bool file_exists = file.is_open(); // Guardamos si el fichero existía al abrirlo // 2. Si el fichero existe, lo leemos para obtener los nombres de los mandos. if (file_exists) { // --- CASO: EL FICHERO EXISTE --- SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\nReading file: %s", getFileName(settings.config_file).c_str()); std::string line; std::string param_name; std::string param_value; while (std::getline(file, line)) { // Elimina comentarios auto comment_pos = line.find('#'); if (comment_pos != std::string::npos) { line.resize(comment_pos); } // Si la línea contiene '=', lo reemplazamos por un espacio para compatibilidad auto equals_pos = line.find('='); if (equals_pos != std::string::npos) { line[equals_pos] = ' '; } // Usa un stream para separar palabras (elimina automáticamente espacios extra) std::istringstream iss(line); if (iss >> param_name >> param_value) { if (!set(param_name, param_value)) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unknown parameter: %s", param_name.c_str()); } } } file.close(); } // 3. Llamamos al asignador inteligente. gamepad_manager.assignAndLinkGamepads(); // 4. Si el fichero no existía, lo creamos ahora con la configuración por defecto. if (!file_exists) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Config file not found. Creating default settings."); saveToFile(); } 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; } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Writing file: %s", getFileName(settings.config_file).c_str()); applyPendingChanges(); // Versión del archivo file << "# Coffee Crisis Arcade Edition - Configuration File\n"; file << "# Format: key value\n"; file << "config.version " << settings.config_version << "\n"; // Opciones de ventana file << "\n# WINDOW\n"; file << "window.zoom " << window.zoom << "\n"; // Opciones de video file << "\n# VIDEO\n"; file << "# video.scale_mode [" << static_cast(SDL_ScaleMode::SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast(SDL_ScaleMode::SDL_SCALEMODE_LINEAR) << ": linear]\n"; file << "video.fullscreen " << boolToString(video.fullscreen) << "\n"; file << "video.scale_mode " << static_cast(video.scale_mode) << "\n"; file << "video.vsync " << boolToString(video.vsync) << "\n"; file << "video.integer_scale " << boolToString(video.integer_scale) << "\n"; file << "video.shaders " << boolToString(video.shaders) << "\n"; // Opciones de audio file << "\n# AUDIO\n"; file << "# volume range: [0 .. 100]\n"; file << "audio.enabled " << boolToString(audio.enabled) << "\n"; file << "audio.volume " << audio.volume << "\n"; file << "audio.music.enabled " << boolToString(audio.music.enabled) << "\n"; file << "audio.music.volume " << audio.music.volume << "\n"; file << "audio.sound.enabled " << boolToString(audio.sound.enabled) << "\n"; file << "audio.sound.volume " << audio.sound.volume << "\n"; // Opciones del juego file << "\n# GAME\n"; file << "# game.language [0: spanish, 1: valencian, 2: english]\n"; file << "# game.difficulty [" << static_cast(Difficulty::Code::EASY) << ": easy, " << static_cast(Difficulty::Code::NORMAL) << ": normal, " << static_cast(Difficulty::Code::HARD) << ": hard]\n"; file << "game.language " << static_cast(settings.language) << "\n"; file << "game.difficulty " << static_cast(settings.difficulty) << "\n"; file << "game.autofire " << boolToString(settings.autofire) << "\n"; file << "game.shutdown_enabled " << boolToString(settings.shutdown_enabled) << "\n"; file << "game.params_file " << settings.params_file << "\n"; // Opciones de mandos file << "\n# CONTROLLERS\n"; gamepad_manager.saveToFile(file); // Opciones de teclado file << "\n# KEYBOARD\n"; file << "keyboard.player " << static_cast(keyboard.player_id) << "\n"; // Cierra el fichero file.close(); return true; } // Función auxiliar para analizar la configuración del mando y reducir duplicación void parseAndSetController(const std::string& var, const std::string& value) { size_t first_dot = var.find('.'); size_t second_dot = var.find('.', first_dot + 1); if (first_dot == std::string::npos || second_dot == std::string::npos) { return; // Formato inválido } try { int controller_index = std::stoi(var.substr(first_dot + 1, second_dot - first_dot - 1)); std::string setting_key = var.substr(second_dot + 1); gamepad_manager.setControllerProperty(controller_index, setting_key, value); } catch (const std::exception&) { // Error en parsing } } auto set(const std::string& var, const std::string& value) -> bool { // Clausula de protección: ignora líneas vacías o comentarios if (var.empty() || var.starts_with("#")) { return true; } // Un mapa estático asegura que se inicializa solo una vez static const std::map> SETTINGS_MAP = { // Configuración {"config.version", [](const auto& val) { settings.config_version = std::stoi(val); }}, // Ventana {"window.zoom", [](const auto& val) { window.zoom = std::stoi(val); }}, // Vídeo {"video.fullscreen", [](const auto& val) { video.fullscreen = stringToBool(val); }}, {"video.scale_mode", [](const auto& val) { video.scale_mode = static_cast(std::stoi(val)); }}, {"video.shaders", [](const auto& val) { video.shaders = stringToBool(val); }}, {"video.integer_scale", [](const auto& val) { video.integer_scale = stringToBool(val); }}, {"video.vsync", [](const auto& val) { video.vsync = stringToBool(val); }}, // Audio {"audio.enabled", [](const auto& val) { audio.enabled = stringToBool(val); }}, {"audio.volume", [](const auto& val) { audio.volume = std::clamp(std::stoi(val), 0, 100); }}, {"audio.music.enabled", [](const auto& val) { audio.music.enabled = stringToBool(val); }}, {"audio.music.volume", [](const auto& val) { audio.music.volume = std::clamp(std::stoi(val), 0, 100); }}, {"audio.sound.enabled", [](const auto& val) { audio.sound.enabled = stringToBool(val); }}, {"audio.sound.volume", [](const auto& val) { audio.sound.volume = std::clamp(std::stoi(val), 0, 100); }}, // Juego {"game.language", [](const auto& val) { settings.language = static_cast(std::stoi(val)); if (settings.language != Lang::Code::ENGLISH && settings.language != Lang::Code::VALENCIAN && settings.language != Lang::Code::SPANISH) { settings.language = Lang::Code::ENGLISH; } pending_changes.new_language = settings.language; }}, {"game.difficulty", [](const auto& val) { settings.difficulty = static_cast(std::stoi(val)); pending_changes.new_difficulty = settings.difficulty; }}, {"game.autofire", [](const auto& val) { settings.autofire = stringToBool(val); }}, {"game.shutdown_enabled", [](const auto& val) { settings.shutdown_enabled = stringToBool(val); }}, {"game.params_file", [](const auto& val) { settings.params_file = val; }}, // Teclado {"keyboard.player", [](const auto& val) { keyboard.player_id = static_cast(stoi(val)); }}}; // Maneja por separado la configuración general de los mandos if (var.starts_with("controller.")) { try { parseAndSetController(var, value); return true; } catch (const std::out_of_range& e) { // Error: por ejemplo, índice de mando fuera de rango return false; } catch (const std::invalid_argument& e) { // Error: por ejemplo, fallo en std::stoi return false; } } // Busca el nombre de la variable en el mapa if (auto it = SETTINGS_MAP.find(var); it != SETTINGS_MAP.end()) { try { // Ejecuta la función lambda asociada it->second(value); return true; } catch (const std::invalid_argument& e) { // Maneja casos donde std::stoi falla por entrada inválida return false; } } // Si la clave no se encontró en el mapa ni en la lógica de mandos return false; } // 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() { // 1. Obtenemos los mandos físicos conectados. auto physical_gamepads = Input::get()->getGamepads(); // 2. Reiniciamos las asignaciones actuales. std::array desired_paths; for (size_t i = 0; i < MAX_PLAYERS; ++i) { desired_paths[i] = gamepads_[i].path; gamepads_[i].instance = nullptr; } // 3. Vector para rastrear los mandos ya asignados. std::vector> assigned_instances; // --- Ejecutamos las pasadas de asignación y limpieza --- // Pasada 1: Intenta asignar por la ruta guardada. assignGamepadsByPath(desired_paths, physical_gamepads, assigned_instances); // Pasada 2: Asigna los mandos restantes a los jugadores libres. assignRemainingGamepads(physical_gamepads, assigned_instances); // Pasada 3: Limpia los datos de los slots que se quedaron sin mando. 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, 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; // No hay ruta guardada para este slot. } // Buscamos un mando físico que coincida con la ruta y no esté ya asignado. for (const auto& physical_gamepad : physical_gamepads) { if (physical_gamepad->path == desired_path && !isGamepadAssigned(physical_gamepad, assigned_instances)) { // Asignamos y actualizamos TODOS los datos. gamepads_[i].instance = physical_gamepad; gamepads_[i].name = physical_gamepad->name; // <--- LA LÍNEA QUE FALTABA // No es necesario actualizar la path aquí porque ya coincide. assigned_instances.push_back(physical_gamepad); break; // Mando encontrado para este jugador, pasamos al siguiente. } } } } // --- SEGUNDA PASADA: Asigna los mandos físicos restantes a los jugadores libres --- void GamepadManager::assignRemainingGamepads( const std::vector>& physical_gamepads, std::vector>& assigned_instances) { for (size_t i = 0; i < MAX_PLAYERS; ++i) { if (gamepads_[i].instance != nullptr) { continue; // Este jugador ya tiene un mando. } // Buscamos un mando físico que todavía esté libre. for (const auto& physical_gamepad : physical_gamepads) { if (!isGamepadAssigned(physical_gamepad, assigned_instances)) { gamepads_[i].instance = physical_gamepad; // MUY IMPORTANTE: Actualizamos la configuración para reflejar la realidad. gamepads_[i].name = physical_gamepad->name; gamepads_[i].path = physical_gamepad->path; assigned_instances.push_back(physical_gamepad); break; // Mando encontrado, pasamos al siguiente jugador. } } } } // --- TERCERA PASADA: Limpia la información "fantasma" de los slots no asignados --- void GamepadManager::clearUnassignedGamepadSlots() { // Recorremos los slots de jugador una última vez. for (auto& gamepad_config : gamepads_) { // Si un slot no tiene una instancia física enlazada (instance == nullptr), // significa que no hay un mando para él. if (gamepad_config.instance == nullptr) { // Limpiamos sus datos de configuración para no mostrar información // de un mando que ya no está conectado. gamepad_config.name = Lang::getText("[SERVICE_MENU] NO_CONTROLLER"); gamepad_config.path = ""; } } } // Función auxiliar para comprobar si un mando físico ya está en la lista de asignados. // Devuelve 'true' si ya ha sido asignado, 'false' en caso contrario. auto GamepadManager::isGamepadAssigned( const std::shared_ptr& physical_gamepad, const std::vector>& assigned_instances) -> bool { return std::ranges::any_of(assigned_instances, [&physical_gamepad](const auto& assigned) { 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