430 lines
17 KiB
C++
430 lines
17 KiB
C++
#include "options.h"
|
|
|
|
#include <SDL3/SDL.h> // Para SDL_ScaleMode, SDL_GamepadButton, SDL_LogCategory, SDL_LogInfo, SDL_LogError, SDL_LogWarn
|
|
|
|
#include <algorithm> // Para clamp, max
|
|
#include <cstddef> // Para size_t
|
|
#include <fstream> // Para basic_ostream, operator<<, basic_ostream::operator<<, basic_ofstream, basic_istream, basic_ifstream, ifstream, ofstream
|
|
#include <functional> // Para function
|
|
#include <map> // Para map, operator==, _Rb_tree_const_iterator
|
|
#include <stdexcept> // Para invalid_argument, out_of_range
|
|
#include <string> // Para char_traits, stoi, operator==, operator<<, allocator, string, basic_string, operator<=>, getline
|
|
#include <utility> // Para swap, pair
|
|
#include <vector> // Para vector
|
|
|
|
#include "difficulty.h" // Para Code, init
|
|
#include "input.h" // Para InputDevice
|
|
#include "lang.h" // Para Code
|
|
#include "utils.h" // 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);
|
|
|
|
// 2. Si el fichero existe, lo leemos para obtener los nombres de los mandos.
|
|
if (file.good()) {
|
|
// --- CASO: EL FICHERO EXISTE ---
|
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\nReading file: %s", getFileName(settings.config_file).c_str());
|
|
std::string line;
|
|
while (std::getline(file, line)) {
|
|
if (line.substr(0, 1) != "#") {
|
|
int pos = line.find("=");
|
|
if (!set(line.substr(0, pos), line.substr(pos + 1, line.length()))) {
|
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unknown parameter: %s", line.substr(0, pos).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.good()) {
|
|
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();
|
|
|
|
// Opciones de ventana
|
|
file << "## WINDOW\n";
|
|
file << "\n";
|
|
|
|
file << "window.zoom=" << window.zoom << "\n";
|
|
|
|
// Opciones de video
|
|
file << "## VIDEO\n";
|
|
file << "## video.scale_mode [" << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_LINEAR) << ": lineal]\n";
|
|
file << "\n";
|
|
|
|
file << "video.fullscreen=" << boolToString(video.fullscreen) << "\n";
|
|
file << "video.scale_mode=" << static_cast<int>(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\n## AUDIO\n";
|
|
file << "## volume [0 .. 100]\n";
|
|
file << "\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\n## GAME\n";
|
|
file << "## game.language [0: spanish, 1: valencian, 2: english]\n";
|
|
file << "## game.difficulty [" << static_cast<int>(Difficulty::Code::EASY) << ": easy, " << static_cast<int>(Difficulty::Code::NORMAL) << ": normal, " << static_cast<int>(Difficulty::Code::HARD) << ": hard]\n";
|
|
file << "\n";
|
|
|
|
file << "game.language=" << static_cast<int>(settings.language) << "\n";
|
|
file << "game.difficulty=" << static_cast<int>(settings.difficulty) << "\n";
|
|
file << "game.autofire=" << boolToString(settings.autofire) << "\n";
|
|
file << "game.shutdown_enabled=" << boolToString(settings.shutdown_enabled) << "\n";
|
|
|
|
// Opciones de mandos
|
|
file << "\n\n## CONTROLLERS\n";
|
|
gamepad_manager.saveToFile(file);
|
|
|
|
// Opciones de teclado
|
|
file << "\n\n## KEYBOARD\n";
|
|
file << "keyboard.player=" << static_cast<int>(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<std::string, std::function<void(const std::string&)>> SETTINGS_MAP = {
|
|
// 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<SDL_ScaleMode>(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<Lang::Code>(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<Difficulty::Code>(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); }},
|
|
// Teclado
|
|
{"keyboard.player", [](const auto& val) { keyboard.player_id = static_cast<Player::Id>(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<int>(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<std::string, MAX_PLAYERS> 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<std::shared_ptr<Input::Gamepad>> 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<std::string, MAX_PLAYERS>& desired_paths,
|
|
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
|
std::vector<std::shared_ptr<Input::Gamepad>>& 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<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
|
std::vector<std::shared_ptr<Input::Gamepad>>& 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<Input::Gamepad>& physical_gamepad,
|
|
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool {
|
|
for (const auto& assigned : assigned_instances) {
|
|
if (assigned == physical_gamepad) {
|
|
return true; // Encontrado, por lo tanto, ya está asignado.
|
|
}
|
|
}
|
|
return false; // No se encontró en la lista.
|
|
}
|
|
|
|
// 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(std::string name) -> Player::Id {
|
|
if (name == Lang::getText("[SERVICE_MENU] PLAYER1")) {
|
|
return Player::Id::PLAYER1;
|
|
} else if (name == Lang::getText("[SERVICE_MENU] PLAYER2")) {
|
|
return Player::Id::PLAYER2;
|
|
} else {
|
|
return Player::Id::NO_PLAYER;
|
|
}
|
|
}
|
|
} // namespace Options
|