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
+13 -1
View File
@@ -41,13 +41,25 @@ final Wolf3dFlutterEngine engine = await Wolf3dFlutterEngine(
`init()` handles platform setup, audio init, and configured game-data discovery.
The facade itself lives in `lib/engine/wolf3d_flutter_engine.dart` and is re-exported
through the package barrel at `lib/wolf_3d_flutter.dart`. External consumers
should keep importing the barrel unless they have a specific reason to target
the engine library directly.
The same pattern applies to the Flutter input adapter and the desktop
persistence adapters: they now live in focused subdirectories and are
re-exported through `lib/wolf_3d_flutter.dart`.
For full host wiring examples, see:
- `apps/wolf_3d_gui/lib/main.dart`
## Package Structure
- `lib/wolf_3d_flutter.dart`exports main host facade and public Flutter APIs.
- `lib/wolf_3d_flutter.dart`barrel export for the public Flutter package surface.
- `lib/engine/wolf3d_flutter_engine.dart` — high-level engine facade and discovery bootstrap.
- `lib/input/` — Flutter-specific input adapters.
- `lib/persistence/` — desktop persistence adapters for saves and renderer settings.
- `lib/renderer/` — renderer host widgets.
- `lib/managers/` — runtime/session/display/persistence managers.
- `lib/audio/` — platform-aware audio backends.
@@ -0,0 +1,122 @@
/// Flutter-specific engine facade for discovery and runtime service wiring.
library;
import 'package:flutter/foundation.dart';
import 'package:wolf_3d_dart/wolf_3d_data.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_flutter/audio/wolf3d_platform_audio.dart';
import 'package:wolf_3d_flutter/input/wolf_3d_input_flutter.dart';
import 'package:wolf_3d_flutter/managers/desktop_windowing_support.dart'
as desktop_windowing_support;
import 'package:wolf_3d_flutter/managers/game_data_directory_persistence.dart';
/// Flutter-specific host facade built on top of [Wolf3dEngine].
///
/// This type keeps platform-neutral session and engine state in
/// `wolf_3d_dart` while owning Flutter-only concerns such as platform
/// discovery helpers and persisted data-directory lookup.
class Wolf3dFlutterEngine extends Wolf3dEngine {
/// Creates an empty facade that must be initialized with [init].
Wolf3dFlutterEngine({
bool debug = false,
EngineAudio? audioBackend,
Wolf3dFlutterInput? inputBackend,
DefaultGameDataDirectoryPersistence? dataDirectoryPersistence,
}) : dataDirectoryPersistence =
dataDirectoryPersistence ?? DefaultGameDataDirectoryPersistence(),
super(
audio: audioBackend ?? Wolf3dPlatformAudio(),
input: inputBackend ?? Wolf3dFlutterInput(),
) {
if (debug) {
enableDebug();
}
}
/// Persists and restores the preferred external game-data directory.
final DefaultGameDataDirectoryPersistence dataDirectoryPersistence;
/// Last configured or loaded external game-data directory path.
String? configuredDataDirectory;
/// Shared Flutter input adapter reused by gameplay screens.
@override
Wolf3dFlutterInput get input => super.input as Wolf3dFlutterInput;
/// Enables host-level debug affordances such as debug navigation UI.
@override
Wolf3dFlutterEngine enableDebug() {
super.enableDebug();
return this;
}
/// Initializes the engine by loading available game data.
///
/// If [directory] is provided, it is persisted and treated as the primary
/// external search root. If omitted, a previously persisted directory is
/// used when available. [additionalDirectories] are scanned after the
/// primary directory and are not persisted.
///
/// This method scans only configured external directories, deduplicating
/// discovered versions by [GameVersion]. Shared package code does not bundle
/// or import game data on behalf of host applications.
Future<Wolf3dFlutterEngine> init({
String? directory,
Iterable<String>? additionalDirectories,
}) async {
await desktop_windowing_support.ensureDesktopWindowingInitialized();
await audio.init();
availableGames.clear();
final String? requestedDirectory = directory?.trim();
final String? resolvedDirectory =
requestedDirectory != null && requestedDirectory.isNotEmpty
? requestedDirectory
: await dataDirectoryPersistence.loadDataDirectory();
configuredDataDirectory = resolvedDirectory;
if (requestedDirectory != null && requestedDirectory.isNotEmpty) {
await dataDirectoryPersistence.saveDataDirectory(requestedDirectory);
}
// On non-web platforms, also scan local filesystem locations for
// user-supplied data folders so the host can pick up extra versions.
final Set<String> directoriesToScan = <String>{};
if (resolvedDirectory != null && resolvedDirectory.isNotEmpty) {
directoriesToScan.add(resolvedDirectory);
}
if (additionalDirectories != null) {
for (final String directoryPath in additionalDirectories) {
final String trimmedPath = directoryPath.trim();
if (trimmedPath.isNotEmpty) {
directoriesToScan.add(trimmedPath);
}
}
}
if (!kIsWeb && directoriesToScan.isNotEmpty) {
for (final String directoryPath in directoriesToScan) {
try {
final externalGames = await WolfensteinLoader.discover(
directoryPath: directoryPath,
recursive: true,
);
for (final MapEntry<GameVersion, WolfensteinData> entry
in externalGames.entries) {
if (!availableGames.any(
(WolfensteinData g) => g.version == entry.key,
)) {
availableGames.add(entry.value);
}
}
} catch (e) {
debugPrint('External discovery failed: $e');
}
}
}
return this;
}
}
@@ -5,8 +5,8 @@ import 'dart:async';
import 'package:flutter/services.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
import 'package:wolf_3d_flutter/input/wolf_3d_input_flutter.dart';
import 'package:wolf_3d_flutter/managers/game_renderer_mode_manager.dart';
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
/// Semantic actions that host-level shortcuts can trigger.
///
@@ -1,19 +1,11 @@
/// High-level Flutter facade for discovering game data and sharing runtime services.
library;
import 'package:flutter/foundation.dart';
import 'package:wolf_3d_dart/wolf_3d_data.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_flutter/audio/wolf3d_platform_audio.dart';
import 'package:wolf_3d_flutter/managers/desktop_windowing_support.dart'
as desktop_windowing_support;
import 'package:wolf_3d_flutter/managers/game_data_directory_persistence.dart';
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
export 'package:wolf_3d_dart/wolf_3d_audio.dart' show DebugMusicPlayer;
export 'audio/wolf3d_platform_audio.dart' show Wolf3dPlatformAudio;
export 'engine/wolf3d_flutter_engine.dart' show Wolf3dFlutterEngine;
export 'input/wolf_3d_input_flutter.dart' show Wolf3dFlutterInput;
export 'managers/game_app_lifecycle_manager.dart' show GameAppLifecycleManager;
export 'managers/game_data_directory_persistence.dart'
show DefaultGameDataDirectoryPersistence;
@@ -29,6 +21,10 @@ export 'managers/game_screen_input_manager.dart'
HostShortcutRegistry,
GameScreenInputManager,
isAltEnterShortcut;
export 'persistence/renderer_settings_persistence_flutter.dart'
show FlutterRendererSettingsPersistence;
export 'persistence/save_game_persistence_flutter.dart'
show FlutterSaveGamePersistence;
export 'screens/audio_gallery.dart' show AudioGallery;
export 'screens/debug_tools_screen.dart' show DebugToolsScreen;
export 'screens/game_screen.dart' show GameScreen;
@@ -37,109 +33,3 @@ export 'screens/vga_gallery.dart' show VgaGallery;
export 'widgets/gallery_game_selector.dart'
show GalleryGameSelector, formatGalleryGameTitle;
export 'widgets/wolf_menu_shell.dart' show WolfMenuShell;
/// Flutter-specific host facade built on top of [Wolf3dEngine].
///
/// This type keeps platform-neutral session/engine state in the Dart package
/// while owning Flutter-only concerns such as platform discovery helpers.
class Wolf3dFlutterEngine extends Wolf3dEngine {
/// Creates an empty facade that must be initialized with [init].
Wolf3dFlutterEngine({
bool debug = false,
EngineAudio? audioBackend,
Wolf3dFlutterInput? inputBackend,
DefaultGameDataDirectoryPersistence? dataDirectoryPersistence,
}) : dataDirectoryPersistence =
dataDirectoryPersistence ?? DefaultGameDataDirectoryPersistence(),
super(
audio: audioBackend ?? Wolf3dPlatformAudio(),
input: inputBackend ?? Wolf3dFlutterInput(),
) {
if (debug) {
enableDebug();
}
}
/// Persists and restores the preferred external game-data directory.
final DefaultGameDataDirectoryPersistence dataDirectoryPersistence;
/// Last configured/loaded external game-data directory path.
String? configuredDataDirectory;
/// Shared Flutter input adapter reused by gameplay screens.
@override
Wolf3dFlutterInput get input => super.input as Wolf3dFlutterInput;
/// Enables host-level debug affordances such as debug navigation UI.
@override
Wolf3dFlutterEngine enableDebug() {
super.enableDebug();
return this;
}
/// Initializes the engine by loading available game data.
///
/// If [directory] is provided, it is persisted and treated as the primary
/// external search root. If omitted, a previously persisted directory is
/// used when available. [additionalDirectories] are scanned after the
/// primary directory and are not persisted.
///
/// This method scans only configured external directories, deduplicating
/// discovered versions by [GameVersion]. Shared package code does not bundle
/// or import game data on behalf of host applications.
Future<Wolf3dFlutterEngine> init({
String? directory,
Iterable<String>? additionalDirectories,
}) async {
await desktop_windowing_support.ensureDesktopWindowingInitialized();
await audio.init();
availableGames.clear();
final String? requestedDirectory = directory?.trim();
final String? resolvedDirectory =
requestedDirectory != null && requestedDirectory.isNotEmpty
? requestedDirectory
: await dataDirectoryPersistence.loadDataDirectory();
configuredDataDirectory = resolvedDirectory;
if (requestedDirectory != null && requestedDirectory.isNotEmpty) {
await dataDirectoryPersistence.saveDataDirectory(requestedDirectory);
}
// On non-web platforms, also scan local filesystem locations for
// user-supplied data folders so the host can pick up extra versions.
final Set<String> directoriesToScan = <String>{};
if (resolvedDirectory != null && resolvedDirectory.isNotEmpty) {
directoriesToScan.add(resolvedDirectory);
}
if (additionalDirectories != null) {
for (final String directoryPath in additionalDirectories) {
final String trimmedPath = directoryPath.trim();
if (trimmedPath.isNotEmpty) {
directoriesToScan.add(trimmedPath);
}
}
}
if (!kIsWeb && directoriesToScan.isNotEmpty) {
for (final String directoryPath in directoriesToScan) {
try {
final externalGames = await WolfensteinLoader.discover(
directoryPath: directoryPath,
recursive: true,
);
for (var entry in externalGames.entries) {
if (!availableGames.any((g) => g.version == entry.key)) {
availableGames.add(entry.value);
}
}
} catch (e) {
debugPrint("External discovery failed: $e");
}
}
}
return this;
}
}