814 lines
37 KiB
C++
814 lines
37 KiB
C++
#include "game/options.hpp"
|
|
|
|
#include <SDL3/SDL.h> // Para SDL_ScaleMode, SDL_LogCategory, SDL_LogError, SDL_LogInfo, SDL_LogWarn
|
|
|
|
#include <algorithm> // Para clamp, ranges::find_if
|
|
#include <cstddef> // Para size_t
|
|
#include <fstream> // Para ifstream, ofstream
|
|
#include <iostream> // Para std::cout
|
|
#include <string> // Para string
|
|
#include <vector> // Para vector
|
|
|
|
#include "core/input/input.hpp" // Para Input
|
|
#include "core/locale/lang.hpp" // Para getText, Code
|
|
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
|
#include "game/gameplay/difficulty.hpp" // Para Code, init
|
|
#include "utils/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
|
|
Loading loading; // Opciones de la pantalla de carga
|
|
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<PostFXPreset> postfx_presets = {
|
|
{.name = "CRT", .vignette = 0.15F, .scanlines = 0.7F, .chroma = 0.2F, .mask = 0.5F, .gamma = 0.1F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
|
{.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma = 0.2F, .mask = 0.3F, .gamma = 0.3F, .curvature = 0.0F, .bleeding = 0.6F, .flicker = 0.0F},
|
|
{.name = "Curved", .vignette = 0.5F, .scanlines = 0.6F, .chroma = 0.1F, .mask = 0.4F, .gamma = 0.4F, .curvature = 0.8F, .bleeding = 0.0F, .flicker = 0.0F},
|
|
{.name = "Scanlines", .vignette = 0.0F, .scanlines = 0.8F, .chroma = 0.0F, .mask = 0.0F, .gamma = 0.0F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
|
{.name = "Subtle", .vignette = 0.3F, .scanlines = 0.4F, .chroma = 0.05F, .mask = 0.0F, .gamma = 0.2F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
|
{.name = "CRT Live", .vignette = 0.15F, .scanlines = 0.6F, .chroma = 0.3F, .mask = 0.3F, .gamma = 0.1F, .curvature = 0.0F, .bleeding = 0.4F, .flicker = 0.8F},
|
|
};
|
|
std::string postfx_file_path;
|
|
std::vector<CrtPiPreset> crtpi_presets;
|
|
std::string crtpi_file_path;
|
|
|
|
// 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; }
|
|
|
|
// Establece la ruta del fichero de CrtPi
|
|
void setCrtPiFile(const std::string& path) { crtpi_file_path = path; }
|
|
|
|
// Helper: extrae un campo float de un nodo YAML si existe, ignorando errores de conversión
|
|
// Llig un camp opcional d'un node YAML. Si no existeix, no toca `target`.
|
|
// Si existeix però el tipus no encaixa, manté el valor previ i avisa per stderr
|
|
// (un fitxer de configuració parcialment malformat no ha de tombar l'arrencada,
|
|
// però l'usuari ha de saber quin camp ha quedat ignorat).
|
|
template <typename T>
|
|
void parseField(const fkyaml::node& node, const std::string& key, T& target) {
|
|
if (!node.contains(key)) { return; }
|
|
try {
|
|
target = node[key].get_value<T>();
|
|
} catch (...) {
|
|
std::cerr << "config YAML: valor invàlid per a '" << key << "', es manté el valor per defecte\n";
|
|
}
|
|
}
|
|
|
|
// Carga los presets de PostFX desde el fichero
|
|
auto loadPostFXFromFile() -> bool {
|
|
postfx_presets.clear();
|
|
|
|
std::ifstream file(postfx_file_path);
|
|
if (!file.good()) {
|
|
return savePostFXToFile();
|
|
}
|
|
|
|
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
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<std::string>();
|
|
}
|
|
parseField(p, "vignette", preset.vignette);
|
|
parseField(p, "scanlines", preset.scanlines);
|
|
parseField(p, "chroma", preset.chroma);
|
|
parseField(p, "mask", preset.mask);
|
|
parseField(p, "gamma", preset.gamma);
|
|
parseField(p, "curvature", preset.curvature);
|
|
parseField(p, "bleeding", preset.bleeding);
|
|
parseField(p, "flicker", preset.flicker);
|
|
postfx_presets.push_back(preset);
|
|
}
|
|
}
|
|
|
|
if (!postfx_presets.empty()) {
|
|
// Resolver nombre → índice
|
|
if (!video.shader.current_postfx_preset_name.empty()) {
|
|
for (int i = 0; i < static_cast<int>(postfx_presets.size()); ++i) {
|
|
if (postfx_presets[static_cast<size_t>(i)].name == video.shader.current_postfx_preset_name) {
|
|
video.shader.current_postfx_preset = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
video.shader.current_postfx_preset = std::clamp(
|
|
video.shader.current_postfx_preset,
|
|
0,
|
|
static_cast<int>(postfx_presets.size()) - 1);
|
|
} else {
|
|
video.shader.current_postfx_preset = 0;
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (const fkyaml::exception& e) {
|
|
std::cout << "Error parsing PostFX YAML: " << e.what() << ". Recreating defaults." << '\n';
|
|
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()) {
|
|
std::cout << "Error: " << postfx_file_path << " can't be opened for writing" << '\n';
|
|
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.15\n";
|
|
file << " scanlines: 0.7\n";
|
|
file << " chroma: 0.2\n";
|
|
file << " mask: 0.5\n";
|
|
file << " gamma: 0.1\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.15\n";
|
|
file << " scanlines: 0.6\n";
|
|
file << " chroma: 0.3\n";
|
|
file << " mask: 0.3\n";
|
|
file << " gamma: 0.1\n";
|
|
file << " curvature: 0.0\n";
|
|
file << " bleeding: 0.4\n";
|
|
file << " flicker: 0.8\n";
|
|
|
|
file.close();
|
|
|
|
// Cargar los presets recién escritos
|
|
postfx_presets.clear();
|
|
postfx_presets.push_back({"CRT", 0.15F, 0.7F, 0.2F, 0.5F, 0.1F, 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.15F, 0.6F, 0.3F, 0.3F, 0.1F, 0.0F, 0.4F, 0.8F});
|
|
video.shader.current_postfx_preset = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Rellena los presets CrtPi por defecto
|
|
static void populateDefaultCrtPiPresets() {
|
|
crtpi_presets.clear();
|
|
crtpi_presets.push_back({"Default", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
|
|
crtpi_presets.push_back({"Curved", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false});
|
|
crtpi_presets.push_back({"Sharp", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true});
|
|
crtpi_presets.push_back({"Minimal", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false});
|
|
}
|
|
|
|
// Escribe los presets CrtPi por defecto al fichero
|
|
static auto saveCrtPiDefaults() -> bool {
|
|
if (crtpi_file_path.empty()) { return false; }
|
|
std::ofstream file(crtpi_file_path);
|
|
if (!file.is_open()) {
|
|
std::cout << "Error: " << crtpi_file_path << " can't be opened for writing" << '\n';
|
|
return false;
|
|
}
|
|
file << "# Coffee Crisis Arcade Edition - CrtPi Shader Presets\n";
|
|
file << "# scanline_weight: gaussian adjustment (higher = narrower scanlines, default 6.0)\n";
|
|
file << "# scanline_gap_brightness: min brightness between scanlines (0.0-1.0, default 0.12)\n";
|
|
file << "# bloom_factor: brightness for bright areas (default 3.5)\n";
|
|
file << "# input_gamma: input gamma - linearization (default 2.4)\n";
|
|
file << "# output_gamma: output gamma - encoding (default 2.2)\n";
|
|
file << "# mask_brightness: sub-pixel brightness (default 0.80)\n";
|
|
file << "# curvature_x/y: barrel CRT distortion (0.0 = flat)\n";
|
|
file << "# mask_type: 0=none, 1=green/magenta, 2=RGB phosphor\n";
|
|
file << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
|
|
file << "\npresets:\n";
|
|
file << " - name: \"Default\"\n scanline_weight: 6.0\n scanline_gap_brightness: 0.12\n bloom_factor: 3.5\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 0.80\n curvature_x: 0.05\n curvature_y: 0.10\n mask_type: 2\n enable_scanlines: true\n enable_multisample: true\n enable_gamma: true\n enable_curvature: false\n enable_sharper: false\n";
|
|
file << " - name: \"Curved\"\n scanline_weight: 6.0\n scanline_gap_brightness: 0.12\n bloom_factor: 3.5\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 0.80\n curvature_x: 0.05\n curvature_y: 0.10\n mask_type: 2\n enable_scanlines: true\n enable_multisample: true\n enable_gamma: true\n enable_curvature: true\n enable_sharper: false\n";
|
|
file << " - name: \"Sharp\"\n scanline_weight: 6.0\n scanline_gap_brightness: 0.12\n bloom_factor: 3.5\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 0.80\n curvature_x: 0.05\n curvature_y: 0.10\n mask_type: 2\n enable_scanlines: true\n enable_multisample: false\n enable_gamma: true\n enable_curvature: false\n enable_sharper: true\n";
|
|
file << " - name: \"Minimal\"\n scanline_weight: 8.0\n scanline_gap_brightness: 0.05\n bloom_factor: 2.0\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 1.00\n curvature_x: 0.0\n curvature_y: 0.0\n mask_type: 0\n enable_scanlines: true\n enable_multisample: false\n enable_gamma: false\n enable_curvature: false\n enable_sharper: false\n";
|
|
file.close();
|
|
populateDefaultCrtPiPresets();
|
|
return true;
|
|
}
|
|
|
|
// Carga los presets de CrtPi desde el fichero
|
|
auto loadCrtPiFromFile() -> bool {
|
|
crtpi_presets.clear();
|
|
|
|
std::ifstream file(crtpi_file_path);
|
|
if (!file.good()) {
|
|
return saveCrtPiDefaults();
|
|
}
|
|
|
|
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
file.close();
|
|
|
|
try {
|
|
auto yaml = fkyaml::node::deserialize(content);
|
|
|
|
if (yaml.contains("presets")) {
|
|
const auto& presets = yaml["presets"];
|
|
for (const auto& p : presets) {
|
|
CrtPiPreset preset;
|
|
if (p.contains("name")) {
|
|
preset.name = p["name"].get_value<std::string>();
|
|
}
|
|
parseField(p, "scanline_weight", preset.scanline_weight);
|
|
parseField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
|
|
parseField(p, "bloom_factor", preset.bloom_factor);
|
|
parseField(p, "input_gamma", preset.input_gamma);
|
|
parseField(p, "output_gamma", preset.output_gamma);
|
|
parseField(p, "mask_brightness", preset.mask_brightness);
|
|
parseField(p, "curvature_x", preset.curvature_x);
|
|
parseField(p, "curvature_y", preset.curvature_y);
|
|
parseField(p, "mask_type", preset.mask_type);
|
|
parseField(p, "enable_scanlines", preset.enable_scanlines);
|
|
parseField(p, "enable_multisample", preset.enable_multisample);
|
|
parseField(p, "enable_gamma", preset.enable_gamma);
|
|
parseField(p, "enable_curvature", preset.enable_curvature);
|
|
parseField(p, "enable_sharper", preset.enable_sharper);
|
|
crtpi_presets.push_back(preset);
|
|
}
|
|
}
|
|
|
|
if (!crtpi_presets.empty()) {
|
|
// Resolver nombre → índice
|
|
if (!video.shader.current_crtpi_preset_name.empty()) {
|
|
for (int i = 0; i < static_cast<int>(crtpi_presets.size()); ++i) {
|
|
if (crtpi_presets[static_cast<size_t>(i)].name == video.shader.current_crtpi_preset_name) {
|
|
video.shader.current_crtpi_preset = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
video.shader.current_crtpi_preset = std::clamp(
|
|
video.shader.current_crtpi_preset,
|
|
0,
|
|
static_cast<int>(crtpi_presets.size()) - 1);
|
|
} else {
|
|
video.shader.current_crtpi_preset = 0;
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (const fkyaml::exception& e) {
|
|
std::cout << "Error parsing CrtPi YAML: " << e.what() << ". Recreating defaults." << '\n';
|
|
return saveCrtPiDefaults();
|
|
}
|
|
}
|
|
|
|
// 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"];
|
|
int zoom = window.zoom;
|
|
parseField(win, "zoom", zoom);
|
|
window.zoom = (zoom > 0) ? zoom : window.zoom;
|
|
}
|
|
|
|
void loadVideoFromYaml(const fkyaml::node& yaml) {
|
|
if (!yaml.contains("video")) { return; }
|
|
const auto& vid = yaml["video"];
|
|
|
|
parseField(vid, "fullscreen", video.fullscreen);
|
|
parseField(vid, "vsync", video.vsync);
|
|
parseField(vid, "integer_scale", video.integer_scale);
|
|
|
|
int scale_mode_int = static_cast<int>(video.scale_mode);
|
|
parseField(vid, "scale_mode", scale_mode_int);
|
|
video.scale_mode = static_cast<SDL_ScaleMode>(scale_mode_int);
|
|
|
|
if (vid.contains("gpu")) {
|
|
const auto& gpu_node = vid["gpu"];
|
|
parseField(gpu_node, "acceleration", video.gpu.acceleration);
|
|
parseField(gpu_node, "preferred_driver", video.gpu.preferred_driver);
|
|
}
|
|
|
|
if (vid.contains("shader")) {
|
|
const auto& sh = vid["shader"];
|
|
parseField(sh, "enabled", video.shader.enabled);
|
|
parseField(sh, "postfx_preset", video.shader.current_postfx_preset_name);
|
|
parseField(sh, "crtpi_preset", video.shader.current_crtpi_preset_name);
|
|
std::string shader_name;
|
|
parseField(sh, "current_shader", shader_name);
|
|
if (!shader_name.empty()) {
|
|
video.shader.current_shader = (shader_name == "crtpi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
|
|
}
|
|
}
|
|
|
|
if (vid.contains("supersampling")) {
|
|
const auto& ss_node = vid["supersampling"];
|
|
parseField(ss_node, "enabled", video.supersampling.enabled);
|
|
parseField(ss_node, "linear_upscale", video.supersampling.linear_upscale);
|
|
parseField(ss_node, "downscale_algo", video.supersampling.downscale_algo);
|
|
}
|
|
}
|
|
|
|
void loadAudioFromYaml(const fkyaml::node& yaml) {
|
|
if (!yaml.contains("audio")) { return; }
|
|
const auto& aud = yaml["audio"];
|
|
|
|
parseField(aud, "enabled", audio.enabled);
|
|
parseField(aud, "volume", audio.volume);
|
|
audio.volume = std::clamp(audio.volume, 0.0F, 1.0F);
|
|
|
|
if (aud.contains("music")) {
|
|
const auto& mus = aud["music"];
|
|
parseField(mus, "enabled", audio.music.enabled);
|
|
parseField(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"];
|
|
parseField(snd, "enabled", audio.sound.enabled);
|
|
parseField(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"];
|
|
parseField(ld, "show", loading.show);
|
|
parseField(ld, "show_resource_name", loading.show_resource_name);
|
|
parseField(ld, "wait_for_input", loading.wait_for_input);
|
|
}
|
|
|
|
void loadGameFromYaml(const fkyaml::node& yaml) {
|
|
if (!yaml.contains("game")) { return; }
|
|
const auto& game = yaml["game"];
|
|
|
|
if (game.contains("language")) {
|
|
int lang_int = static_cast<int>(settings.language);
|
|
parseField(game, "language", lang_int);
|
|
const auto LANG = static_cast<Lang::Code>(lang_int);
|
|
settings.language = (LANG == Lang::Code::ENGLISH || LANG == Lang::Code::VALENCIAN || LANG == Lang::Code::SPANISH)
|
|
? LANG
|
|
: Lang::Code::ENGLISH;
|
|
pending_changes.new_language = settings.language;
|
|
}
|
|
if (game.contains("difficulty")) {
|
|
int diff_int = static_cast<int>(settings.difficulty);
|
|
parseField(game, "difficulty", diff_int);
|
|
settings.difficulty = static_cast<Difficulty::Code>(diff_int);
|
|
pending_changes.new_difficulty = settings.difficulty;
|
|
}
|
|
parseField(game, "autofire", settings.autofire);
|
|
parseField(game, "shutdown_enabled", settings.shutdown_enabled);
|
|
parseField(game, "params_file", settings.params_file);
|
|
}
|
|
|
|
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; }
|
|
parseField(ctrl, "name", gamepad_manager[i].name);
|
|
parseField(ctrl, "path", gamepad_manager[i].path);
|
|
int player_int = 0;
|
|
parseField(ctrl, "player", player_int);
|
|
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;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
|
|
void loadKeyboardFromYaml(const fkyaml::node& yaml) {
|
|
if (!yaml.contains("keyboard")) { return; }
|
|
const auto& kb = yaml["keyboard"];
|
|
int player_int = static_cast<int>(keyboard.player_id);
|
|
parseField(kb, "player", player_int);
|
|
keyboard.player_id = static_cast<Player::Id>(player_int);
|
|
}
|
|
|
|
// Carga el fichero de configuración
|
|
auto loadFromFile() -> bool {
|
|
init();
|
|
|
|
std::ifstream file(settings.config_file);
|
|
if (!file.is_open()) {
|
|
saveToFile();
|
|
return true;
|
|
}
|
|
|
|
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
file.close();
|
|
|
|
try {
|
|
auto yaml = fkyaml::node::deserialize(content);
|
|
|
|
// Comprobar versión: si no coincide, regenerar config por defecto
|
|
int file_version = 0;
|
|
parseField(yaml, "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();
|
|
saveToFile();
|
|
return true;
|
|
}
|
|
|
|
loadWindowFromYaml(yaml);
|
|
loadVideoFromYaml(yaml);
|
|
loadAudioFromYaml(yaml);
|
|
loadLoadingFromYaml(yaml);
|
|
loadGameFromYaml(yaml);
|
|
loadControllersFromYaml(yaml);
|
|
loadKeyboardFromYaml(yaml);
|
|
|
|
} catch (const fkyaml::exception& e) {
|
|
std::cout << "Error parsing YAML config: " << e.what() << ". Using defaults." << '\n';
|
|
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()) {
|
|
std::cout << "Error: " << getFileName(settings.config_file) << " can't be opened" << '\n';
|
|
return false;
|
|
}
|
|
|
|
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<int>(video.scale_mode) << " # " << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_LINEAR) << ": linear\n";
|
|
file << " vsync: " << boolToString(video.vsync) << "\n";
|
|
file << " integer_scale: " << boolToString(video.integer_scale) << "\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";
|
|
{
|
|
std::string postfx_name = (!postfx_presets.empty() && video.shader.current_postfx_preset < static_cast<int>(postfx_presets.size()))
|
|
? postfx_presets[static_cast<size_t>(video.shader.current_postfx_preset)].name
|
|
: "";
|
|
std::string crtpi_name = (!crtpi_presets.empty() && video.shader.current_crtpi_preset < static_cast<int>(crtpi_presets.size()))
|
|
? crtpi_presets[static_cast<size_t>(video.shader.current_crtpi_preset)].name
|
|
: "";
|
|
file << " postfx_preset: \"" << postfx_name << "\"\n";
|
|
file << " crtpi_preset: \"" << crtpi_name << "\"\n";
|
|
}
|
|
file << " supersampling:\n";
|
|
file << " enabled: " << boolToString(video.supersampling.enabled) << "\n";
|
|
file << " linear_upscale: " << boolToString(video.supersampling.linear_upscale) << "\n";
|
|
file << " downscale_algo: " << video.supersampling.downscale_algo << "\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";
|
|
|
|
// 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";
|
|
file << "\n";
|
|
|
|
// GAME
|
|
file << "# GAME\n";
|
|
file << "game:\n";
|
|
file << " language: " << static_cast<int>(settings.language) << " # 0: spanish, 1: valencian, 2: english\n";
|
|
file << " difficulty: " << static_cast<int>(settings.difficulty) << " # " << static_cast<int>(Difficulty::Code::EASY) << ": easy, " << static_cast<int>(Difficulty::Code::NORMAL) << ": normal, " << static_cast<int>(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<int>(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<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.
|
|
//
|
|
// Tres pases en orden decreciente de prioridad:
|
|
// 1) Match por path exacto: resuelve el caso arcade (dos sticks idénticos
|
|
// enchufados siempre a los mismos puertos USB).
|
|
// 2) Match por nombre cuando el path no coincidió: recoloca un mando
|
|
// conocido que se ha reenchufado a otro puerto (ej. DualSense). En ese
|
|
// caso se refresca el path guardado al nuevo.
|
|
// 3) Reparto en orden de los mandos físicos sobrantes a los slots que
|
|
// sigan libres.
|
|
void GamepadManager::assignAndLinkGamepads() {
|
|
auto physical_gamepads = Input::get()->getGamepads();
|
|
|
|
std::array<std::string, MAX_PLAYERS> desired_paths;
|
|
std::array<std::string, MAX_PLAYERS> desired_names;
|
|
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
|
desired_paths[i] = gamepads_[i].path;
|
|
desired_names[i] = gamepads_[i].name;
|
|
gamepads_[i].instance = nullptr;
|
|
}
|
|
|
|
std::vector<std::shared_ptr<Input::Gamepad>> assigned_instances;
|
|
|
|
assignGamepadsByPath(desired_paths, physical_gamepads, assigned_instances);
|
|
assignGamepadsByName(desired_names, 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<std::string, MAX_PLAYERS>& desired_paths,
|
|
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
|
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;
|
|
}
|
|
|
|
const auto IT = std::ranges::find_if(physical_gamepads,
|
|
[&desired_path, &assigned_instances](const auto& pg) {
|
|
return pg->path == desired_path && !isGamepadAssigned(pg, assigned_instances);
|
|
});
|
|
if (IT != physical_gamepads.end()) {
|
|
gamepads_[i].instance = *IT;
|
|
gamepads_[i].name = (*IT)->name;
|
|
assigned_instances.push_back(*IT);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- SEGUNDA PASADA: Match por nombre para slots cuyo path guardado no casó ---
|
|
// Si un slot tiene nombre guardado pero el path no ha coincidido (mando reenchufado
|
|
// a otro puerto, o path del sistema cambiado), lo enlazamos por nombre y
|
|
// refrescamos el path guardado al del dispositivo físico actual.
|
|
void GamepadManager::assignGamepadsByName(
|
|
const std::array<std::string, MAX_PLAYERS>& desired_names,
|
|
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
|
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
|
|
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
|
if (gamepads_[i].instance != nullptr) {
|
|
continue;
|
|
}
|
|
const std::string& desired_name = desired_names[i];
|
|
if (desired_name.empty()) {
|
|
continue;
|
|
}
|
|
|
|
const auto IT = std::ranges::find_if(physical_gamepads,
|
|
[&desired_name, &assigned_instances](const auto& pg) {
|
|
return pg->name == desired_name && !isGamepadAssigned(pg, assigned_instances);
|
|
});
|
|
if (IT != physical_gamepads.end()) {
|
|
gamepads_[i].instance = *IT;
|
|
gamepads_[i].name = (*IT)->name;
|
|
gamepads_[i].path = (*IT)->path;
|
|
assigned_instances.push_back(*IT);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- TERCERA PASADA: Asigna los mandos físicos restantes a los jugadores libres ---
|
|
void GamepadManager::assignRemainingGamepads(
|
|
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
|
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
|
|
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
|
if (gamepads_[i].instance != nullptr) {
|
|
continue;
|
|
}
|
|
|
|
const auto IT = std::ranges::find_if(physical_gamepads,
|
|
[&assigned_instances](const auto& pg) {
|
|
return !isGamepadAssigned(pg, assigned_instances);
|
|
});
|
|
if (IT != physical_gamepads.end()) {
|
|
gamepads_[i].instance = *IT;
|
|
gamepads_[i].name = (*IT)->name;
|
|
gamepads_[i].path = (*IT)->path;
|
|
assigned_instances.push_back(*IT);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- LIMPIEZA FINAL: 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<Input::Gamepad>& physical_gamepad,
|
|
const std::vector<std::shared_ptr<Input::Gamepad>>& 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
|