Files
aee-arcade/docs/service-menu-plan.md
T

22 KiB
Raw Blame History

Pla d'implementació — Menú de servei

Pla per a portar el menú de servei de ../aee (source/core/rendering/menu.cpp) a aee_arcade, adaptat a l'arquitectura actual (Scenes + Director instanciat + Options amb setters persistents + Screen ARGB + Text bitmap). Document de referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi encara.

Objectiu visual: caixa centrada amb vora i títol, navegable amb fletxes + Retorn + Retrocés, oberta i tancada amb F12 (hotkey menu_toggle), retolada amb la font ari-w9500-display.


1. Referència a ../aee

Tot el que segueix és la base; la còpia mental serveix per a no reinventar res:

  • source/core/rendering/menu.hpp — API pública (init/destroy/toggle/close/isOpen/isVisible/render/handleKey).
  • source/core/rendering/menu.cpp — implementació completa (676 línies). Conté:
    • Struct Item i Page amb ItemKind::{Toggle, Cycle, IntRange, Submenu, KeyBind, Action}.
    • Animació d'obertura/tancament (OPEN_SPEED, CLOSE_SPEED, HEIGHT_RATE) i transició entre pàgines (pushPage/popPage).
    • Dibuix manual sobre Uint32* pixel_data amb blendRect/fillRect/drawBorder.
    • buildRoot/buildVideo/buildAudio/buildControls/buildGame/buildSystem.
  • Claus de locale/ca.yaml sota menu.{titles, items, values} — es poden reutilitzar gairebé literalment.

2. Diferències d'arquitectura rellevants

Punt ../aee aee_arcade
Overlay Overlay::render() dibuixa Menú + Notifier al buffer No hi ha Overlay. Cada Scene crida Notifier::render() i RenderInfo::render()
Director Singleton Director::get() amb requestQuit/requestRestart Classe instanciada; sortida via SDL_EVENT_QUIT a EventBuffer
Options Membres inline mutats directament Getters const-ref + setters que persisteixen automàticament
Setter → subsistema Dins del mètode (ex. Screen::toggleVSync) El caller crida Options::setX(v) i Screen::setX(v) per separat
Remapping tecles KeyConfig::scancodePtr(id) retorna mutable KeyConfig actual no té scancodePtr — veure limitació a §5
Render Pixel-level (blend manual sobre Uint32*) Screen::fillRect(argb) + Text::writeDX; ARGB cru via getCanvasArgb()
Font 8bithud.fnt via Text Text via Resource::Cache (paletat sobre ui)

Conseqüències per al disseny:

  1. El menú ha d'integrar-se en el pipeline de render de cada Scene, no via Overlay. Pot ser tan simple com una línia al final de cada render() abans de Screen::get().present(), al costat de Notifier/RenderInfo.
  2. El menú intercepta input abans que GlobalInputs::update() processi les hotkeys (si és obert, només F12 i les fletxes arriben al menú; la resta d'esdeveniments es consumeixen perquè no activin F1–F11).
  3. Els setters de Options:: ja persisteixen. El menú crida Options::setX(v); Screen::setX(v); com fa global_inputs.cpp.

3. Ubicació i mòduls nous

source/game/ui/service_menu.hpp     (nou)
source/game/ui/service_menu.cpp     (nou)
data/fonts/ari-w9500-display.{fnt,gif}   (ja existeixen)
data/locale/ca.yaml                 (afegir clau arrel `menu:`)
data/locale/en.yaml                 (afegir clau arrel `menu:`)
data/input/keys.yaml                (canviar `toggle_render_info` → `menu_toggle` en F12, o moure F12 a menu i reubicar render_info)

Raonament:

  • Viu a game/ui/ perquè el contingut de les pàgines toca Options:: i Screen:: del joc; no és un primitiu de renderització com Text o Notifier.
  • Exposa l'API com a singleton (ServiceMenu::get()) per coherència amb Notifier i RenderInfo.
  • init() rep std::shared_ptr<Text> + Palette ui_palette, igual que Notifier::init(). El Director construeix la font display i la injecta.

Esquelet de l'API:

// source/game/ui/service_menu.hpp
class ServiceMenu {
 public:
  static auto get() -> ServiceMenu&;
  void init(std::shared_ptr<Text> font, const Palette& ui_palette);

  [[nodiscard]] auto isOpen() const -> bool;      // accepta input
  [[nodiscard]] auto isVisible() const -> bool;   // dibuixa caixa (també tancant-se)

  void toggle();
  void close();

  void update(int delta_ms);   // avança animacions
  void render();               // dibuixa sobre el canvas de Screen

  // Retorna true si el menú ha consumit l'esdeveniment (tecla empassada).
  auto handleEvent(const SDL_Event& ev) -> bool;
};

4. Hotkey i integració amb GlobalInputs

Decisió: F12 passa a ser menu_toggle i el cicle de l'overlay d'info es mou a una altra tecla (per ex. F10) o es manté a F12 i s'obre el menú amb una altra (per ex. F9). Opció recomanada:

  • menu_toggle → F9 (tecla nova, no conflictiva).
  • toggle_render_info es queda a F12 (hàbit actual).

Motius: evita trencar la memòria muscular existent i deixa F12 — una tecla familiar per a debug — lliure per a la seua funció actual.

Afegir a data/input/keys.yaml:

- id: menu_toggle
  code: "F9"
  desc: "Obri/tanca el menú de servei"

Flux d'input (escena GameScene::update):

// Abans de GlobalInputs::update, donar prioritat al menú.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
    if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) {
        it = EventBuffer::events.erase(it);
    } else if (it->type == SDL_EVENT_KEY_DOWN &&
               it->key.scancode == KeyConfig::get().scancode("menu_toggle")) {
        ServiceMenu::get().toggle();
        it = EventBuffer::events.erase(it);
    } else {
        ++it;
    }
}
GlobalInputs::update(delta_ms);
  • Mentre el menú està obert, les hotkeys (F1–F8, F11, F12) queden bloquejades perquè els KEY_DOWN els consumeix el menú.
  • Escape dins del menú ↔ close (o cancel·lació de captura de tecla, si es porta el KeyBind — veure §5).
  • F9 sempre passa el toggle (encara que el menú estiga obert).

