335 lines
14 KiB
C++
335 lines
14 KiB
C++
#include "game/options.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
#include "external/fkyaml_node.hpp"
|
|
#include "utils/defines.hpp"
|
|
|
|
namespace {
|
|
|
|
auto scalingModeToString(Defaults::ScalingMode mode) -> const char* {
|
|
switch (mode) {
|
|
case Defaults::ScalingMode::DISABLED:
|
|
return "disabled";
|
|
case Defaults::ScalingMode::STRETCH:
|
|
return "stretch";
|
|
case Defaults::ScalingMode::LETTERBOX:
|
|
return "letterbox";
|
|
case Defaults::ScalingMode::OVERSCAN:
|
|
return "overscan";
|
|
case Defaults::ScalingMode::INTEGER:
|
|
return "integer";
|
|
}
|
|
return "letterbox";
|
|
}
|
|
|
|
auto scalingModeFromString(const std::string& text) -> Defaults::ScalingMode {
|
|
if (text == "disabled") { return Defaults::ScalingMode::DISABLED; }
|
|
if (text == "stretch") { return Defaults::ScalingMode::STRETCH; }
|
|
if (text == "overscan") { return Defaults::ScalingMode::OVERSCAN; }
|
|
if (text == "integer") { return Defaults::ScalingMode::INTEGER; }
|
|
return Defaults::ScalingMode::LETTERBOX;
|
|
}
|
|
|
|
auto filterToString(Defaults::TextureFilter filter) -> const char* {
|
|
return filter == Defaults::TextureFilter::LINEAR ? "linear" : "nearest";
|
|
}
|
|
|
|
auto filterFromString(const std::string& text) -> Defaults::TextureFilter {
|
|
return text == "linear" ? Defaults::TextureFilter::LINEAR : Defaults::TextureFilter::NEAREST;
|
|
}
|
|
|
|
auto readFileOrEmpty(const std::string& path) -> std::string {
|
|
std::ifstream file(path);
|
|
if (!file) { return {}; }
|
|
std::stringstream buffer;
|
|
buffer << file.rdbuf();
|
|
return buffer.str();
|
|
}
|
|
|
|
auto writeFile(const std::string& path, const std::string& content) -> bool {
|
|
std::ofstream file(path, std::ios::binary);
|
|
if (!file) { return false; }
|
|
file.write(content.data(), static_cast<std::streamsize>(content.size()));
|
|
return file.good();
|
|
}
|
|
|
|
void parseWindow(const fkyaml::node& yaml) {
|
|
if (!yaml.contains("window")) { return; }
|
|
const auto& node = yaml["window"];
|
|
if (node.contains("zoom")) { Options::window.zoom = node["zoom"].get_value<int>(); }
|
|
if (node.contains("fullscreen")) { Options::window.fullscreen = node["fullscreen"].get_value<bool>(); }
|
|
}
|
|
|
|
void parseGpu(const fkyaml::node& video_node) {
|
|
if (!video_node.contains("gpu")) { return; }
|
|
const auto& node = video_node["gpu"];
|
|
if (node.contains("acceleration")) { Options::video.gpu.acceleration = node["acceleration"].get_value<bool>(); }
|
|
if (node.contains("preferred_driver")) { Options::video.gpu.preferred_driver = node["preferred_driver"].get_value<std::string>(); }
|
|
}
|
|
|
|
void parseShader(const fkyaml::node& video_node) {
|
|
if (!video_node.contains("shader")) { return; }
|
|
const auto& node = video_node["shader"];
|
|
if (node.contains("enabled")) { Options::video.shader.enabled = node["enabled"].get_value<bool>(); }
|
|
if (node.contains("current_postfx_preset")) {
|
|
Options::video.shader.current_postfx_preset = node["current_postfx_preset"].get_value<std::string>();
|
|
}
|
|
}
|
|
|
|
void parseVideo(const fkyaml::node& yaml) {
|
|
if (!yaml.contains("video")) { return; }
|
|
const auto& node = yaml["video"];
|
|
if (node.contains("vsync")) { Options::video.vsync = node["vsync"].get_value<bool>(); }
|
|
if (node.contains("scaling_mode")) {
|
|
Options::video.scaling_mode = scalingModeFromString(node["scaling_mode"].get_value<std::string>());
|
|
}
|
|
if (node.contains("texture_filter")) {
|
|
Options::video.texture_filter = filterFromString(node["texture_filter"].get_value<std::string>());
|
|
}
|
|
parseGpu(node);
|
|
parseShader(node);
|
|
}
|
|
|
|
void parseMusic(const fkyaml::node& audio_node) {
|
|
if (!audio_node.contains("music")) { return; }
|
|
const auto& node = audio_node["music"];
|
|
if (node.contains("enabled")) { Options::audio.music.enabled = node["enabled"].get_value<bool>(); }
|
|
if (node.contains("volume")) { Options::audio.music.volume = node["volume"].get_value<float>(); }
|
|
}
|
|
|
|
void parseSound(const fkyaml::node& audio_node) {
|
|
if (!audio_node.contains("sound")) { return; }
|
|
const auto& node = audio_node["sound"];
|
|
if (node.contains("enabled")) { Options::audio.sound.enabled = node["enabled"].get_value<bool>(); }
|
|
if (node.contains("volume")) { Options::audio.sound.volume = node["volume"].get_value<float>(); }
|
|
}
|
|
|
|
void parseAudio(const fkyaml::node& yaml) {
|
|
if (!yaml.contains("audio")) { return; }
|
|
const auto& node = yaml["audio"];
|
|
if (node.contains("enabled")) { Options::audio.enabled = node["enabled"].get_value<bool>(); }
|
|
if (node.contains("volume")) { Options::audio.volume = node["volume"].get_value<float>(); }
|
|
parseMusic(node);
|
|
parseSound(node);
|
|
}
|
|
|
|
// Presets per defecte de PostFX, usats quan el fitxer postfx.yaml no existeix.
|
|
auto defaultPostFXPresets() -> std::vector<Options::PostFXPreset> {
|
|
return {
|
|
{.name = "off"}, // passthrough (tot a 0)
|
|
{.name = "crt",
|
|
.vignette = 0.6F,
|
|
.scanlines = 0.7F,
|
|
.chroma = 0.15F,
|
|
.mask = 0.6F,
|
|
.gamma = 0.8F,
|
|
.curvature = 0.0F,
|
|
.bleeding = 0.0F,
|
|
.flicker = 0.0F},
|
|
{.name = "ntsc",
|
|
.vignette = 0.4F,
|
|
.scanlines = 0.5F,
|
|
.chroma = 0.25F,
|
|
.mask = 0.3F,
|
|
.gamma = 0.6F,
|
|
.curvature = 0.0F,
|
|
.bleeding = 0.4F,
|
|
.flicker = 0.1F},
|
|
};
|
|
}
|
|
|
|
void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) {
|
|
if (node.contains("name")) { preset.name = node["name"].get_value<std::string>(); }
|
|
if (node.contains("vignette")) { preset.vignette = node["vignette"].get_value<float>(); }
|
|
if (node.contains("scanlines")) { preset.scanlines = node["scanlines"].get_value<float>(); }
|
|
if (node.contains("chroma")) { preset.chroma = node["chroma"].get_value<float>(); }
|
|
if (node.contains("mask")) { preset.mask = node["mask"].get_value<float>(); }
|
|
if (node.contains("gamma")) { preset.gamma = node["gamma"].get_value<float>(); }
|
|
if (node.contains("curvature")) { preset.curvature = node["curvature"].get_value<float>(); }
|
|
if (node.contains("bleeding")) { preset.bleeding = node["bleeding"].get_value<float>(); }
|
|
if (node.contains("flicker")) { preset.flicker = node["flicker"].get_value<float>(); }
|
|
}
|
|
|
|
void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) {
|
|
out << " - name: \"" << preset.name << "\"\n";
|
|
out << " vignette: " << preset.vignette << "\n";
|
|
out << " scanlines: " << preset.scanlines << "\n";
|
|
out << " chroma: " << preset.chroma << "\n";
|
|
out << " mask: " << preset.mask << "\n";
|
|
out << " gamma: " << preset.gamma << "\n";
|
|
out << " curvature: " << preset.curvature << "\n";
|
|
out << " bleeding: " << preset.bleeding << "\n";
|
|
out << " flicker: " << preset.flicker << "\n";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace Options {
|
|
|
|
void setConfigFile(const std::string& path) {
|
|
config_file_path = path;
|
|
}
|
|
|
|
auto loadFromFile() -> bool {
|
|
if (config_file_path.empty()) { return false; }
|
|
|
|
const std::string CONTENT = readFileOrEmpty(config_file_path);
|
|
if (CONTENT.empty()) {
|
|
// Primera execució: creem el fitxer amb els valors per defecte.
|
|
return saveToFile();
|
|
}
|
|
|
|
try {
|
|
auto yaml = fkyaml::node::deserialize(CONTENT);
|
|
|
|
std::string file_version;
|
|
if (yaml.contains("version")) {
|
|
file_version = yaml["version"].get_value<std::string>();
|
|
}
|
|
// Sense migració per ara: si la versió no coincideix, reescrivim
|
|
// amb els defaults de la versió nova.
|
|
if (file_version != Texts::VERSION) { return saveToFile(); }
|
|
config_version = file_version;
|
|
|
|
parseWindow(yaml);
|
|
parseVideo(yaml);
|
|
parseAudio(yaml);
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
|
std::cerr << "Options: reescrivint el fitxer amb defaults.\n";
|
|
return saveToFile();
|
|
}
|
|
}
|
|
|
|
auto saveToFile() -> bool {
|
|
if (config_file_path.empty()) { return false; }
|
|
|
|
config_version = Texts::VERSION;
|
|
|
|
std::ostringstream out;
|
|
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
|
|
out << "version: \"" << config_version << "\"\n\n";
|
|
|
|
out << "window:\n";
|
|
out << " zoom: " << window.zoom << "\n";
|
|
out << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n\n";
|
|
|
|
out << "video:\n";
|
|
out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
|
out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n";
|
|
out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n";
|
|
out << " gpu:\n";
|
|
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
|
|
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
|
|
out << " shader:\n";
|
|
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
|
|
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n\n";
|
|
|
|
out << "audio:\n";
|
|
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
|
out << " volume: " << audio.volume << "\n";
|
|
out << " music:\n";
|
|
out << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n";
|
|
out << " volume: " << audio.music.volume << "\n";
|
|
out << " sound:\n";
|
|
out << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
|
|
out << " volume: " << audio.sound.volume << "\n";
|
|
|
|
return writeFile(config_file_path, out.str());
|
|
}
|
|
|
|
auto buildScreenConfig() -> Screen::Config {
|
|
Screen::Config config;
|
|
config.zoom = window.zoom;
|
|
config.fullscreen = window.fullscreen;
|
|
config.vsync = video.vsync;
|
|
config.scaling_mode = video.scaling_mode;
|
|
config.texture_filter = video.texture_filter;
|
|
config.gpu_acceleration = video.gpu.acceleration;
|
|
config.preferred_driver = video.gpu.preferred_driver;
|
|
return config;
|
|
}
|
|
|
|
void setPostFXFile(const std::string& path) {
|
|
postfx_file_path = path;
|
|
}
|
|
|
|
auto loadPostFXFromFile() -> bool {
|
|
if (postfx_file_path.empty()) { return false; }
|
|
|
|
const std::string CONTENT = readFileOrEmpty(postfx_file_path);
|
|
if (CONTENT.empty()) {
|
|
postfx_presets = defaultPostFXPresets();
|
|
return savePostFXToFile();
|
|
}
|
|
|
|
try {
|
|
auto yaml = fkyaml::node::deserialize(CONTENT);
|
|
if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) {
|
|
postfx_presets = defaultPostFXPresets();
|
|
return savePostFXToFile();
|
|
}
|
|
|
|
postfx_presets.clear();
|
|
for (const auto& preset_node : yaml["presets"]) {
|
|
PostFXPreset preset;
|
|
parsePostFXPreset(preset_node, preset);
|
|
postfx_presets.push_back(preset);
|
|
}
|
|
|
|
if (postfx_presets.empty()) {
|
|
postfx_presets = defaultPostFXPresets();
|
|
return savePostFXToFile();
|
|
}
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Options: error parsejant postfx.yaml: " << e.what() << '\n';
|
|
std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n";
|
|
postfx_presets = defaultPostFXPresets();
|
|
return savePostFXToFile();
|
|
}
|
|
}
|
|
|
|
auto savePostFXToFile() -> bool {
|
|
if (postfx_file_path.empty()) { return false; }
|
|
|
|
std::ostringstream out;
|
|
out << "# Presets PostFX per a AEE 2026. Editable a mà.\n";
|
|
out << "# Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.\n";
|
|
out << "presets:\n";
|
|
for (const auto& preset : postfx_presets) {
|
|
writePostFXPreset(out, preset);
|
|
}
|
|
return writeFile(postfx_file_path, out.str());
|
|
}
|
|
|
|
auto buildPostFXParams() -> Rendering::PostFXParams {
|
|
// Si el shader està deshabilitat, retornem params a zero → passthrough.
|
|
if (!video.shader.enabled) { return {}; }
|
|
|
|
const auto IT = std::ranges::find_if(postfx_presets,
|
|
[](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; });
|
|
if (IT == postfx_presets.end()) {
|
|
std::cerr << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
|
return {};
|
|
}
|
|
return {
|
|
.vignette = IT->vignette,
|
|
.scanlines = IT->scanlines,
|
|
.chroma = IT->chroma,
|
|
.mask = IT->mask,
|
|
.gamma = IT->gamma,
|
|
.curvature = IT->curvature,
|
|
.bleeding = IT->bleeding,
|
|
.flicker = IT->flicker,
|
|
};
|
|
}
|
|
|
|
} // namespace Options
|