#include "core/ui/ui_config.hpp" #include #include #include #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" #include "utils/defaults.hpp" #include "utils/easing.hpp" #include "utils/log.hpp" namespace { // Lector de camp amb fallback: si la clau existeix al node, escriu a // `dst` el valor convertit; si no, deixa `dst` com estava (que al // constructor ja hem inicialitzat amb Defaults::UI). template void readField(const fkyaml::node& node, const char* key, T& dst) { if (node.contains(key)) { dst = node[key].get_value(); } } // Variant per a Uint8 — fkyaml no serialitza a Uint8 directament, cal // passar per int i encaixar al rang 0..255 (valors fora de rang es // trunquen i s'avisen). void readColorIndex(const fkyaml::node& node, const char* key, Uint8& dst) { if (!node.contains(key)) { return; } const int VALUE = node[key].get_value(); if (VALUE < 0 || VALUE > 255) { Log::warn("UiConfig", "índex '{}'={} fora de rang 0..255 — ignorat", key, VALUE); return; } dst = static_cast(VALUE); } // Registre de corbes disponibles. Afegir-ne una nova és només una // entrada ací: la llegeix `UiConfig::curveByName` i queda exposada al // YAML pel seu nom. No hi ha fallback mut: si el nom no hi és, l'api // retorna linear i avisa per stderr perquè l'error no passi en silenci. auto curveRegistry() -> const std::unordered_map& { static const std::unordered_map REGISTRY = { {"linear", &Easing::linear}, {"outQuad", &Easing::outQuad}, {"inQuad", &Easing::inQuad}, {"inOutQuad", &Easing::inOutQuad}, {"outCubic", &Easing::outCubic}, {"inCubic", &Easing::inCubic}, }; return REGISTRY; } void readCurve(const fkyaml::node& node, const char* key, UiConfig::CurveFn& dst) { if (!node.contains(key)) { return; } const auto NAME = node[key].get_value(); dst = UiConfig::curveByName(NAME); } void readFont(const fkyaml::node& node, const char* key, UiConfig::Font& dst) { if (!node.contains(key)) { return; } const auto& f = node[key]; readField(f, "name", dst.name); readField(f, "gif", dst.gif); readField(f, "fnt", dst.fnt); readField(f, "kerning", dst.kerning); } } // namespace UiConfig::UiConfig() : render_info_text_(Defaults::UI::RENDER_INFO_TEXT), render_info_shadow_(Defaults::UI::RENDER_INFO_SHADOW), canvas_bg_(Defaults::UI::CANVAS_BG), notification_text_(Defaults::UI::NOTIFICATION_TEXT), notification_bg_(Defaults::UI::NOTIFICATION_BG), exit_text_(Defaults::UI::EXIT_TEXT), exit_bg_(Defaults::UI::EXIT_BG), menu_bg_(Defaults::UI::MENU_BG), menu_border_(Defaults::UI::MENU_BORDER), menu_title_(Defaults::UI::MENU_TITLE), menu_label_(Defaults::UI::MENU_LABEL), menu_value_(Defaults::UI::MENU_VALUE), menu_cursor_(Defaults::UI::MENU_CURSOR), menu_disabled_(Defaults::UI::MENU_DISABLED), hud_bg_(Defaults::UI::HUD_BG), hud_p1_label_(Defaults::UI::HUD_P1_LABEL), hud_p1_value_(Defaults::UI::HUD_P1_VALUE), hud_p1_shadow_(Defaults::UI::HUD_P1_SHADOW), hud_center_label_(Defaults::UI::HUD_CENTER_LABEL), hud_center_value_(Defaults::UI::HUD_CENTER_VALUE), hud_center_shadow_(Defaults::UI::HUD_CENTER_SHADOW), hud_p2_label_(Defaults::UI::HUD_P2_LABEL), hud_p2_value_(Defaults::UI::HUD_P2_VALUE), hud_p2_shadow_(Defaults::UI::HUD_P2_SHADOW), hud_flash_(Defaults::UI::HUD_FLASH), room_text_(Defaults::UI::ROOM_TEXT), room_text_outline_(Defaults::UI::ROOM_TEXT_OUTLINE), notification_ms_(Defaults::UI::NOTIFICATION_TIME_MS), exit_ms_(Defaults::UI::EXIT_TIME_MS), hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS), hud_flash_ms_(Defaults::UI::HUD_FLASH_MS), hud_shake_ms_(Defaults::UI::HUD_SHAKE_MS), pause_enter_ms_(Defaults::UI::PAUSE_ENTER_MS), pause_exit_ms_(Defaults::UI::PAUSE_EXIT_MS), pause_enter_curve_(curveByName(Defaults::UI::PAUSE_ENTER_CURVE)), pause_exit_curve_(curveByName(Defaults::UI::PAUSE_EXIT_CURVE)), title_duration_ms_(Defaults::UI::Title::DURATION_MS), title_bg_color_(Defaults::UI::Title::BG_COLOR), title_text_color_(Defaults::UI::Title::TEXT_COLOR), title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR), title_press_start_outline_color_(Defaults::UI::Title::PRESS_START_OUTLINE_COLOR), title_logo_y_percent_(Defaults::UI::Title::LOGO_Y_PERCENT), title_prompt_y_percent_(Defaults::UI::Title::PROMPT_Y_PERCENT), title_copyright_y_percent_(Defaults::UI::Title::COPYRIGHT_Y_PERCENT), title_mini_logo_gap_px_(Defaults::UI::Title::MINI_LOGO_GAP_PX), title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS), title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS), title_starting_ms_(Defaults::UI::Title::STARTING_MS), title_starting_blink_period_ms_(Defaults::UI::Title::STARTING_BLINK_PERIOD_MS), title_fade_out_ms_(Defaults::UI::Title::FADE_OUT_MS), title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK), title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS), title_attract_fade_to_black_ms_(Defaults::UI::Title::ATTRACT_FADE_TO_BLACK_MS), title_intro_{ .fade_in_ms = Defaults::UI::Title::Intro::FADE_IN_MS, .game_logo_at_ms = Defaults::UI::Title::Intro::GAME_LOGO_AT_MS, .mini_logo_at_ms = Defaults::UI::Title::Intro::MINI_LOGO_AT_MS, .mini_logo_slide_ms = Defaults::UI::Title::Intro::MINI_LOGO_SLIDE_MS, .copyright_at_ms = Defaults::UI::Title::Intro::COPYRIGHT_AT_MS, .copyright_slide_ms = Defaults::UI::Title::Intro::COPYRIGHT_SLIDE_MS, .slide_curve = curveByName(Defaults::UI::Title::Intro::SLIDE_CURVE), .press_start_at_ms = Defaults::UI::Title::Intro::PRESS_START_AT_MS, }, title_heat_haze_{ .enabled = Defaults::UI::Title::HeatHaze::ENABLED, .amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX, .wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX, .speed_hz = Defaults::UI::Title::HeatHaze::SPEED_HZ, }, logo_scan_{ .duration_ms = Defaults::UI::Logo::Scan::DURATION_MS, .tail_glitch_ms = Defaults::UI::Logo::Scan::TAIL_GLITCH_MS, .band_height_px = Defaults::UI::Logo::Scan::BAND_HEIGHT_PX, .band_alpha = Defaults::UI::Logo::Scan::BAND_ALPHA, .band_pulse_hz = Defaults::UI::Logo::Scan::BAND_PULSE_HZ, .band_pulse_depth = Defaults::UI::Logo::Scan::BAND_PULSE_DEPTH, .glitch_amplitude_px = Defaults::UI::Logo::Scan::GLITCH_AMPLITUDE_PX, .glitch_band_radius_px = Defaults::UI::Logo::Scan::GLITCH_BAND_RADIUS_PX, .vhs_burst_per_sec = Defaults::UI::Logo::Scan::VHS_BURST_PER_SEC, .vhs_burst_duration_ms = Defaults::UI::Logo::Scan::VHS_BURST_DURATION_MS, .vhs_burst_height_px = Defaults::UI::Logo::Scan::VHS_BURST_HEIGHT_PX, .vhs_burst_amplitude_px = Defaults::UI::Logo::Scan::VHS_BURST_AMPLITUDE_PX, .flash_ms = Defaults::UI::Logo::Scan::FLASH_MS, }, logo_timeline_{ .black_intro_ms = Defaults::UI::Logo::Timeline::BLACK_INTRO_MS, .hold_silent_ms = Defaults::UI::Logo::Timeline::HOLD_SILENT_MS, .hold_with_music_ms = Defaults::UI::Logo::Timeline::HOLD_WITH_MUSIC_MS, .fade_out_ms = Defaults::UI::Logo::Timeline::FADE_OUT_MS, .black_outro_ms = Defaults::UI::Logo::Timeline::BLACK_OUTRO_MS, }, menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND), menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND), player_start_sound_(Defaults::UI::PLAYER_START_SOUND), logo_glitch_sound_(Defaults::UI::LOGO_GLITCH_SOUND), hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING}, placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING}, room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING}, menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING}, menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING}, copyright_font_{.name = Defaults::UI::Font::COPYRIGHT_NAME, .gif = Defaults::UI::Font::COPYRIGHT_GIF, .fnt = Defaults::UI::Font::COPYRIGHT_FNT, .kerning = Defaults::UI::Font::COPYRIGHT_KERNING}, notifier_{ .padding_h = Defaults::UI::Notifier::PADDING_H, .padding_top = Defaults::UI::Notifier::PADDING_TOP, .padding_bottom = Defaults::UI::Notifier::PADDING_BOTTOM, .line_gap = Defaults::UI::Notifier::LINE_GAP, .margin_top = Defaults::UI::Notifier::MARGIN_TOP, .border_px = Defaults::UI::Notifier::BORDER_PX, .slide_ms = Defaults::UI::Notifier::SLIDE_MS, .slide_in_curve = curveByName(Defaults::UI::Notifier::SLIDE_IN_CURVE), .slide_out_curve = curveByName(Defaults::UI::Notifier::SLIDE_OUT_CURVE), } { } auto UiConfig::get() -> UiConfig& { static UiConfig instance_; return instance_; } auto UiConfig::curveByName(const std::string& name) -> CurveFn { const auto& reg = curveRegistry(); const auto IT = reg.find(name); if (IT == reg.end()) { Log::warn("UiConfig", "corba '{}' desconeguda — s'usa linear", name); return &Easing::linear; } return IT->second; } void UiConfig::load(const std::string& file_path) { auto bytes = Resource::Helper::loadFile(file_path); if (bytes.empty()) { Log::warn("UiConfig", "no s'ha pogut obrir {} — s'usaran els defaults compilats", file_path); return; } try { const auto* begin = reinterpret_cast(bytes.data()); const auto* end = begin + bytes.size(); auto yaml = fkyaml::node::deserialize(begin, end); if (yaml.contains("colors")) { const auto& c = yaml["colors"]; readColorIndex(c, "render_info_text", render_info_text_); readColorIndex(c, "render_info_shadow", render_info_shadow_); readColorIndex(c, "canvas_bg", canvas_bg_); readColorIndex(c, "notification_text", notification_text_); readColorIndex(c, "notification_bg", notification_bg_); readColorIndex(c, "exit_text", exit_text_); readColorIndex(c, "exit_bg", exit_bg_); readColorIndex(c, "menu_bg", menu_bg_); readColorIndex(c, "menu_border", menu_border_); readColorIndex(c, "menu_title", menu_title_); readColorIndex(c, "menu_label", menu_label_); readColorIndex(c, "menu_value", menu_value_); readColorIndex(c, "menu_cursor", menu_cursor_); readColorIndex(c, "menu_disabled", menu_disabled_); readColorIndex(c, "hud_bg", hud_bg_); readColorIndex(c, "hud_flash", hud_flash_); readColorIndex(c, "room_text", room_text_); readColorIndex(c, "room_text_outline", room_text_outline_); } // Famílies de color del HUD: blocs `hud_p1`, `hud_center`, // `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i // cada clau són opcionals — el que manqui cau al default // compilat a `Defaults::UI`. auto read_hud_family = [&](const std::string& key, Uint8& label, Uint8& value, Uint8& shadow) { if (!yaml.contains(key)) { return; } const auto& f = yaml[key]; readColorIndex(f, "label", label); readColorIndex(f, "value", value); readColorIndex(f, "shadow", shadow); }; read_hud_family("hud_p1", hud_p1_label_, hud_p1_value_, hud_p1_shadow_); read_hud_family("hud_center", hud_center_label_, hud_center_value_, hud_center_shadow_); read_hud_family("hud_p2", hud_p2_label_, hud_p2_value_, hud_p2_shadow_); if (yaml.contains("timings")) { const auto& t = yaml["timings"]; readField(t, "notification_ms", notification_ms_); readField(t, "exit_ms", exit_ms_); } if (yaml.contains("fonts")) { const auto& f = yaml["fonts"]; readFont(f, "hud", hud_font_); readFont(f, "placeholder", placeholder_font_); readFont(f, "room_text", room_text_font_); readFont(f, "menu_title", menu_title_font_); readFont(f, "menu_items", menu_items_font_); readFont(f, "copyright", copyright_font_); } if (yaml.contains("hud")) { const auto& h = yaml["hud"]; readField(h, "bg_enabled", hud_bg_enabled_); readField(h, "entry_ms", hud_entry_ms_); readField(h, "flash_ms", hud_flash_ms_); readField(h, "shake_ms", hud_shake_ms_); } if (yaml.contains("pause")) { const auto& p = yaml["pause"]; readField(p, "enter_ms", pause_enter_ms_); readField(p, "exit_ms", pause_exit_ms_); readCurve(p, "enter_curve", pause_enter_curve_); readCurve(p, "exit_curve", pause_exit_curve_); } if (yaml.contains("sounds")) { const auto& s = yaml["sounds"]; readField(s, "menu_select", menu_select_sound_); readField(s, "menu_accept", menu_accept_sound_); readField(s, "player_start", player_start_sound_); readField(s, "logo_glitch", logo_glitch_sound_); } if (yaml.contains("title")) { const auto& t = yaml["title"]; readField(t, "duration_ms", title_duration_ms_); readColorIndex(t, "bg_color", title_bg_color_); readColorIndex(t, "text_color", title_text_color_); readColorIndex(t, "copyright_color", title_copyright_color_); readColorIndex(t, "press_start_outline_color", title_press_start_outline_color_); readField(t, "logo_y_percent", title_logo_y_percent_); readField(t, "prompt_y_percent", title_prompt_y_percent_); readField(t, "copyright_y_percent", title_copyright_y_percent_); readField(t, "mini_logo_gap_px", title_mini_logo_gap_px_); readField(t, "blink_visible_ms", title_blink_visible_ms_); readField(t, "blink_hidden_ms", title_blink_hidden_ms_); readField(t, "starting_ms", title_starting_ms_); readField(t, "starting_blink_period_ms", title_starting_blink_period_ms_); readField(t, "fade_out_ms", title_fade_out_ms_); readField(t, "fade_out_max_block", title_fade_out_max_block_); readField(t, "music_fade_out_ms", title_music_fade_out_ms_); readField(t, "attract_fade_to_black_ms", title_attract_fade_to_black_ms_); if (t.contains("intro")) { const auto& in = t["intro"]; readField(in, "fade_in_ms", title_intro_.fade_in_ms); readField(in, "game_logo_at_ms", title_intro_.game_logo_at_ms); readField(in, "mini_logo_at_ms", title_intro_.mini_logo_at_ms); readField(in, "mini_logo_slide_ms", title_intro_.mini_logo_slide_ms); readField(in, "copyright_at_ms", title_intro_.copyright_at_ms); readField(in, "copyright_slide_ms", title_intro_.copyright_slide_ms); readCurve(in, "slide_curve", title_intro_.slide_curve); readField(in, "press_start_at_ms", title_intro_.press_start_at_ms); } if (t.contains("heat_haze")) { const auto& hh = t["heat_haze"]; readField(hh, "enabled", title_heat_haze_.enabled); readField(hh, "amplitude_px", title_heat_haze_.amplitude_px); readField(hh, "wavelength_px", title_heat_haze_.wavelength_px); readField(hh, "speed_hz", title_heat_haze_.speed_hz); } } if (yaml.contains("logo")) { const auto& l = yaml["logo"]; if (l.contains("scan")) { const auto& s = l["scan"]; readField(s, "duration_ms", logo_scan_.duration_ms); readField(s, "tail_glitch_ms", logo_scan_.tail_glitch_ms); readField(s, "band_height_px", logo_scan_.band_height_px); readField(s, "band_alpha", logo_scan_.band_alpha); readField(s, "band_pulse_hz", logo_scan_.band_pulse_hz); readField(s, "band_pulse_depth", logo_scan_.band_pulse_depth); readField(s, "glitch_amplitude_px", logo_scan_.glitch_amplitude_px); readField(s, "glitch_band_radius_px", logo_scan_.glitch_band_radius_px); readField(s, "vhs_burst_per_sec", logo_scan_.vhs_burst_per_sec); readField(s, "vhs_burst_duration_ms", logo_scan_.vhs_burst_duration_ms); readField(s, "vhs_burst_height_px", logo_scan_.vhs_burst_height_px); readField(s, "vhs_burst_amplitude_px", logo_scan_.vhs_burst_amplitude_px); readField(s, "flash_ms", logo_scan_.flash_ms); } if (l.contains("timeline")) { const auto& tl = l["timeline"]; readField(tl, "black_intro_ms", logo_timeline_.black_intro_ms); readField(tl, "hold_silent_ms", logo_timeline_.hold_silent_ms); readField(tl, "hold_with_music_ms", logo_timeline_.hold_with_music_ms); readField(tl, "fade_out_ms", logo_timeline_.fade_out_ms); readField(tl, "black_outro_ms", logo_timeline_.black_outro_ms); } } if (yaml.contains("notifier")) { const auto& n = yaml["notifier"]; readField(n, "padding_h", notifier_.padding_h); readField(n, "padding_top", notifier_.padding_top); readField(n, "padding_bottom", notifier_.padding_bottom); readField(n, "line_gap", notifier_.line_gap); readField(n, "margin_top", notifier_.margin_top); readField(n, "border_px", notifier_.border_px); readField(n, "slide_ms", notifier_.slide_ms); readCurve(n, "slide_in_curve", notifier_.slide_in_curve); readCurve(n, "slide_out_curve", notifier_.slide_out_curve); } Log::info("UiConfig", "carregat des de {}", file_path); } catch (const std::exception& e) { Log::error("UiConfig", "error parsejant {}: {}", file_path, e.what()); } }