Alternativa més neta a mig termini: un InputRouter centralitzat al Director amb prioritats (Menu → GlobalInputs → Scene). De moment, cadascuna de les Scenes (LogoScene, TitleScene, IntroScene, GameScene) crida el bloc d'input-del-menú al principi de update(), igual que ja fan amb GlobalInputs::update.


5. Contingut de les pàgines (adaptat a Options actuals)

Root

  • Vídeo → submenú
  • Àudio → submenú
  • Sistema → submenú

No hi ha Controls ni Game: Options:: actual no exposa remapping de tecles de joc (les KeysGame són constants de Defaults::), ni opcions "de joc" equivalents a use_new_logo / show_preload. Si en el futur apareixen, es pot afegir sense reescriure res.

Vídeo

Items, cadascun mapa el setter+notificador com global_inputs.cpp:

Label Tipus Origen Handler
menu.items.zoom IntRange Options::window().zoom Options::setWindowZoom(v); Screen::get().setWindowZoom(v) (rang 1–6, clamp)
menu.items.screen Toggle Options::window().fullscreen Options::setWindowFullscreen(v); Screen::get().setFullscreen(v)
menu.items.vsync Toggle Options::video().vsync Options::setVideoVSync(v); Screen::get().setVSync(v)
menu.items.scaling_mode Cycle Options::video().scaling_mode Options::setVideoScalingMode(m); Screen::get().setScalingMode(m)
menu.items.shader Toggle Options::video().shader.enabled Options::setShaderEnabled(v) + reapplyShader() (copiar helper de global_inputs.cpp)
menu.items.shader_type Cycle Options::video().shader.current_shader Options::setActiveShaderName("postfx"/"crtpi") + reapplyShader()
menu.items.preset Cycle preset actiu segons shader Options::setPostFXPreset(s) o setCrtPiPreset(s) + reapplyShader()
menu.items.render_info Cycle Options::video().render_info_position Options::setVideoRenderInfoPosition(p)
menu.items.uptime Toggle Options::video().render_info_show_time Options::setVideoRenderInfoShowTime(v) (sols visible si render_info ≠ OFF)

Claus visible (oculta quan no apliquen):

  • shader_type, preset → només si shader.enabled == true.
  • uptime → només si render_info_position != OFF.

Àudio

