Refactor menu structure and add Flutter-specific input and persistence layers

- Moved menu-related classes to a new structure under `src/menu/`.
- Introduced `WolfMenuPresentation` to handle menu art and mappings.
- Added `MenuManager` tests to ensure menu state reflects game status.
- Implemented `FlutterRendererSettingsPersistence` and `FlutterSaveGamePersistence` for managing settings and save files on desktop platforms.
- Created `Wolf3dFlutterInput` to handle keyboard and mouse input in a Flutter environment.
- Updated README to reflect new package structure and usage instructions.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-24 18:45:34 +01:00
parent 9f3651b122
commit 5c309c2240
37 changed files with 2356 additions and 1565 deletions
+216 -1
View File
@@ -22,7 +22,7 @@ Primary entry libraries in `lib/`:
- `wolf_3d_renderer.dart` — rendering/backends integration points.
- `wolf_3d_audio.dart` — audio interfaces and host backends.
- `wolf_3d_input.dart` — input abstractions.
- `wolf_3d_menu.dart` — menu models/managers and shared `WolfMenuPalette` color accessors for hosts.
- `wolf_3d_menu.dart` — menu models/managers and the registry-backed `WolfMenuPresentation` helpers for hosts.
- `wolf_3d_host.dart` — host-level glue contracts.
Implementation details live under `lib/src/`.
@@ -50,6 +50,221 @@ dart test
- This package owns deterministic engine/frame progression and shared game logic.
- Frame-buffer sizing is controlled by hosts through engine APIs.
- Rendering code is maintained under `lib/src/rendering/`.
- Menu coordination is split under `lib/src/menu/manager/`; public consumers should prefer `lib/wolf_3d_menu.dart` or the internal barrel at `lib/src/menu/menu_manager.dart` instead of reaching into individual implementation files.
- Menu presentation is selected through `AssetRegistry.menuPresentation`, which keeps retail/shareware/Spear variants and user-defined menu overrides aligned with the rest of the registry system.
## Custom Menus
Custom menu support is split across two registry modules:
- `MenuPicModule` maps symbolic menu keys such as `MenuPicKey.title` or an episode selection entry to concrete VGA picture indices in `WolfensteinData.vgaImages`.
- `MenuPresentationModule` defines the palette indices and higher-level menu art lookups that renderers and hosts consume.
That split is intentional:
- `MenuPicModule` answers "which image index represents this menu asset for this game/mod?"
- `MenuPresentationModule` answers "which colors and optional art should the UI use?"
In practice, most custom variants will either:
- reuse an existing `MenuPicModule` and only change colors/presentation, or
- provide both a custom `MenuPicModule` and a matching `MenuPresentationModule` when the menu art layout itself changes.
### Using Menu Presentation From Loaded Data
Once game data has been loaded, bind menu presentation through the active registry:
```dart
final WolfMenuPresentation menu = WolfMenuPresentation(data);
final int panelColor = menu.panelColor;
final VgaImage? title = menu.title;
final VgaImage? episode1 = menu.episodeOption(0);
```
This is the normal path for renderers and any UI that should track the active game variant automatically.
### Fallback Presentation Before Data Loads
Host-owned screens that appear before game data discovery can still use menu-consistent colors:
```dart
const WolfMenuPresentation classicMenu = WolfMenuPresentation.classic();
const WolfMenuPresentation spearMenu = WolfMenuPresentation.spear();
```
Those fallback constructors expose colors without requiring a loaded `WolfensteinData` instance. Art getters return `null` until real data is attached.
### Implementing A Custom MenuPicModule
Use `MenuPicModule` when your mod changes which VGA pictures back the classic menu keys:
```dart
class ModMenuPics extends MenuPicModule {
const ModMenuPics();
@override
MenuPicRef? resolve(MenuPicKey key) {
switch (key) {
case MenuPicKey.title:
return const MenuPicRef(140);
case MenuPicKey.optionTitle:
return const MenuPicRef(141);
case MenuPicKey.customizeTitle:
return const MenuPicRef(142);
default:
return null;
}
}
@override
MenuPicKey episodeKey(int episodeIndex) {
switch (episodeIndex) {
case 0:
return MenuPicKey.episode1;
case 1:
return MenuPicKey.episode2;
case 2:
return MenuPicKey.episode3;
default:
return MenuPicKey.episode1;
}
}
@override
MenuPicKey difficultyKey(Difficulty difficulty) {
switch (difficulty) {
case Difficulty.easy:
return MenuPicKey.skill1;
case Difficulty.medium:
return MenuPicKey.skill2;
case Difficulty.hard:
return MenuPicKey.skill3;
case Difficulty.expert:
return MenuPicKey.skill4;
}
}
}
```
Returning `null` from `resolve` means that the key is not provided by that module.
### Implementing A Custom MenuPresentationModule
Use `MenuPresentationModule` when you want to change menu colors, point existing menu concepts at different art, or selectively omit optional art:
### Custom Menu Presentation Example
```dart
class ModMenuPresentation extends MenuPresentationModule {
const ModMenuPresentation();
@override
int get backgroundIndex => 111;
@override
int get panelIndex => 97;
@override
int get borderIndex => 87;
@override
int get emphasisIndex => 10;
@override
int get warningIndex => 14;
@override
int get mutedIndex => 8;
@override
int get selectedTextIndex => 19;
@override
int get unselectedTextIndex => 23;
@override
int get disabledTextIndex => 4;
@override
int get headerTextIndex => 15;
@override
VgaImage? controlBackground(WolfensteinData data) => null;
@override
VgaImage? title(WolfensteinData data) => null;
@override
VgaImage? heading(WolfensteinData data) => null;
@override
VgaImage? selectedMarker(WolfensteinData data) => null;
@override
VgaImage? unselectedMarker(WolfensteinData data) => null;
@override
VgaImage? optionsLabel(WolfensteinData data) => null;
@override
VgaImage? customizeLabel(WolfensteinData data) => null;
@override
VgaImage? credits(WolfensteinData data) => null;
@override
VgaImage? episodeOption(WolfensteinData data, int episodeIndex) => null;
@override
VgaImage? difficultyOption(WolfensteinData data, Difficulty difficulty) =>
null;
@override
VgaImage? mappedPic(WolfensteinData data, int index) => null;
}
final registry = AssetRegistry(
sfx: mySfxModule,
music: myMusicModule,
entities: myEntityModule,
hud: myHudModule,
menu: myMenuPicModule,
menuPresentation: const ModMenuPresentation(),
);
```
The presentation module should treat its image-returning methods as optional hooks:
- return a `VgaImage` when that surface has variant-specific art,
- return `null` when the presentation intentionally has no image for that concept,
- use `mappedPic(...)` only for legacy numeric menu art lookups that still matter for a renderer path.
### Wiring A Fully Custom Registry
To ship a complete custom menu variant, provide both modules through `AssetRegistry` when loading data:
```dart
final registry = AssetRegistry(
sfx: mySfxModule,
music: myMusicModule,
entities: myEntityModule,
hud: myHudModule,
menu: const ModMenuPics(),
menuPresentation: const ModMenuPresentation(),
);
```
If your menu art still follows the built-in retail/shareware layout, you may not need a custom `MenuPicModule`. In that case, keep the built-in module and only swap `menuPresentation`.
### Choosing The Right Extension Point
- Change colors only: implement `MenuPresentationModule`.
- Change symbolic menu art mapping: implement `MenuPicModule`.
- Change both colors and art layout: implement both modules.
- Build a host setup screen before data loads: use `WolfMenuPresentation.classic()` or `WolfMenuPresentation.spear()`.
For most host code, prefer the public `wolf_3d_menu.dart` surface instead of importing internal files directly.
## Non-Goals