Files
coffee-crisis/source/game/options.cpp
T

549 lines
24 KiB
C++

#include "game/options.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <string>
#include "core/input/input.h" // for Input::Device::KEYBOARD, Input::Device::GAMECONTROLLER
#include "core/locale/lang.h" // for Lang::Code, Lang::MAX_LANGUAGES
#include "external/fkyaml_node.hpp" // for fkyaml::node
#include "utils/utils.h" // for boolToString
namespace Options {
// --- Variables globales ---
Window window;
Video video;
Audio audio;
Loading loading;
Settings settings;
Gameplay gameplay;
std::vector<InputDevice> inputs;
std::vector<PostFXPreset> postfx_presets;
std::string postfx_file_path;
int current_postfx_preset = 0;
std::vector<CrtPiPreset> crtpi_presets;
std::string crtpi_file_path;
int current_crtpi_preset = 0;
// Lectura tolerant d'un camp YAML: assigna a `target` el valor del camp
// si existeix i el tipus encaixa. Si la clau no hi és o el tipus YAML
// no és compatible amb T, conserva el valor previ de `target` (default).
// Retorna true si s'ha llegit, false si s'ha conservat el default.
template <typename T>
auto tryGet(const fkyaml::node &node, const std::string &key, T &target) -> bool {
if (!node.contains(key)) {
return false;
}
try {
target = node[key].get_value<T>();
return true;
} catch (...) {
// @INTENTIONAL: valor YAML amb tipus incompatible per a T. La
// política del loader és conservar el default existent en
// `target` enlloc d'avortar la càrrega de la resta del fitxer.
return false;
}
}
// --- Helpers locals ---
namespace {
void parseBoolField(const fkyaml::node &node, const std::string &key, bool &target) {
tryGet<bool>(node, key, target);
}
void parseIntField(const fkyaml::node &node, const std::string &key, int &target) {
tryGet<int>(node, key, target);
}
void parseStringField(const fkyaml::node &node, const std::string &key, std::string &target) {
tryGet<std::string>(node, key, target);
}
void loadWindowFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("window")) { return; }
const auto &win = yaml["window"];
parseIntField(win, "zoom", window.zoom);
// El bound superior s'aplica més tard a `Screen::detectMaxZoom()`
// un cop sabem la resolució real del display.
if (window.zoom < 1) {
window.zoom = Defaults::Window::ZOOM;
}
}
void loadVideoFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("video")) { return; }
const auto &vid = yaml["video"];
parseBoolField(vid, "fullscreen", video.fullscreen);
parseBoolField(vid, "vsync", video.vsync);
parseBoolField(vid, "integer_scale", video.integer_scale);
int scale_mode_int = static_cast<int>(video.scale_mode);
if (tryGet<int>(vid, "scale_mode", scale_mode_int)) {
video.scale_mode = static_cast<SDL_ScaleMode>(scale_mode_int);
}
if (vid.contains("gpu")) {
const auto &gpu = vid["gpu"];
parseBoolField(gpu, "acceleration", video.gpu.acceleration);
parseStringField(gpu, "preferred_driver", video.gpu.preferred_driver);
}
if (vid.contains("shader")) {
const auto &sh = vid["shader"];
parseBoolField(sh, "enabled", video.shader.enabled);
std::string shader_kind;
if (tryGet<std::string>(sh, "current_shader", shader_kind)) {
video.shader.current_shader = (shader_kind == "crtpi" || shader_kind == "CRTPI")
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
}
parseStringField(sh, "current_postfx_preset", video.shader.current_postfx_preset_name);
parseStringField(sh, "current_crtpi_preset", video.shader.current_crtpi_preset_name);
}
}
void loadAudioFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("audio")) { return; }
const auto &aud = yaml["audio"];
parseBoolField(aud, "enabled", audio.enabled);
if (tryGet<float>(aud, "volume", audio.volume)) {
audio.volume = std::clamp(audio.volume, 0.0F, 1.0F);
}
if (aud.contains("music")) {
const auto &mus = aud["music"];
parseBoolField(mus, "enabled", audio.music.enabled);
if (tryGet<float>(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"];
parseBoolField(snd, "enabled", audio.sound.enabled);
if (tryGet<float>(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"];
parseBoolField(ld, "show", loading.show);
parseBoolField(ld, "show_resource_name", loading.show_resource_name);
parseBoolField(ld, "wait_for_input", loading.wait_for_input);
}
void loadSettingsFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("settings")) { return; }
const auto &st = yaml["settings"];
parseIntField(st, "difficulty", settings.difficulty);
int language_int = static_cast<int>(settings.language);
parseIntField(st, "language", language_int);
if (language_int < 0 || language_int >= Lang::MAX_LANGUAGES) {
language_int = static_cast<int>(Lang::Code::EN_UK);
}
settings.language = static_cast<Lang::Code>(language_int);
parseIntField(st, "player_selected", settings.player_selected);
}
void loadGameplayFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("gameplay")) { return; }
const auto &gp = yaml["gameplay"];
parseBoolField(gp, "pause_countdown", gameplay.pause_countdown);
}
void loadInputsFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("input") || inputs.size() < 2) { return; }
const auto &ins = yaml["input"];
size_t i = 0;
for (const auto &entry : ins) {
if (i >= inputs.size()) { break; }
int device_type_int = static_cast<int>(inputs[i].device_type);
if (tryGet<int>(entry, "device_type", device_type_int)) {
inputs[i].device_type = static_cast<Input::Device>(device_type_int);
}
++i;
}
}
} // namespace
// --- Funciones públiques ---
void setConfigFile(const std::string &file_path) {
settings.config_file = file_path;
}
void init() {
// Reinicia structs a defaults (els member-initializers ho fan sols).
window = Window{};
video = Video{};
audio = Audio{};
loading = Loading{};
gameplay = Gameplay{};
// Preserva config_file si ja s'ha establert abans.
const std::string PREV_CONFIG_FILE = settings.config_file;
settings = Settings{};
settings.config_file = PREV_CONFIG_FILE;
#ifdef __EMSCRIPTEN__
// En Emscripten la ventana la gestiona el navegador
window.zoom = 4;
video.fullscreen = false;
video.integer_scale = true;
#endif
// Dispositius d'entrada per defecte
inputs.clear();
InputDevice kb;
kb.id = 0;
kb.name = "KEYBOARD";
kb.device_type = Input::Device::KEYBOARD;
inputs.push_back(kb);
InputDevice gc;
gc.id = 0;
gc.name = "GAME CONTROLLER";
gc.device_type = Input::Device::GAMECONTROLLER;
inputs.push_back(gc);
}
auto loadFromFile() -> bool {
init();
std::ifstream file(settings.config_file);
if (!file.is_open()) {
// Primera execució: crea el YAML amb defaults.
return saveToFile();
}
const std::string CONTENT((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
int file_version = 0;
tryGet<int>(yaml, "config_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();
return saveToFile();
}
loadWindowFromYaml(yaml);
loadVideoFromYaml(yaml);
loadAudioFromYaml(yaml);
loadLoadingFromYaml(yaml);
loadSettingsFromYaml(yaml);
loadGameplayFromYaml(yaml);
loadInputsFromYaml(yaml);
} catch (const fkyaml::exception &e) {
std::cout << "Error parsing YAML config: " << e.what() << ". Using defaults.\n";
init();
return saveToFile();
}
return true;
}
auto saveToFile() -> bool {
if (settings.config_file.empty()) { return false; }
std::ofstream file(settings.config_file);
if (!file.is_open()) {
std::cout << "Error: " << settings.config_file << " can't be opened for writing\n";
return false;
}
file << "# Coffee Crisis - Configuration file\n";
file << "# Auto-generated, managed by the game.\n\n";
file << "config_version: " << settings.config_version << "\n\n";
// WINDOW
// `max_zoom` no es guarda — es deriva del display a cada arranc via
// `Screen::detectMaxZoom()`.
file << "# WINDOW\n";
file << "window:\n";
file << " zoom: " << window.zoom << "\n\n";
// VIDEO
file << "# VIDEO\n";
file << "video:\n";
file << " fullscreen: " << boolToString(video.fullscreen) << "\n";
file << " vsync: " << boolToString(video.vsync) << "\n";
file << " integer_scale: " << boolToString(video.integer_scale) << "\n";
file << " scale_mode: " << static_cast<int>(video.scale_mode)
<< " # " << static_cast<int>(SDL_SCALEMODE_NEAREST) << ": nearest, "
<< static_cast<int>(SDL_SCALEMODE_LINEAR) << ": linear\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";
file << " current_postfx_preset: \"" << video.shader.current_postfx_preset_name << "\"\n";
file << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset_name << "\"\n\n";
// AUDIO
file << "# AUDIO (volume range: 0.0..1.0)\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\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\n";
// SETTINGS
file << "# SETTINGS\n";
file << "settings:\n";
file << " difficulty: " << settings.difficulty << "\n";
file << " language: " << static_cast<int>(settings.language) << "\n";
file << " player_selected: " << settings.player_selected << "\n\n";
// GAMEPLAY
file << "# GAMEPLAY\n";
file << "gameplay:\n";
file << " pause_countdown: " << boolToString(gameplay.pause_countdown) << "\n\n";
// INPUT
file << "# INPUT DEVICES (device_type: "
<< static_cast<int>(Input::Device::KEYBOARD) << "=KEYBOARD, "
<< static_cast<int>(Input::Device::GAMECONTROLLER) << "=GAMECONTROLLER)\n";
file << "input:\n";
for (size_t i = 0; i < inputs.size(); ++i) {
file << " - slot: " << i << "\n";
file << " device_type: " << static_cast<int>(inputs[i].device_type) << "\n";
}
file.close();
return true;
}
// ========================================================================
// Presets de shaders (postfx.yaml / crtpi.yaml)
//
// Els defaults viuen en una única font (defaultPostFXPresets / defaultCrtPiPresets).
// Generem el YAML a partir d'ells el primer cop i els usem també com a
// fallback si el YAML és absent o corrupte. Si algú toca els valors, ho fa
// en un sol lloc.
// ========================================================================
namespace {
void parseFloatField(const fkyaml::node &node, const std::string &key, float &target) {
tryGet<float>(node, key, target);
}
auto defaultPostFXPresets() -> const std::vector<PostFXPreset> & {
static const std::vector<PostFXPreset> DEFAULTS = {
{.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F},
{.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma_min = 0.2F, .chroma_max = 0.2F, .mask = 0.4F, .gamma = 0.5F, .curvature = 0.0F, .bleeding = 0.6F},
{.name = "CURVED", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.1F, .mask = 0.5F, .gamma = 0.7F, .curvature = 0.8F},
{.name = "SCANLINES", .vignette = 0.0F, .scanlines = 0.8F},
{.name = "SUBTLE", .vignette = 0.3F, .scanlines = 0.4F, .chroma_min = 0.05F, .chroma_max = 0.05F, .mask = 0.0F, .gamma = 0.3F},
{.name = "CRT LIVE", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.3F, .mask = 0.3F, .gamma = 0.4F, .curvature = 0.3F, .bleeding = 0.4F, .flicker = 0.8F},
};
return DEFAULTS;
}
auto defaultCrtPiPresets() -> const std::vector<CrtPiPreset> & {
static const std::vector<CrtPiPreset> DEFAULTS = {
{.name = "Default", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = false, .enable_sharper = false},
{.name = "Curved", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = true, .enable_sharper = false},
{.name = "Sharp", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = true, .enable_curvature = false, .enable_sharper = true},
{.name = "Minimal", .scanline_weight = 8.0F, .scanline_gap_brightness = 0.05F, .bloom_factor = 2.0F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 1.00F, .curvature_x = 0.0F, .curvature_y = 0.0F, .mask_type = 0, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = false, .enable_curvature = false, .enable_sharper = false},
};
return DEFAULTS;
}
void writePostFXDefaults(std::ostream &out) {
out << "# Coffee Crisis - PostFX Shader Presets\n\n";
out << "presets:\n";
for (const auto &p : defaultPostFXPresets()) {
out << " - name: \"" << p.name << "\"\n";
out << " vignette: " << p.vignette << "\n";
out << " scanlines: " << p.scanlines << "\n";
out << " chroma_min: " << p.chroma_min << "\n";
out << " chroma_max: " << p.chroma_max << "\n";
out << " mask: " << p.mask << "\n";
out << " gamma: " << p.gamma << "\n";
out << " curvature: " << p.curvature << "\n";
out << " bleeding: " << p.bleeding << "\n";
out << " flicker: " << p.flicker << "\n";
out << " scan_dark_ratio: " << p.scan_dark_ratio << "\n";
out << " scan_dark_floor: " << p.scan_dark_floor << "\n";
out << " scan_edge_soft: " << p.scan_edge_soft << "\n";
}
}
void writeCrtPiDefaults(std::ostream &out) {
out << "# Coffee Crisis - CrtPi Shader Presets\n\n";
out << "presets:\n";
for (const auto &p : defaultCrtPiPresets()) {
out << " - name: \"" << p.name << "\"\n";
out << " scanline_weight: " << p.scanline_weight << "\n";
out << " scanline_gap_brightness: " << p.scanline_gap_brightness << "\n";
out << " bloom_factor: " << p.bloom_factor << "\n";
out << " input_gamma: " << p.input_gamma << "\n";
out << " output_gamma: " << p.output_gamma << "\n";
out << " mask_brightness: " << p.mask_brightness << "\n";
out << " curvature_x: " << p.curvature_x << "\n";
out << " curvature_y: " << p.curvature_y << "\n";
out << " mask_type: " << p.mask_type << "\n";
out << " enable_scanlines: " << boolToString(p.enable_scanlines) << "\n";
out << " enable_multisample: " << boolToString(p.enable_multisample) << "\n";
out << " enable_gamma: " << boolToString(p.enable_gamma) << "\n";
out << " enable_curvature: " << boolToString(p.enable_curvature) << "\n";
out << " enable_sharper: " << boolToString(p.enable_sharper) << "\n";
}
}
} // namespace
void setPostFXFile(const std::string &path) {
postfx_file_path = path;
}
auto loadPostFXFromFile() -> bool {
postfx_presets.clear();
current_postfx_preset = 0;
std::ifstream file(postfx_file_path);
if (!file.is_open()) {
// No existeix: escriu el YAML a partir dels defaults i copia'ls a memòria.
std::ofstream out(postfx_file_path);
if (out.is_open()) {
writePostFXDefaults(out);
out.close();
}
postfx_presets = defaultPostFXPresets();
return true;
}
const 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")) {
for (const auto &p : yaml["presets"]) {
PostFXPreset preset;
tryGet<std::string>(p, "name", preset.name);
parseFloatField(p, "vignette", preset.vignette);
parseFloatField(p, "scanlines", preset.scanlines);
// Compatibilitat: si només hi ha "chroma", l'usem per a min i max.
float legacy_chroma = -1.0F;
if (tryGet<float>(p, "chroma", legacy_chroma)) {
preset.chroma_min = legacy_chroma;
preset.chroma_max = legacy_chroma;
}
parseFloatField(p, "chroma_min", preset.chroma_min);
parseFloatField(p, "chroma_max", preset.chroma_max);
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);
parseFloatField(p, "scan_dark_ratio", preset.scan_dark_ratio);
parseFloatField(p, "scan_dark_floor", preset.scan_dark_floor);
parseFloatField(p, "scan_edge_soft", preset.scan_edge_soft);
postfx_presets.push_back(preset);
}
}
std::cout << "PostFX loaded: " << postfx_presets.size() << " preset(s)\n";
} catch (const fkyaml::exception &e) {
std::cout << "Error parsing PostFX YAML: " << e.what() << ". Using defaults.\n";
postfx_presets = defaultPostFXPresets();
return false;
}
if (postfx_presets.empty()) {
postfx_presets = defaultPostFXPresets();
}
return true;
}
void setCrtPiFile(const std::string &path) {
crtpi_file_path = path;
}
auto loadCrtPiFromFile() -> bool {
crtpi_presets.clear();
current_crtpi_preset = 0;
std::ifstream file(crtpi_file_path);
if (!file.is_open()) {
std::ofstream out(crtpi_file_path);
if (out.is_open()) {
writeCrtPiDefaults(out);
out.close();
}
crtpi_presets = defaultCrtPiPresets();
return true;
}
const 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")) {
for (const auto &p : yaml["presets"]) {
CrtPiPreset preset;
tryGet<std::string>(p, "name", preset.name);
parseFloatField(p, "scanline_weight", preset.scanline_weight);
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
parseFloatField(p, "bloom_factor", preset.bloom_factor);
parseFloatField(p, "input_gamma", preset.input_gamma);
parseFloatField(p, "output_gamma", preset.output_gamma);
parseFloatField(p, "mask_brightness", preset.mask_brightness);
parseFloatField(p, "curvature_x", preset.curvature_x);
parseFloatField(p, "curvature_y", preset.curvature_y);
tryGet<int>(p, "mask_type", preset.mask_type);
tryGet<bool>(p, "enable_scanlines", preset.enable_scanlines);
tryGet<bool>(p, "enable_multisample", preset.enable_multisample);
tryGet<bool>(p, "enable_gamma", preset.enable_gamma);
tryGet<bool>(p, "enable_curvature", preset.enable_curvature);
tryGet<bool>(p, "enable_sharper", preset.enable_sharper);
crtpi_presets.push_back(preset);
}
}
std::cout << "CrtPi loaded: " << crtpi_presets.size() << " preset(s)\n";
} catch (const fkyaml::exception &e) {
std::cout << "Error parsing CrtPi YAML: " << e.what() << ". Using defaults.\n";
crtpi_presets = defaultCrtPiPresets();
return false;
}
if (crtpi_presets.empty()) {
crtpi_presets = defaultCrtPiPresets();
}
return true;
}
} // namespace Options