Config.yaml + Screen::Config desacoblat via Options (v0.3.0)

This commit is contained in:
JailDesigner committed 2026-04-18 21:38:40 +02:00
1 parent de5a670fdb
commit 0348acb0eb
8 files changed
+335 -16

No files matched your search

+35 -14
View File
@@ -8,7 +8,6 @@
#include <cstdlib>
#include <ranges>
#include "utils/defaults.hpp"
#include "utils/defines.hpp"
namespace {
@@ -48,12 +47,41 @@ Screen::Screen()
std::fprintf(stderr, "SDL_InitSubSystem(VIDEO) failed: %s\n", SDL_GetError());
std::exit(EXIT_FAILURE);
}
// Els recursos SDL (finestra, renderer, textura) es creen a configure().
}
const int WIN_W = GameCanvas::WIDTH * Defaults::Window::ZOOM;
const int WIN_H = GameCanvas::HEIGHT * Defaults::Window::ZOOM;
Screen::~Screen() {
destroySdl();
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
void Screen::configure(const Config& config) {
destroySdl();
config_ = config;
createSdl();
}
void Screen::destroySdl() {
if (game_texture_ != nullptr) {
SDL_DestroyTexture(game_texture_);
game_texture_ = nullptr;
}
if (renderer_ != nullptr) {
SDL_DestroyRenderer(renderer_);
renderer_ = nullptr;
}
if (window_ != nullptr) {
SDL_DestroyWindow(window_);
window_ = nullptr;
}
}
void Screen::createSdl() {
const int WIN_W = GameCanvas::WIDTH * config_.zoom;
const int WIN_H = GameCanvas::HEIGHT * config_.zoom;
Uint32 win_flags = SDL_WINDOW_RESIZABLE;
if (Defaults::Window::FULLSCREEN) { win_flags |= SDL_WINDOW_FULLSCREEN; }
if (config_.fullscreen) { win_flags |= SDL_WINDOW_FULLSCREEN; }
window_ = SDL_CreateWindow(Texts::WINDOW_TITLE, WIN_W, WIN_H, win_flags);
if (window_ == nullptr) {
@@ -67,8 +95,8 @@ Screen::Screen()
std::exit(EXIT_FAILURE);
}
SDL_SetRenderVSync(renderer_, Defaults::Video::VSYNC ? 1 : 0);
SDL_SetRenderLogicalPresentation(renderer_, GameCanvas::WIDTH, GameCanvas::HEIGHT, toSdlPresentation(Defaults::Video::SCALING_MODE));
SDL_SetRenderVSync(renderer_, config_.vsync ? 1 : 0);
SDL_SetRenderLogicalPresentation(renderer_, GameCanvas::WIDTH, GameCanvas::HEIGHT, toSdlPresentation(config_.scaling_mode));
// STREAMING: el framebuffer ARGB del joc s'actualitza per CPU cada frame
// i s'envia a la GPU via SDL_UpdateTexture en present().
@@ -77,14 +105,7 @@ Screen::Screen()
std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError());
std::exit(EXIT_FAILURE);
}
SDL_SetTextureScaleMode(game_texture_, toSdlScaleMode(Defaults::Video::TEXTURE_FILTER));
}
Screen::~Screen() {
if (game_texture_ != nullptr) { SDL_DestroyTexture(game_texture_); }
if (renderer_ != nullptr) { SDL_DestroyRenderer(renderer_); }
if (window_ != nullptr) { SDL_DestroyWindow(window_); }
SDL_QuitSubSystem(SDL_INIT_VIDEO);
SDL_SetTextureScaleMode(game_texture_, toSdlScaleMode(config_.texture_filter));
}
void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
+21
View File
@@ -5,8 +5,20 @@
#include <cstdint>
#include <vector>
#include "utils/defaults.hpp"
class Screen {
public:
// Configuració typada de Screen. L'omple el Director des d'Options,
// de manera que Screen no coneix YAML ni la capa Options directament.
struct Config {
int zoom{Defaults::Window::ZOOM};
bool fullscreen{Defaults::Window::FULLSCREEN};
bool vsync{Defaults::Video::VSYNC};
Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE};
Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER};
};
// Meyers singleton: la destrucció ocorre durant la termianció estàtica
// (LIFO respecte a Resource::Cache). El SDL_Quit s'ha de registrar via
// std::atexit ABANS de la primera crida a get(); així Screen::~Screen
@@ -18,6 +30,10 @@ class Screen {
auto operator=(const Screen&) -> Screen& = delete;
auto operator=(Screen&&) -> Screen& = delete;
// Crea (o recrea) finestra, renderer i textura a partir de la config.
// Imprescindible cridar-lo abans de qualsevol clear()/present().
void configure(const Config& config);
void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0);
void present();
@@ -30,6 +46,11 @@ class Screen {
Screen();
~Screen();
void destroySdl();
void createSdl();
Config config_{};
SDL_Window* window_{nullptr};
SDL_Renderer* renderer_{nullptr};
SDL_Texture* game_texture_{nullptr};
+33 -1
View File
@@ -5,6 +5,8 @@
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <memory>
#include <utility>
@@ -12,6 +14,7 @@
#include "core/resources/resource_cache.hpp"
#include "core/scene/scene.hpp"
#include "core/system/event_buffer.hpp"
#include "game/options.hpp"
#include "game/scenes/game_scene.hpp"
Director::Director() {
@@ -26,10 +29,18 @@ Director::Director() {
// amb SDL ja apagat → caos de destrucció clàssic.
std::atexit(+[] { SDL_Quit(); });
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
// les opcions; si no existeix config.yaml el crea amb els defaults.
createSystemFolder();
Options::setConfigFile(system_folder_ + "config.yaml");
if (!Options::loadFromFile()) {
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
}
// Forcem la construcció dels singletons en l'ordre desitjat: primer
// Screen (necessari per a renderitzar), després Cache. La destrucció
// estàtica anirà LIFO: Cache primer, Screen últim.
(void)Screen::get();
Screen::get().configure(Options::buildScreenConfig());
(void)Resource::Cache::get();
current_scene_ = std::make_unique<GameScene>();
@@ -41,6 +52,27 @@ Director::~Director() {
// SDL_Quit corre via atexit; els singletons via destrucció estàtica.
}
void Director::createSystemFolder() {
// Volem la config a $XDG_CONFIG_HOME/jailgames/aee_2026/ (defecte
// ~/.config/jailgames/aee_2026/). SDL_GetPrefPath apunta a XDG_DATA_HOME
// (.local/share), que és el lloc per a dades, no per a configuració.
const char* xdg = std::getenv("XDG_CONFIG_HOME");
std::string base;
if (xdg != nullptr && *xdg != '\0') {
base = xdg;
} else {
const char* home = std::getenv("HOME");
base = (home != nullptr) ? std::string(home) + "/.config" : ".";
}
system_folder_ = base + "/jailgames/aee_2026/";
std::error_code error;
std::filesystem::create_directories(system_folder_, error);
if (error) {
std::cerr << "Director: no s'ha pogut crear " << system_folder_ << ": " << error.message() << '\n';
}
}
void Director::processEvents() {
const bool QUIT_REQUESTED = std::ranges::any_of(
EventBuffer::events,
+3
View File
@@ -4,6 +4,7 @@
#include <cstdint>
#include <memory>
#include <string>
class Scene;
@@ -15,9 +16,11 @@ class Director {
auto iterate() -> SDL_AppResult;
private:
std::string system_folder_;
std::unique_ptr<Scene> current_scene_;
std::uint64_t last_ticks_ms_{0};
void createSystemFolder();
void processEvents();
void switchSceneIfRequested();
};
+183
View File
@@ -0,0 +1,183 @@
#include "game/options.hpp"
#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 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>());
}
}
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);
}
} // 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\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;
return config;
}
} // namespace Options
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <string>
#include "core/rendering/screen.hpp"
#include "utils/defaults.hpp"
// Gestor de configuració del joc. És l'ÚNIC lloc que coneix YAML i la ruta
// del config.yaml. Els subsistemes (Screen, Audio, ...) reben structs tipats
// a través dels build*Config(), de manera que no queden acoblats a aquest
// namespace ni al format concret del fitxer.
namespace Options {
struct Window {
int zoom{Defaults::Window::ZOOM};
bool fullscreen{Defaults::Window::FULLSCREEN};
};
struct Video {
bool vsync{Defaults::Video::VSYNC};
Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE};
Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER};
};
struct Music {
bool enabled{Defaults::Audio::MUSIC_ENABLED};
float volume{Defaults::Audio::MUSIC_VOLUME};
};
struct Sound {
bool enabled{Defaults::Audio::SOUND_ENABLED};
float volume{Defaults::Audio::SOUND_VOLUME};
};
struct Audio {
bool enabled{Defaults::Audio::ENABLED};
float volume{Defaults::Audio::VOLUME};
Music music{};
Sound sound{};
};
// --- Estat global (inline vars per a que una sola definició viva a tot
// el programa sense necessitar un .cpp extra per a cada una) ---
inline std::string config_version{};
inline std::string config_file_path{};
inline Window window{};
inline Video video{};
inline Audio audio{};
// --- API pública ---
void setConfigFile(const std::string& path);
auto loadFromFile() -> bool;
auto saveToFile() -> bool;
// --- Builders per als subsistemes ---
[[nodiscard]] auto buildScreenConfig() -> Screen::Config;
} // namespace Options
+1 -1
View File
@@ -3,7 +3,7 @@
namespace Texts {
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
constexpr const char* VERSION = "0.2.0"; // Single source of truth — Makefile el grep
constexpr const char* VERSION = "0.3.0"; // Single source of truth — Makefile el grep
} // namespace Texts
namespace GameCanvas {