Label Tipus Origen Handler
menu.items.master_enable Toggle Options::audio().enabled Caldrà afegir Options::setAudioEnabled(v) (no hi és avui) + Audio::applyConfig()
menu.items.master_volume IntRange Options::audio().volume Options::setAudioVolume(v) (afegir) + Audio::applyConfig()
menu.items.music Toggle Options::audio().music.enabled Options::setAudioMusicEnabled(v) (afegir) + Audio::applyConfig()
menu.items.music_volume IntRange Options::audio().music.volume Options::setAudioMusicVolume(v) (afegir) + Audio::applyConfig()
menu.items.sounds Toggle Options::audio().sound.enabled Options::setAudioSoundEnabled(v) (afegir) + Audio::applyConfig()
menu.items.sounds_volume IntRange Options::audio().sound.volume Options::setAudioSoundVolume(v) (afegir) + Audio::applyConfig()

Dependència prèvia: Options:: no té setters d'àudio. Cal afegir-los seguint el mateix patró que setVideoVSync (muten namespace intern + saveToFile()) i exposar Audio::applyConfig(Audio::Config) o Audio::setVolumes(...) — depenent del que ja existeixi a core/audio/.

Les escales de volum són 0.0–1.0 amb pas 0.05 (5 %), com a ../aee.

Sistema

Subtítol: v<VERSION> (<GIT_HASH>) — usar el version.h generat per CMake (ja existeix a source/version.h.in).

Label Tipus Handler
menu.items.exit_game Action Injectar un SDL_EVENT_QUIT a EventBuffer::events perquè Director::processEvents el vegi

Sense Restart de moment: Director no té un pas de reinici en calent, i simular-lo a partir del director actual implica destruir l'escena i reconstruir el flux — fora d'abast d'aquest menú. Deixar-ho com a TODO si es necessita.

Limitació: pàgina Controls diferida

El menú de ../aee té una pàgina Controls (buildControls) que permet remappar up/down/left/right i la tecla de menú. A aee_arcade:

  • Les tecles de joc són Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT} — constants constexpr, no mutables.
  • KeyConfig actual no exposa scancodePtr(id).

Opcions, de menys a més invasiu:

  1. No portar Controls en aquesta iteració (recomanat per a la primera versió).
  2. Migrar KeysGame a Options:: + afegir KeyConfig::scancodePtr(id) per a hotkeys. Requereix tocar Player::Controls, el config.yaml i keys.yaml.

Si s'opta per (2), la pàgina Controls s'afegeix exactament igual que a ../aee.


6. Renderitzat

Dos camins segons nivell d'ambició:

A. Simple — sòlid opac (MVP)

  • Caixa centrada: Screen::get().fillRect(x, y, w, h, ui_palette_[idx_fons]).
  • Vora: 4 fillRect d'1 px amb color ui_palette_[idx_vora].
  • Text: Text::writeDX(CENTER_FLAG | COLOR_FLAG | SHADOW_FLAG, ...).
  • Cap blend; el joc queda totalment ocult darrere de la caixa (equivalent a ../aee però amb BG_ALPHA = 255).

Afegir dos índexs nous a utils/defaults.hpp → UI:::

constexpr Uint8 MENU_BG     = /* triar paleta sega-cube */;
constexpr Uint8 MENU_BORDER = /* triar */;
constexpr Uint8 MENU_TITLE  = /* blanc */;
constexpr Uint8 MENU_LABEL  = /* gris clar */;
constexpr Uint8 MENU_VALUE  = /* cian/groc */;
constexpr Uint8 MENU_CURSOR = /* destacat */;
constexpr Uint8 MENU_EMPTY  = /* gris */;

B. Semi-transparent (opcional, iteració 2)

Calca el blendRect de ../aee: expandir Screen:: amb un blendRect(x, y, w, h, argb, alpha) que opere sobre canvas_.data(). El canvas és std::uint32_t* format ABGR (equivalent a ARGB little-endian). Amb això, el fons del menú pot ser alpha=220 i es veu el joc apagat per darrere.

Mida fixa recomanada per al MVP: BOX_W = 220, altura dinàmica (igual que ../aee). Canvas lògic d'aee_arcade és GameCanvas::WIDTH/HEIGHT — confirmar mida real (mateixos 320×200?) a utils/defines.hpp abans de hardcoded.


7. Font ari-w9500-display

