22 KiB
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
ItemiPageambItemKind::{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_dataambblendRect/fillRect/drawBorder. buildRoot/buildVideo/buildAudio/buildControls/buildGame/buildSystem.
- Struct
- Claus de
locale/ca.yamlsotamenu.{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:
- 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 deScreen::get().present(), al costat de Notifier/RenderInfo. - 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). - Els setters de
Options::ja persisteixen. El menú cridaOptions::setX(v); Screen::setX(v);com faglobal_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 tocaOptions::iScreen::del joc; no és un primitiu de renderització comTextoNotifier. - Exposa l'API com a singleton (
ServiceMenu::get()) per coherència ambNotifieriRenderInfo. init()repstd::shared_ptr<Text>+Palette ui_palette, igual queNotifier::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_infoes 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 sishader.enabled == true.uptime→ només sirender_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}— constantsconstexpr, no mutables. KeyConfigactual no exposascancodePtr(id).
Opcions, de menys a més invasiu:
- No portar Controls en aquesta iteració (recomanat per a la primera versió).
- Migrar
KeysGameaOptions::+ afegirKeyConfig::scancodePtr(id)per a hotkeys. Requereix tocarPlayer::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
fillRectd'1 px amb colorui_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
../aeeperò ambBG_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,setAudioSoundVolumeaoptions.hpp/.cpp. Patró exacte desetVideoVSync: muten estat intern +saveToFile(). - Exposar un mètode de reconfiguració en calent a
core/audio/(Audio::applyConfigo similar) si no hi és ja. - Smoke test:
xvfb-run5 s, joc arrenca i música sona.
Fase 2 — hotkey i esquelet ServiceMenu
- Afegir
menu_toggleadata/input/keys.yamlamb 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, inicialitzarServiceMenu::get(). - A
GameScene::update(): bloc d'input abans deGlobalInputs::update(veure §4). AGameScene::render():ServiceMenu::get().render()abans deScreen::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),handleKeyde../aee(quasi literalment). - Portar
renderPageContentsimplificat (sense transicions ni clipping avançat al principi): render de títol, línia, items amb cursor i valor. - Afegir les claus
menu.titles.rootimenu.values.*aca.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()deglobal_inputs.cppa un lloc compartit (p. ex.game/options_apply.{hpp,cpp}) perquè el cridin tantglobal_inputscom 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. HelpersvolPct/stepVolumecopiats 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ó iexit_gameque injectaSDL_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_SPEEDde../aee). blendRectaScreen::per a fons semi-transparent.- Replicar el bloc d'input del menú a
Title/Intro/Logosi 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 declose().nextVisibleCursorsalta items ocults.handleEventretornatrueper a KEY_DOWN quan està obert,falsequan està tancat.
- Manual:
- Obrir F9 → tancar F9 → obrir de nou (no leak d'estat).
- Entrar a Vídeo → Shader off → verificar que
shader_typeipresetdesapareixen (anim o salt). - Tancar el joc, re-obrir, verificar que
config.yamlconserva els canvis (els faOptions::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ó ambit = 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 paletauiamb la resta d'overlays. Verificar que el GIF només té la tinta a l'índexText::INK_COLOR_INDEX = 1; si no, el font_gen l'ha de regenerar (veure memòriareference_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.hppsource/game/ui/service_menu.cpp- (opcional)
source/game/options_apply.{hpp,cpp}per compartirreapplyShaderambglobal_inputs.cpp.
Modificats:
source/utils/defaults.hpp— afegir índexsUI::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ú aupdate, render del menú arenderabans depresent.data/input/keys.yaml— afegirmenu_toggle: F9.data/locale/ca.yaml,data/locale/en.yaml— secciómenu:.CMakeLists.txtosource/CMakeLists.txt— afegir el nouservice_menu.cpp.
Sense tocar per a l'MVP:
source/core/input/key_config.*(no calscancodePtr).source/core/audio/*si ja exposa configuració en calent; si no, mínima adició.- LogoScene/TitleScene/IntroScene.