feat: Refactor to use Wolf3dFlutterEngine across the application

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 18:56:51 +01:00
parent 5a2681e89b
commit 5ef59d9980
14 changed files with 273 additions and 223 deletions
@@ -24,7 +24,7 @@ class _AudioRow {
/// Displays all decoded SFX and music tracks for the selected game data.
class AudioGallery extends StatefulWidget {
/// Shared app facade used to access game assets and the audio backend.
final Wolf3d wolf3d;
final Wolf3dFlutterEngine wolf3d;
const AudioGallery({super.key, required this.wolf3d});
@@ -7,7 +7,7 @@ import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
/// Presents debug-only navigation shortcuts for asset galleries.
class DebugToolsScreen extends StatelessWidget {
/// Shared app facade used to access active game assets.
final Wolf3d wolf3d;
final Wolf3dFlutterEngine wolf3d;
/// Creates the debug tools screen for [wolf3d].
const DebugToolsScreen({super.key, required this.wolf3d});
@@ -11,10 +11,11 @@ import 'package:wolf_3d_flutter/renderer/wolf_3d_flutter_renderer.dart';
import 'package:wolf_3d_flutter/renderer/wolf_3d_glsl_renderer.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
/// Launches a [WolfEngine] via [Wolf3d] and exposes renderer/input integrations.
/// Launches a [WolfEngine] via [Wolf3dFlutterEngine] and exposes
/// renderer/input integrations.
class GameScreen extends StatefulWidget {
/// Shared application facade owning the engine, audio, and input.
final Wolf3d wolf3d;
final Wolf3dFlutterEngine wolf3d;
/// Optional host-level shortcut override.
///
@@ -10,7 +10,7 @@ import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
/// Displays every sprite frame in the active game along with enemy metadata.
class SpriteGallery extends StatefulWidget {
/// Shared application facade used to access the active game's sprite set.
final Wolf3d wolf3d;
final Wolf3dFlutterEngine wolf3d;
/// Creates the sprite gallery for [wolf3d].
const SpriteGallery({super.key, required this.wolf3d});
@@ -9,7 +9,7 @@ import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
/// Shows each VGA image extracted from the currently selected game data set.
class VgaGallery extends StatefulWidget {
/// Shared app facade used to access available game data sets.
final Wolf3d wolf3d;
final Wolf3dFlutterEngine wolf3d;
/// Creates the gallery for the currently selected or browsed game.
const VgaGallery({super.key, required this.wolf3d});
@@ -20,7 +20,7 @@ String formatGalleryGameTitle(GameVersion version) {
/// Selects which discovered game data set gallery screens should display.
class GalleryGameSelector extends StatelessWidget {
final Wolf3d wolf3d;
final Wolf3dFlutterEngine wolf3d;
final WolfensteinData selectedGame;
final ValueChanged<WolfensteinData> onSelected;
@@ -3,10 +3,11 @@ library;
import 'package:flutter/material.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
/// Minimal app shell that binds a prepared [Wolf3d] instance to host screens.
/// Minimal app shell that binds a prepared [Wolf3dFlutterEngine] instance to
/// host screens.
class Wolf3dApp extends StatelessWidget {
/// Shared initialized facade that owns game data, input, and audio services.
final Wolf3d wolf3d;
final Wolf3dFlutterEngine wolf3d;
const Wolf3dApp({
super.key,
+28 -203
View File
@@ -37,203 +37,40 @@ export 'widgets/gallery_game_selector.dart'
export 'widgets/wolf3d_app.dart' show Wolf3dApp;
export 'widgets/wolf_menu_shell.dart' show WolfMenuShell;
/// Coordinates asset discovery, audio initialization, and input reuse for apps.
class Wolf3d {
/// 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 bundle loading and discovery.
class Wolf3dFlutterEngine extends Wolf3dEngine {
/// Creates an empty facade that must be initialized with [init].
Wolf3d({EngineAudio? audioBackend})
: audio = audioBackend ?? Wolf3dPlatformAudio();
/// All successfully discovered or bundled game data sets.
final List<WolfensteinData> availableGames = [];
WolfensteinData? _activeGame;
/// Shared engine audio backend used by menus and gameplay sessions.
final EngineAudio audio;
Future<void>? _audioShutdownFuture;
/// Engine menu background color as 24-bit RGB.
int menuBackgroundRgb = 0x890000;
/// Engine menu panel color as 24-bit RGB.
int menuPanelRgb = 0x590002;
Wolf3dFlutterEngine({
EngineAudio? audioBackend,
Wolf3dFlutterInput? inputBackend,
}) : super(
audio: audioBackend ?? Wolf3dPlatformAudio(),
input: inputBackend ?? Wolf3dFlutterInput(),
);
/// Shared Flutter input adapter reused by gameplay screens.
final Wolf3dFlutterInput input = Wolf3dFlutterInput();
bool _debugEnabled = false;
/// Whether host-level debug affordances should be visible.
bool get isDebugEnabled => _debugEnabled;
@override
Wolf3dFlutterInput get input => super.input as Wolf3dFlutterInput;
/// Enables host-level debug affordances such as debug navigation UI.
Wolf3d enableDebug() {
_debugEnabled = true;
@override
Wolf3dFlutterEngine enableDebug() {
super.enableDebug();
return this;
}
/// The currently selected game data set.
///
/// Throws a [StateError] until [setActiveGame] has been called.
WolfensteinData get activeGame {
if (_activeGame == null) {
throw StateError("No active game selected. Call setActiveGame() first.");
}
return _activeGame!;
}
/// Nullable access to the selected game, useful during menu bootstrap.
WolfensteinData? get maybeActiveGame => _activeGame;
// Episode selection lives on the facade so menus can configure gameplay
// before constructing a new engine instance.
int? _activeEpisode;
/// Index of the episode currently selected in the UI flow.
int? get activeEpisode => _activeEpisode;
Difficulty? _activeDifficulty;
/// The difficulty applied when [launchEngine] creates a new session.
Difficulty? get activeDifficulty => _activeDifficulty;
/// Stores [difficulty] so the next [launchEngine] call uses it.
void setActiveDifficulty(Difficulty difficulty) {
_activeDifficulty = difficulty;
}
/// Clears any previously selected difficulty so the engine can prompt for one.
void clearActiveDifficulty() {
_activeDifficulty = null;
}
WolfEngine? _engine;
/// The most recently launched engine.
///
/// Throws a [StateError] until [launchEngine] has been called.
WolfEngine get engine {
if (_engine == null) {
throw StateError('No engine launched. Call launchEngine() first.');
}
return _engine!;
}
/// Creates and initializes a [WolfEngine] for the current session config.
///
/// Uses [activeGame], [activeEpisode], and [activeDifficulty]. Stores the
/// engine so it can be retrieved via [engine]. [onGameWon] is invoked when
/// the player completes the final level of the episode.
WolfEngine launchEngine({
required void Function() onGameWon,
void Function()? onQuit,
SaveGamePersistence? saveGamePersistence,
WolfRendererCapabilities? rendererCapabilities,
WolfRendererSettings? rendererSettings,
void Function(WolfRendererSettings settings)? onRendererSettingsChanged,
}) {
if (availableGames.isEmpty) {
throw StateError(
'No game data was discovered. Add game files before launching the engine.',
);
}
_engine = WolfEngine(
availableGames: availableGames,
difficulty: _activeDifficulty,
startingEpisode: _activeEpisode,
frameBuffer: FrameBuffer(320, 200),
menuBackgroundRgb: menuBackgroundRgb,
menuPanelRgb: menuPanelRgb,
engineAudio: audio,
input: input,
onGameWon: onGameWon,
// In Flutter we keep the renderer screen active while browsing menus,
// so backing out of the top-level menu should not pop the route.
onMenuExit: () {},
onQuit: onQuit,
saveGamePersistence: saveGamePersistence,
rendererCapabilities: rendererCapabilities,
rendererSettings: rendererSettings,
onRendererSettingsChanged: onRendererSettingsChanged,
onGameSelected: (game) {
_activeGame = game;
audio.activeGame = game;
},
onEpisodeSelected: (episodeIndex) {
_activeEpisode = episodeIndex;
},
);
_engine!.init();
return _engine!;
}
/// Sets the active episode for the current [activeGame].
void setActiveEpisode(int episodeIndex) {
if (_activeGame == null) {
throw StateError("No active game selected. Call setActiveGame() first.");
}
if (episodeIndex < 0 || episodeIndex >= _activeGame!.episodes.length) {
throw RangeError("Episode index out of range for the active game.");
}
_activeEpisode = episodeIndex;
}
/// Clears any selected episode so menu flow starts fresh.
void clearActiveEpisode() {
_activeEpisode = null;
}
/// Convenience access to the active episode's level list.
List<WolfLevel> get levels {
if (_activeEpisode == null) {
throw StateError('No active episode selected.');
}
return activeGame.episodes[_activeEpisode!].levels;
}
/// Convenience access to the active game's wall textures.
List<Sprite> get walls => activeGame.walls;
/// Convenience access to the active game's sprite set.
List<Sprite> get sprites => activeGame.sprites;
/// Convenience access to digitized PCM effects.
List<PcmSound> get sounds => activeGame.sounds;
/// Convenience access to AdLib/OPL effect assets.
List<PcmSound> get adLibSounds => activeGame.adLibSounds;
/// Convenience access to level music tracks.
List<ImfMusic> get music => activeGame.music;
/// Convenience access to VGA UI and splash images.
List<VgaImage> get vgaImages => activeGame.vgaImages;
/// Makes [game] the active data set and points shared services at it.
void setActiveGame(WolfensteinData game) {
if (!availableGames.contains(game)) {
throw ArgumentError(
"The provided game data is not in the list of available games.",
);
}
if (_activeGame == game) {
return; // No change needed
}
_activeGame = game;
_activeEpisode = null;
audio.activeGame = game;
}
/// Initializes the engine by loading available game data.
///
/// Set [debug] to `true` to explicitly enable host-level debug affordances.
Future<Wolf3d> init({String? directory, bool debug = false}) async {
Future<Wolf3dFlutterEngine> init({
String? directory,
bool debug = false,
}) async {
if (debug) {
_debugEnabled = true;
enableDebug();
}
await audio.init();
availableGames.clear();
@@ -288,24 +125,6 @@ class Wolf3d {
return this;
}
/// Stops and disposes shared audio exactly once for app shutdown.
///
/// Repeated calls return the same in-flight/completed future so hosts can
/// safely invoke shutdown from multiple lifecycle paths.
Future<void> shutdownAudio() {
final existing = _audioShutdownFuture;
if (existing != null) {
return existing;
}
final shutdown = () async {
await audio.stopAllAudio();
audio.dispose();
}();
_audioShutdownFuture = shutdown;
return shutdown;
}
/// Loads an asset from the Flutter bundle, returning `null` when absent.
Future<ByteData?> _tryLoad(String path) async {
try {
@@ -316,3 +135,9 @@ class Wolf3d {
}
}
}
/// Backward-compatible alias for the previous Flutter host facade name.
typedef Wolf3dFlutter = Wolf3dFlutterEngine;
/// Backward-compatible alias for the legacy Flutter host facade name.
typedef Wolf3d = Wolf3dFlutterEngine;
@@ -47,7 +47,7 @@ class _CountingAudio implements EngineAudio {
void main() {
testWidgets('dispose path shuts down audio', (tester) async {
final audio = _CountingAudio();
final wolf3d = Wolf3d(audioBackend: audio);
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
await tester.pumpWidget(
MaterialApp(
@@ -39,15 +39,15 @@ class _NoopAudio implements EngineAudio {
}
void main() {
group('Wolf3d debug mode', () {
group('Wolf3dFlutterEngine debug mode', () {
test('is disabled by default', () {
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio());
expect(wolf3d.isDebugEnabled, isFalse);
});
test('enableDebug toggles debug mode', () {
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio());
final returned = wolf3d.enableDebug();
@@ -56,7 +56,7 @@ void main() {
});
test('init(debug: true) enables debug mode', () async {
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio());
await wolf3d.init(debug: true);
@@ -103,7 +103,7 @@ void main() {
});
}
class _TestWolf3d extends Wolf3d {
class _TestWolf3d extends Wolf3dFlutterEngine {
_TestWolf3d({required super.audioBackend});
@override
@@ -44,10 +44,10 @@ class _CountingAudio implements EngineAudio {
}
void main() {
group('Wolf3d.shutdownAudio', () {
group('Wolf3dFlutterEngine.shutdownAudio', () {
test('stops and disposes audio once', () async {
final audio = _CountingAudio();
final wolf3d = Wolf3d(audioBackend: audio);
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
await wolf3d.shutdownAudio();
await wolf3d.shutdownAudio();
@@ -58,7 +58,7 @@ void main() {
test('concurrent calls share the same shutdown work', () async {
final audio = _CountingAudio();
final wolf3d = Wolf3d(audioBackend: audio);
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
await Future.wait<void>([
wolf3d.shutdownAudio(),