Ja és al disc i ja té el metadatfnt corresponent (data/fonts/ari-w9500-display.fnt, 11 px d'altura, 12 px de box). És paletable sobre ui com les altres fonts bitmap.

Al Director::Director(), afegir després del bloc de fonts HUD/Placeholder:

constexpr const char* MENU_FONT_NAME = "ari-w9500-display";
constexpr const char* MENU_FONT_GIF  = "data/fonts/ari-w9500-display.gif";
constexpr const char* MENU_FONT_FNT  = "data/fonts/ari-w9500-display.fnt";

const std::string MENU_FONT_GIF_KEY = std::string(MENU_FONT_NAME) + ".gif";
Resource::Cache::get().loadSurface(MENU_FONT_GIF_KEY, MENU_FONT_GIF);
Resource::Cache::get().getSurface(MENU_FONT_GIF_KEY)->setPalette(
    Resource::Cache::get().getPalette("ui"));
auto menu_font_surface = Resource::Cache::get().getSurface(MENU_FONT_GIF_KEY);
auto menu_text = std::make_shared<Text>(menu_font_surface, MENU_FONT_FNT);

ServiceMenu::get().init(menu_text, ui_palette);

Centralitzar les constants a Defaults::UI::Font::MENU_{NAME,GIF,FNT} perquè encaixi amb el memo project_ui_fonts.md ("tot a Defaults::UI::Font").


8. Locale

Copiar les claus de ../aee/data/locale/ca.yaml sota menu: (titles, items, values) a ca.yaml i en.yaml. El primer commit pot incloure només ca; en val la pena fer-lo a la vegada perquè és traducció directa.

Valors a eliminar de la còpia (no apliquen ací):

  • menu.items.{use_new_logo, show_title_credits, show_preload} (no hi ha pàgina Game).
  • menu.items.{move_up, move_down, move_left, move_right, menu_key, restart, aspect_4_3, texture_filter, internal_resolution, supersampling} (no portats a l'MVP — veure §5 i les decisions de pàgina Vídeo).

Claus noves que no tenia ../aee — cap per a l'MVP; si s'afegeixen pàgines en futures iteracions, traduir a mesura.


9. Fases d'implementació

Cada fase és un commit. Ordre perquè cada pas compila i s'executa.

Fase 1 — setters d'àudio a Options::

Preparatori; sense això la pàgina Àudio no pot mutar res.

  • Afegir setAudioEnabled, setAudioVolume, setAudioMusicEnabled, setAudioMusicVolume, setAudioSoundEnabled, setAudioSoundVolume a options.hpp/.cpp. Patró exacte de setVideoVSync: muten estat intern + saveToFile().
  • Exposar un mètode de reconfiguració en calent a core/audio/ (Audio::applyConfig o similar) si no hi és ja.
  • Smoke test: xvfb-run 5 s, joc arrenca i música sona.

Fase 2 — hotkey i esquelet ServiceMenu

  • Afegir menu_toggle a data/input/keys.yaml amb F9.
  • Crear service_menu.{hpp,cpp} amb API mínima: init, toggle, close, isOpen, isVisible, update, render (render de caixa buida amb títol fix "Opcions"), handleEvent (només F9, Escape, Backspace).
  • Al Director: construir font ari-w9500-display, inicialitzar ServiceMenu::get().
  • A GameScene::update(): bloc d'input abans de GlobalInputs::update (veure §4). A GameScene::render(): ServiceMenu::get().render() abans de Screen::present(). No replicar encara a Logo/Title/Intro — el menú només té sentit quan l'usuari juga.
  • Smoke test: F9 obri caixa centrada, Escape/F9 la tanca.

Fase 3 — framework de pàgines

  • Portar Item, Page, ItemKind, stack_, pushPage/popPage, nextVisibleCursor, isVisible(Item), handleKey de ../aee (quasi literalment).
  • Portar renderPageContent simplificat (sense transicions ni clipping avançat al principi): render de títol, línia, items amb cursor i valor.
  • Afegir les claus menu.titles.root i menu.values.* a ca.yaml/en.yaml.
  • Implementar buildRoot() amb tres entrades Submenu buides (es naveguen però totes porten a pàgines sense items visibles, text "(Buit)").
  • Smoke test: navegació amunt/avall, enter a submenú, backspace torna.

Fase 4 — Pàgina Vídeo

  • Implementar buildVideo() amb els items de §5.
  • Copiar el helper reapplyShader() de global_inputs.cpp a un lloc compartit (p. ex. game/options_apply.{hpp,cpp}) perquè el cridin tant global_inputs com el menú, sense duplicar la lògica.
  • Afegir claus menu.items.{zoom,screen,vsync,scaling_mode,shader,shader_type,preset,render_info,uptime}.
  • Smoke test: obrir menú, toggle vsync, canvi d'escala, comprovar que la notificació NO apareix (és el menú qui fa el canvi, no les hotkeys).

Fase 5 — Pàgina Àudio

  • buildAudio() amb els 6 items. Helpers volPct/stepVolume copiats de ../aee.
  • Claus menu.items.{master_enable,master_volume,music,music_volume,sounds,sounds_volume}.
  • Smoke test: baixar volum de música, pujar volum de sons.

Fase 6 — Pàgina Sistema

  • buildSystem() amb subtítol de versió i exit_game que injecta SDL_EVENT_QUIT.
  • Smoke test: obrir sistema, exit_game tanca el joc net.

Fase 7 (opcional) — polit

  • Animació d'obertura/tancament (porta les constants OPEN_SPEED, CLOSE_SPEED, HEIGHT_RATE, TRANSITION_SPEED de ../aee).
  • blendRect a Screen:: per a fons semi-transparent.
  • Replicar el bloc d'input del menú a Title/Intro/Logo si s'acorda que el menú hi és accessible també.
  • Traduccions EN si no es van fer abans.

10. Tests

  • Smoke (xvfb-run 5 s) després de cada fase — memòria feedback_smoke_test_mandatory.
  • Unitaris amb Catch2 si el framework està muntat (veure tests/ actual):
    • ServiceMenu::toggle() idempotent després de close().
    • nextVisibleCursor salta items ocults.
    • handleEvent retorna true per a KEY_DOWN quan està obert, false quan està tancat.
  • Manual:
    • Obrir F9 → tancar F9 → obrir de nou (no leak d'estat).
    • Entrar a Vídeo → Shader off → verificar que shader_type i preset desapareixen (anim o salt).
    • Tancar el joc, re-obrir, verificar que config.yaml conserva els canvis (els fa Options::setX).

11. Riscos i punts oberts

  • Input routing als altres Scenes. Si l'usuari vol obrir el menú durant el LOGO/TITLE/INTRO, cal replicar el bloc d'input. Decisió: no. Motiu: són placeholders amb 5 s de durada; obrir un menú allà és overkill. Anotar-ho a la capçalera de service_menu.hpp.
  • EventBuffer::events.erase(it) invalida els iteradors — mà amb el loop. Patró amb it = events.erase(it) ↔ ++it és segur.
  • Blend rect vs. GPU pipeline. Si el shader PostFX reprocessa el canvas, el menú ha d'estar pintat abans del shader (i ja ho està, perquè el canvas ARGB va al shader). Res a fer; tot passa per Screen::present() al final.
  • Captura de tecla (KeyBind). No s'implementa aquesta iteració (§5). Si es porta, l'event router ha de distingir "menú obert i capturant" — el menú consumeix tota tecla, no només Enter/Backspace/etc.
  • ari-w9500-display és paletada. Ha de compartir la paleta ui amb la resta d'overlays. Verificar que el GIF només té la tinta a l'índex Text::INK_COLOR_INDEX = 1; si no, el font_gen l'ha de regenerar (veure memòria reference_font_generator).
  • F9 lliure? Abans d'agafar F9 per al menu_toggle, confirmar que cap altra hotkey hi és mapejada en builds futures.

12. Resum d'arxius a tocar

Nous:

  • source/game/ui/service_menu.hpp
  • source/game/ui/service_menu.cpp
  • (opcional) source/game/options_apply.{hpp,cpp} per compartir reapplyShader amb global_inputs.cpp.

Modificats:

  • source/utils/defaults.hpp — afegir índexs UI::MENU_* + UI::Font::MENU_*.
  • source/game/options.hpp/.cpp — setters d'àudio (fase 1).
  • source/core/system/director.cpp — carregar font menú + ServiceMenu::get().init(...).
  • source/game/scenes/game_scene.cpp — bloc d'input del menú a update, render del menú a render abans de present.
  • data/input/keys.yaml — afegir menu_toggle: F9.
  • data/locale/ca.yaml, data/locale/en.yaml — secció menu:.
  • CMakeLists.txt o source/CMakeLists.txt — afegir el nou service_menu.cpp.

Sense tocar per a l'MVP:

  • source/core/input/key_config.* (no cal scancodePtr).
  • source/core/audio/* si ja exposa configuració en calent; si no, mínima adició.
  • LogoScene/TitleScene/IntroScene.