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

473 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](../../aee/source/core/rendering/menu.hpp) — API
pública (`init/destroy/toggle/close/isOpen/isVisible/render/handleKey`).
- [source/core/rendering/menu.cpp](../../aee/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:
```cpp
// 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`:
```yaml
- id: menu_toggle
code: "F9"
desc: "Obri/tanca el menú de servei"
```
Flux d'input (escena GameScene::update):
```cpp
// 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::`:
```cpp
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](../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:
```cpp
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.