Files
aee-arcade/source/game/options.cpp
T
JailDesignerandClaude Opus 4.7 c8882a3f6e refactor(log): migra els subsistemes restants i elimina iostream
Acaba la migració dels std::cout/cerr a Log::*:

- core/locale, core/ui/ui_config, core/rendering/{screen,text,gif,
  surface, sprite/animated_sprite, sdl3gpu/*}.
- game/{options, debug_config, editor/editor_config, levels/level_manager,
  map/{map,tilemap,paint_layer}, entities/entity_manager_loadlevel}.

Severitats coherents: warns per a configs absents amb fallback,
errors per a YAML malformat o E/S fallida, infos per a estats.

Resultat:
- 0 occurrences de std::cout/std::cerr a source/.
- Cap #include <iostream> orfà (eliminat de surface.cpp).
- Output funcional idèntic al històric; AEE_LOG filtra (ex.
  `AEE_LOG=warn` només mostra warnings, `AEE_LOG=*:warn,Tag:info`
  combina globals i overrides).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:34:24 +02:00

892 lines
37 KiB
C++

#include "game/options.hpp"
#include <algorithm>
#include <array>
#include <fstream>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include "core/input/gamepad_manager.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defines.hpp"
#include "utils/log.hpp"
namespace {
// --- Estat mutable (privat al TU) ---
// Encapsulat ací per impedir mutacions directes des de fora del namespace
// Options:: (decisió COUP-03 / docs/audit-game.md). Els callers llegeixen
// via getters const-ref i muten via setters específics que persisteixen.
std::string g_config_version;
std::string g_config_file_path;
std::string g_postfx_file_path;
std::string g_crtpi_file_path;
std::string g_locale{Defaults::I18n::LOCALE};
Options::Window g_window{};
Options::Video g_video{};
Options::Audio g_audio{};
Options::Controls g_controls{};
std::vector<Options::PostFXPreset> g_postfx_presets{};
std::vector<Options::CrtPiPreset> g_crtpi_presets{};
// Taules enum ↔ string per a la serialització a YAML. La primera entrada
// és el fallback quan l'enum no té nom conegut o el text no casa amb
// cap entrada coneguda — per això `LETTERBOX`/`NEAREST`/`OFF` encapçalen.
constexpr std::array SCALING_MODE_NAMES = {
std::pair{Defaults::ScalingMode::LETTERBOX, std::string_view{"letterbox"}},
std::pair{Defaults::ScalingMode::DISABLED, std::string_view{"disabled"}},
std::pair{Defaults::ScalingMode::STRETCH, std::string_view{"stretch"}},
std::pair{Defaults::ScalingMode::OVERSCAN, std::string_view{"overscan"}},
std::pair{Defaults::ScalingMode::INTEGER, std::string_view{"integer"}},
};
constexpr std::array TEXTURE_FILTER_NAMES = {
std::pair{Defaults::TextureFilter::NEAREST, std::string_view{"nearest"}},
std::pair{Defaults::TextureFilter::LINEAR, std::string_view{"linear"}},
};
constexpr std::array RENDER_INFO_POSITION_NAMES = {
std::pair{Defaults::RenderInfoPosition::OFF, std::string_view{"off"}},
std::pair{Defaults::RenderInfoPosition::TOP, std::string_view{"top"}},
std::pair{Defaults::RenderInfoPosition::BOTTOM, std::string_view{"bottom"}},
};
// Helpers generics: el fallback és la primera entrada de la taula.
template <typename Enum, std::size_t N>
auto enumToString(const std::array<std::pair<Enum, std::string_view>, N>& table, Enum value) -> const char* {
for (const auto& [e, name] : table) {
if (e == value) { return name.data(); }
}
return table.front().second.data();
}
template <typename Enum, std::size_t N>
auto enumFromString(const std::array<std::pair<Enum, std::string_view>, N>& table, const std::string& text) -> Enum {
for (const auto& [e, name] : table) {
if (name == text) { return e; }
}
return table.front().first;
}
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();
}
// Helper trivial per a llegir un camp optional del YAML: si existeix,
// sobreescriu `dst`; si no, el deixa com estava (valor per defecte).
// Centralitza el patró `if (node.contains("x")) { x = node["x"].get_value<T>(); }`.
template <typename T>
void readField(const fkyaml::node& node, const char* key, T& dst) {
if (node.contains(key)) { dst = node[key].get_value<T>(); }
}
// Còpia `<path>` a `<path>.bak` per protegir ediciones manuals de
// l'usuari quan estem a punt de reescriure el fitxer amb defaults per
// error de parseig o canvi de versió. Si el fitxer no existeix (primera
// execució) o no es pot obrir, no fa res: no hi ha res a salvar.
void backupFile(const std::string& path) {
if (path.empty()) { return; }
std::ifstream src(path, std::ios::binary);
if (!src) { return; }
std::ofstream dst(path + ".bak", std::ios::binary);
if (!dst) { return; }
dst << src.rdbuf();
Log::info("Options", "còpia de seguretat a {}.bak", path);
}
void parseWindow(const fkyaml::node& yaml) {
if (!yaml.contains("window")) { return; }
const auto& node = yaml["window"];
readField(node, "zoom", g_window.zoom);
readField(node, "fullscreen", g_window.fullscreen);
}
void parseGpu(const fkyaml::node& video_node) {
if (!video_node.contains("gpu")) { return; }
const auto& node = video_node["gpu"];
readField(node, "acceleration", g_video.gpu.acceleration);
readField(node, "preferred_driver", g_video.gpu.preferred_driver);
}
void parseShader(const fkyaml::node& video_node) {
if (!video_node.contains("shader")) { return; }
const auto& node = video_node["shader"];
readField(node, "enabled", g_video.shader.enabled);
readField(node, "current_shader", g_video.shader.current_shader);
readField(node, "current_postfx_preset", g_video.shader.current_postfx_preset);
readField(node, "current_crtpi_preset", g_video.shader.current_crtpi_preset);
}
void parseVideo(const fkyaml::node& yaml) {
if (!yaml.contains("video")) { return; }
const auto& node = yaml["video"];
readField(node, "vsync", g_video.vsync);
if (node.contains("scaling_mode")) {
g_video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value<std::string>());
}
if (node.contains("texture_filter")) {
g_video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value<std::string>());
}
if (node.contains("render_info_position")) {
g_video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>());
}
readField(node, "render_info_show_time", g_video.render_info_show_time);
parseGpu(node);
parseShader(node);
}
void parseMusic(const fkyaml::node& audio_node) {
if (!audio_node.contains("music")) { return; }
const auto& node = audio_node["music"];
readField(node, "enabled", g_audio.music.enabled);
readField(node, "volume", g_audio.music.volume);
}
void parseSound(const fkyaml::node& audio_node) {
if (!audio_node.contains("sound")) { return; }
const auto& node = audio_node["sound"];
readField(node, "enabled", g_audio.sound.enabled);
readField(node, "volume", g_audio.sound.volume);
}
void parseGamepadAssignment(const fkyaml::node& parent, const char* key, Options::GamepadAssignment& dst) {
if (!parent.contains(key)) { return; }
const auto& node = parent[key];
readField(node, "name", dst.name);
readField(node, "path", dst.path);
}
// Llegeix una tecla del YAML pel nom (com `SDL_GetScancodeName`). Si la
// clau no hi és o és invàlida, deixa `dst` intacte per preservar el
// default. Replica la política d'AEE (`KeyConfig::applyOverrides`).
void readScancodeField(const fkyaml::node& node, const char* key, SDL_Scancode& dst) {
if (!node.contains(key)) { return; }
const auto NAME = node[key].get_value<std::string>();
const SDL_Scancode SC = SDL_GetScancodeFromName(NAME.c_str());
if (SC != SDL_SCANCODE_UNKNOWN) { dst = SC; }
}
void parseKeyboard(const fkyaml::node& parent, const char* key, Options::Keyboard& dst) {
if (!parent.contains(key)) { return; }
const auto& node = parent[key];
readScancodeField(node, "up", dst.up);
readScancodeField(node, "down", dst.down);
readScancodeField(node, "left", dst.left);
readScancodeField(node, "right", dst.right);
readScancodeField(node, "jump", dst.jump);
readScancodeField(node, "attack", dst.attack);
}
void parseControls(const fkyaml::node& yaml) {
if (!yaml.contains("controls")) { return; }
const auto& node = yaml["controls"];
parseGamepadAssignment(node, "gamepad_p1", g_controls.gamepad_p1);
parseGamepadAssignment(node, "gamepad_p2", g_controls.gamepad_p2);
parseKeyboard(node, "keyboard_p1", g_controls.keyboard_p1);
parseKeyboard(node, "keyboard_p2", g_controls.keyboard_p2);
}
// Escriu una tecla com a string del nom SDL ("Up", "W", ...). Si
// l'scancode és UNKNOWN o sense nom registrat, escriu "" — no en farem
// override en càrrega gràcies a `readScancodeField`.
void writeKeyboard(std::ostringstream& out, const char* key, const Options::Keyboard& kb) {
const auto NAME_OR_EMPTY = [](SDL_Scancode sc) -> std::string {
const char* n = SDL_GetScancodeName(sc);
return (n != nullptr) ? std::string{n} : std::string{};
};
out << " " << key << ":\n";
out << " up: \"" << NAME_OR_EMPTY(kb.up) << "\"\n";
out << " down: \"" << NAME_OR_EMPTY(kb.down) << "\"\n";
out << " left: \"" << NAME_OR_EMPTY(kb.left) << "\"\n";
out << " right: \"" << NAME_OR_EMPTY(kb.right) << "\"\n";
out << " jump: \"" << NAME_OR_EMPTY(kb.jump) << "\"\n";
out << " attack: \"" << NAME_OR_EMPTY(kb.attack) << "\"\n";
}
// Aplica les assignacions persistides al GamepadManager. Ordre garantit
// pel Director: GamepadManager::init (obre tots els mandos físics) →
// Options::loadFromFile (llig YAML) → aquesta funció (casa
// mandos↔jugadors) → auto-assign per ordre els slots que hagen quedat
// buits (equivalent al patró de `coffee_crisis_arcade_edition`).
void applyControlsToGamepadManager() {
auto& gm = GamepadManager::get();
gm.assignToPlayer(0, g_controls.gamepad_p1.name, g_controls.gamepad_p1.path);
gm.assignToPlayer(1, g_controls.gamepad_p2.name, g_controls.gamepad_p2.path);
const auto AUTO = gm.autoAssignFreeSlots();
if (AUTO.empty()) { return; }
// Sincronitzem Options amb el que el manager ha vinculat per
// ordre, per persistir-ho al config.yaml. No cridem els setters
// individuals perquè cada un dispararia un saveToFile; preferim
// fer un sol save al final.
for (const auto& a : AUTO) {
auto& slot = (a.player_id == 0) ? g_controls.gamepad_p1 : g_controls.gamepad_p2;
slot.name = a.name;
slot.path = a.path;
}
}
void parseAudio(const fkyaml::node& yaml) {
if (!yaml.contains("audio")) { return; }
const auto& node = yaml["audio"];
readField(node, "enabled", g_audio.enabled);
readField(node, "volume", g_audio.volume);
parseMusic(node);
parseSound(node);
}
// Presets per defecte de PostFX, portats directament de projecte_2026.
auto defaultPostFXPresets() -> std::vector<Options::PostFXPreset> {
return {
{.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.2F,
.mask = 0.4F,
.gamma = 0.5F,
.curvature = 0.0F,
.bleeding = 0.6F,
.flicker = 0.0F},
{.name = "Curved",
.vignette = 0.5F,
.scanlines = 0.6F,
.chroma = 0.1F,
.mask = 0.5F,
.gamma = 0.7F,
.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.3F,
.curvature = 0.0F,
.bleeding = 0.0F,
.flicker = 0.0F},
{.name = "CRT Live",
.vignette = 0.5F,
.scanlines = 0.6F,
.chroma = 0.3F,
.mask = 0.3F,
.gamma = 0.4F,
.curvature = 0.3F,
.bleeding = 0.4F,
.flicker = 0.8F},
};
}
void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) {
readField(node, "name", preset.name);
readField(node, "vignette", preset.vignette);
readField(node, "scanlines", preset.scanlines);
readField(node, "chroma", preset.chroma);
readField(node, "mask", preset.mask);
readField(node, "gamma", preset.gamma);
readField(node, "curvature", preset.curvature);
readField(node, "bleeding", preset.bleeding);
readField(node, "flicker", preset.flicker);
readField(node, "scan_dark_ratio", preset.scan_dark_ratio);
readField(node, "scan_dark_floor", preset.scan_dark_floor);
readField(node, "scan_edge_soft", preset.scan_edge_soft);
}
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";
out << " scan_dark_ratio: " << preset.scan_dark_ratio << "\n";
out << " scan_dark_floor: " << preset.scan_dark_floor << "\n";
out << " scan_edge_soft: " << preset.scan_edge_soft << "\n";
}
// Presets per defecte del shader CRT-Pi, portats de projecte_2026.
auto defaultCrtPiPresets() -> std::vector<Options::CrtPiPreset> {
return {
{.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},
};
}
void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) {
readField(node, "name", preset.name);
readField(node, "scanline_weight", preset.scanline_weight);
readField(node, "scanline_gap_brightness", preset.scanline_gap_brightness);
readField(node, "bloom_factor", preset.bloom_factor);
readField(node, "input_gamma", preset.input_gamma);
readField(node, "output_gamma", preset.output_gamma);
readField(node, "mask_brightness", preset.mask_brightness);
readField(node, "curvature_x", preset.curvature_x);
readField(node, "curvature_y", preset.curvature_y);
readField(node, "mask_type", preset.mask_type);
readField(node, "enable_scanlines", preset.enable_scanlines);
readField(node, "enable_multisample", preset.enable_multisample);
readField(node, "enable_gamma", preset.enable_gamma);
readField(node, "enable_curvature", preset.enable_curvature);
readField(node, "enable_sharper", preset.enable_sharper);
}
void writeCrtPiPreset(std::ostringstream& out, const Options::CrtPiPreset& preset) {
out << " - name: \"" << preset.name << "\"\n";
out << " scanline_weight: " << preset.scanline_weight << "\n";
out << " scanline_gap_brightness: " << preset.scanline_gap_brightness << "\n";
out << " bloom_factor: " << preset.bloom_factor << "\n";
out << " input_gamma: " << preset.input_gamma << "\n";
out << " output_gamma: " << preset.output_gamma << "\n";
out << " mask_brightness: " << preset.mask_brightness << "\n";
out << " curvature_x: " << preset.curvature_x << "\n";
out << " curvature_y: " << preset.curvature_y << "\n";
out << " mask_type: " << preset.mask_type << "\n";
out << " enable_scanlines: " << (preset.enable_scanlines ? "true" : "false") << "\n";
out << " enable_multisample: " << (preset.enable_multisample ? "true" : "false") << "\n";
out << " enable_gamma: " << (preset.enable_gamma ? "true" : "false") << "\n";
out << " enable_curvature: " << (preset.enable_curvature ? "true" : "false") << "\n";
out << " enable_sharper: " << (preset.enable_sharper ? "true" : "false") << "\n";
}
// Pipeline genèric de càrrega de presets: si el fitxer no existeix o és
// invàlid (no deserialitza, manca `presets:`, no és seqüència, queda
// buit), es restauren els defaults via `defaults_fn` i es reescriu via
// `save_fn`. Unifica PostFX i CrtPi — abans eren dues còpies del mateix
// codi que només diferien en el tipus concret de preset.
template <typename T>
auto loadPresetsFromFile(
const std::string& path,
std::vector<T>& presets,
std::vector<T> (*defaults_fn)(),
void (*parse_fn)(const fkyaml::node&, T&),
bool (*save_fn)(),
const char* kind_label) -> bool {
if (path.empty()) { return false; }
const std::string CONTENT = readFileOrEmpty(path);
if (CONTENT.empty()) {
// Primera execució (no existia o era buit): no cal backup.
presets = defaults_fn();
return save_fn();
}
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) {
Log::warn("Options", "{} manca `presets:`; reescrivint amb defaults.", kind_label);
backupFile(path);
presets = defaults_fn();
return save_fn();
}
presets.clear();
for (const auto& preset_node : yaml["presets"]) {
T preset;
parse_fn(preset_node, preset);
presets.push_back(preset);
}
if (presets.empty()) {
Log::warn("Options", "{} sense presets; reescrivint amb defaults.", kind_label);
backupFile(path);
presets = defaults_fn();
return save_fn();
}
return true;
} catch (const std::exception& e) {
Log::error("Options", "error parsejant {}: {}", kind_label, e.what());
Log::info("Options", "reescrivint el fitxer amb presets per defecte.");
backupFile(path);
presets = defaults_fn();
return save_fn();
}
}
// Pipeline genèric d'escriptura: capçalera de comentari + `presets:` +
// una entrada per preset via `write_fn`. `config_key_note` és la segona
// línia de comentari que recorda amb quin camp de config.yaml han de
// coincidir els noms.
template <typename T>
auto savePresetsToFile(
const std::string& path,
const std::vector<T>& presets,
void (*write_fn)(std::ostringstream&, const T&),
const char* header_comment,
const char* config_key_note) -> bool {
if (path.empty()) { return false; }
std::ostringstream out;
out << "# " << header_comment << "\n";
out << "# " << config_key_note << "\n";
out << "presets:\n";
for (const auto& preset : presets) {
write_fn(out, preset);
}
return writeFile(path, out.str());
}
} // namespace
namespace Options {
// --- Getters de l'estat ---
auto window() -> const Window& { return g_window; }
auto video() -> const Video& { return g_video; }
auto audio() -> const Audio& { return g_audio; }
auto controls() -> const Controls& { return g_controls; }
auto locale() -> const std::string& { return g_locale; }
auto configVersion() -> const std::string& { return g_config_version; }
auto postfxPresets() -> const std::vector<PostFXPreset>& { return g_postfx_presets; }
auto crtpiPresets() -> const std::vector<CrtPiPreset>& { return g_crtpi_presets; }
// --- Setters: muten + persisteixen ---
void setWindowZoom(int value) {
g_window.zoom = value;
saveToFile();
}
void setWindowFullscreen(bool value) {
g_window.fullscreen = value;
saveToFile();
}
void setVideoVSync(bool value) {
g_video.vsync = value;
saveToFile();
}
void setVideoScalingMode(Defaults::ScalingMode value) {
g_video.scaling_mode = value;
saveToFile();
}
void setVideoRenderInfoPosition(Defaults::RenderInfoPosition value) {
g_video.render_info_position = value;
saveToFile();
}
void setVideoRenderInfoShowTime(bool value) {
g_video.render_info_show_time = value;
saveToFile();
}
void setShaderEnabled(bool value) {
g_video.shader.enabled = value;
saveToFile();
}
void setActiveShaderName(const std::string& value) {
g_video.shader.current_shader = value;
saveToFile();
}
void setPostFXPreset(const std::string& value) {
g_video.shader.current_postfx_preset = value;
saveToFile();
}
void setCrtPiPreset(const std::string& value) {
g_video.shader.current_crtpi_preset = value;
saveToFile();
}
void setAudioEnabled(bool value) {
g_audio.enabled = value;
saveToFile();
}
void setAudioVolume(float value) {
g_audio.volume = std::clamp(value, 0.0F, 1.0F);
saveToFile();
}
void setAudioMusicEnabled(bool value) {
g_audio.music.enabled = value;
saveToFile();
}
void setAudioMusicVolume(float value) {
g_audio.music.volume = std::clamp(value, 0.0F, 1.0F);
saveToFile();
}
void setAudioSoundEnabled(bool value) {
g_audio.sound.enabled = value;
saveToFile();
}
void setAudioSoundVolume(float value) {
g_audio.sound.volume = std::clamp(value, 0.0F, 1.0F);
saveToFile();
}
void setGamepadAssignment(int player_id, const std::string& name, const std::string& path) {
if (player_id < 0 || player_id > 1) { return; }
auto& slot = (player_id == 0) ? g_controls.gamepad_p1 : g_controls.gamepad_p2;
slot.name = name;
slot.path = path;
GamepadManager::get().assignToPlayer(player_id, name, path);
saveToFile();
}
void setKeyboardBinding(int player_id, KeyboardAction action, SDL_Scancode scancode) {
if (player_id < 0 || player_id > 1) { return; }
auto& kb = (player_id == 0) ? g_controls.keyboard_p1 : g_controls.keyboard_p2;
switch (action) {
case KeyboardAction::UP:
kb.up = scancode;
break;
case KeyboardAction::DOWN:
kb.down = scancode;
break;
case KeyboardAction::LEFT:
kb.left = scancode;
break;
case KeyboardAction::RIGHT:
kb.right = scancode;
break;
case KeyboardAction::JUMP:
kb.jump = scancode;
break;
case KeyboardAction::ATTACK:
kb.attack = scancode;
break;
}
saveToFile();
}
void setConfigFile(const std::string& path) {
g_config_file_path = path;
}
auto loadFromFile() -> bool {
if (g_config_file_path.empty()) { return false; }
const std::string CONTENT = readFileOrEmpty(g_config_file_path);
if (CONTENT.empty()) {
// Primera execució: fem l'auto-assign inicial dels mandos
// (per a que P1 obtingui el primer mando connectat sense que
// l'usuari haja de passar pel menú) i després escrivim el
// fitxer amb els valors resultants.
applyControlsToGamepadManager();
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. El fitxer original queda
// a `<path>.bak` per si l'usuari volia preservar tuning manual.
if (file_version != Texts::VERSION) {
Log::warn("Options", "config.yaml versió \"{}\" != \"{}\"; reescrivint amb defaults.", file_version, Texts::VERSION);
backupFile(g_config_file_path);
return saveToFile();
}
g_config_version = file_version;
if (yaml.contains("locale")) { g_locale = yaml["locale"].get_value<std::string>(); }
parseWindow(yaml);
parseVideo(yaml);
parseAudio(yaml);
parseControls(yaml);
// Captura les assignacions inicials per detectar si
// l'auto-assign (dins d'`applyControls...`) les ha tocat i
// cal reescriure el fitxer amb els valors nous.
const auto BEFORE_P1 = g_controls.gamepad_p1;
const auto BEFORE_P2 = g_controls.gamepad_p2;
applyControlsToGamepadManager();
const bool CHANGED =
(BEFORE_P1.name != g_controls.gamepad_p1.name || BEFORE_P1.path != g_controls.gamepad_p1.path ||
BEFORE_P2.name != g_controls.gamepad_p2.name || BEFORE_P2.path != g_controls.gamepad_p2.path);
if (CHANGED) { saveToFile(); }
return true;
} catch (const std::exception& e) {
Log::error("Options", "error parsejant config.yaml: {}", e.what());
Log::info("Options", "reescrivint el fitxer amb defaults.");
backupFile(g_config_file_path);
return saveToFile();
}
}
auto saveToFile() -> bool {
if (g_config_file_path.empty()) { return false; }
g_config_version = Texts::VERSION;
std::ostringstream out;
out << "# Configuració d'AEE ARCADE. Generat automàticament; editable a mà.\n";
out << "version: \"" << g_config_version << "\"\n";
out << "locale: \"" << g_locale << "\"\n\n";
out << "window:\n";
out << " zoom: " << g_window.zoom << "\n";
out << " fullscreen: " << (g_window.fullscreen ? "true" : "false") << "\n\n";
out << "video:\n";
out << " vsync: " << (g_video.vsync ? "true" : "false") << "\n";
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, g_video.scaling_mode) << "\"\n";
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, g_video.texture_filter) << "\"\n";
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, g_video.render_info_position) << "\"\n";
out << " render_info_show_time: " << (g_video.render_info_show_time ? "true" : "false") << "\n";
out << " gpu:\n";
out << " acceleration: " << (g_video.gpu.acceleration ? "true" : "false") << "\n";
out << " preferred_driver: \"" << g_video.gpu.preferred_driver << "\"\n";
out << " shader:\n";
out << " enabled: " << (g_video.shader.enabled ? "true" : "false") << "\n";
out << " current_shader: \"" << g_video.shader.current_shader << "\"\n";
out << " current_postfx_preset: \"" << g_video.shader.current_postfx_preset << "\"\n";
out << " current_crtpi_preset: \"" << g_video.shader.current_crtpi_preset << "\"\n\n";
out << "audio:\n";
out << " enabled: " << (g_audio.enabled ? "true" : "false") << "\n";
out << " volume: " << g_audio.volume << "\n";
out << " music:\n";
out << " enabled: " << (g_audio.music.enabled ? "true" : "false") << "\n";
out << " volume: " << g_audio.music.volume << "\n";
out << " sound:\n";
out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n";
out << " volume: " << g_audio.sound.volume << "\n\n";
out << "controls:\n";
out << " gamepad_p1:\n";
out << " name: \"" << g_controls.gamepad_p1.name << "\"\n";
out << " path: \"" << g_controls.gamepad_p1.path << "\"\n";
out << " gamepad_p2:\n";
out << " name: \"" << g_controls.gamepad_p2.name << "\"\n";
out << " path: \"" << g_controls.gamepad_p2.path << "\"\n";
writeKeyboard(out, "keyboard_p1", g_controls.keyboard_p1);
writeKeyboard(out, "keyboard_p2", g_controls.keyboard_p2);
return writeFile(g_config_file_path, out.str());
}
auto buildScreenConfig() -> Screen::Config {
return {
.zoom = g_window.zoom,
.fullscreen = g_window.fullscreen,
.vsync = g_video.vsync,
.scaling_mode = g_video.scaling_mode,
.texture_filter = g_video.texture_filter,
.gpu_acceleration = g_video.gpu.acceleration,
.preferred_driver = g_video.gpu.preferred_driver,
};
}
auto buildAudioConfig() -> ::Audio::Config {
return {
.enabled = g_audio.enabled,
.volume = g_audio.volume,
.music_enabled = g_audio.music.enabled,
.music_volume = g_audio.music.volume,
.sound_enabled = g_audio.sound.enabled,
.sound_volume = g_audio.sound.volume,
};
}
void setPostFXFile(const std::string& path) {
g_postfx_file_path = path;
}
auto loadPostFXFromFile() -> bool {
return loadPresetsFromFile<PostFXPreset>(
g_postfx_file_path,
g_postfx_presets,
&defaultPostFXPresets,
&parsePostFXPreset,
&savePostFXToFile,
"postfx.yaml");
}
auto savePostFXToFile() -> bool {
return savePresetsToFile<PostFXPreset>(
g_postfx_file_path,
g_postfx_presets,
&writePostFXPreset,
"Presets PostFX per a AEE ARCADE. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
}
void setCrtPiFile(const std::string& path) {
g_crtpi_file_path = path;
}
auto loadCrtPiFromFile() -> bool {
return loadPresetsFromFile<CrtPiPreset>(
g_crtpi_file_path,
g_crtpi_presets,
&defaultCrtPiPresets,
&parseCrtPiPreset,
&saveCrtPiToFile,
"crtpi.yaml");
}
auto saveCrtPiToFile() -> bool {
return savePresetsToFile<CrtPiPreset>(
g_crtpi_file_path,
g_crtpi_presets,
&writeCrtPiPreset,
"Presets CRT-Pi per a AEE ARCADE. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
}
auto buildActiveShader() -> Rendering::ShaderType {
// Si el shader està deshabilitat, forcem POSTFX. El backend tracta
// POSTFX amb params a zero com a passthrough; CRTPI, en canvi, manté
// el seu pipeline encara amb params buits (no és passthrough), així
// que mai s'ha d'arribar al pipeline de CRTPI quan l'usuari ha apagat
// els shaders, ni des d'F4 ni des del menú de servei.
if (!g_video.shader.enabled) { return Rendering::ShaderType::POSTFX; }
if (g_video.shader.current_shader == "crtpi") {
return Rendering::ShaderType::CRTPI;
}
return Rendering::ShaderType::POSTFX;
}
auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame {
const std::string& active_preset = (g_video.shader.current_shader == "crtpi")
? g_video.shader.current_crtpi_preset
: g_video.shader.current_postfx_preset;
return {
.target_position = g_video.render_info_position,
.show_time = g_video.render_info_show_time,
.shader_enabled = g_video.shader.enabled,
.shader_name = g_video.shader.current_shader,
.shader_preset = active_preset,
.text_color = text_color,
.shadow_color = shadow_color,
};
}
auto buildCrtPiParams() -> Rendering::CrtPiParams {
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
// PostFX passthrough, però sent nets per si algú consulta aquests params).
if (!g_video.shader.enabled) { return {}; }
const auto IT = std::ranges::find_if(g_crtpi_presets,
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
if (IT == g_crtpi_presets.end()) {
Log::warn("Options", "preset CrtPi \"{}\" no trobat; aplicant defaults.", g_video.shader.current_crtpi_preset);
return {};
}
return {
.scanline_weight = IT->scanline_weight,
.scanline_gap_brightness = IT->scanline_gap_brightness,
.bloom_factor = IT->bloom_factor,
.input_gamma = IT->input_gamma,
.output_gamma = IT->output_gamma,
.mask_brightness = IT->mask_brightness,
.curvature_x = IT->curvature_x,
.curvature_y = IT->curvature_y,
.mask_type = IT->mask_type,
.enable_scanlines = IT->enable_scanlines,
.enable_multisample = IT->enable_multisample,
.enable_gamma = IT->enable_gamma,
.enable_curvature = IT->enable_curvature,
.enable_sharper = IT->enable_sharper,
};
}
auto buildPostFXParams() -> Rendering::PostFXParams {
// Si el shader està deshabilitat, retornem params a zero → passthrough.
if (!g_video.shader.enabled) { return {}; }
const auto IT = std::ranges::find_if(g_postfx_presets,
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
if (IT == g_postfx_presets.end()) {
Log::warn("Options", "preset PostFX \"{}\" no trobat; aplicant passthrough.", g_video.shader.current_postfx_preset);
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,
.scan_dark_ratio = IT->scan_dark_ratio,
.scan_dark_floor = IT->scan_dark_floor,
.scan_edge_soft = IT->scan_edge_soft,
};
}
} // namespace Options