feat: Refactor to use Wolf3dFlutterEngine across the application
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
/// Flutter entry point for the GUI host application.
|
/// Flutter entry point for the GUI host application.
|
||||||
///
|
///
|
||||||
/// The GUI bootstraps bundled and discoverable game data through [Wolf3d]
|
/// The GUI bootstraps bundled and discoverable game data through
|
||||||
|
/// [Wolf3dFlutterEngine]
|
||||||
/// before presenting the game-selection flow.
|
/// before presenting the game-selection flow.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
@@ -17,7 +18,9 @@ void main() async {
|
|||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Wolf3d wolf3d = await Wolf3d().init(debug: kDebugMode);
|
final Wolf3dFlutterEngine wolf3d = await Wolf3dFlutterEngine().init(
|
||||||
|
debug: kDebugMode,
|
||||||
|
);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
|
||||||
|
/// Platform-agnostic gameplay session facade for Wolf3D hosts.
|
||||||
|
///
|
||||||
|
/// This class owns shared engine state and host-facing session configuration
|
||||||
|
/// while remaining independent of Flutter and other UI frameworks.
|
||||||
|
class Wolf3dEngine {
|
||||||
|
/// Creates a session facade backed by [audio] and [input].
|
||||||
|
Wolf3dEngine({required this.audio, required this.input});
|
||||||
|
|
||||||
|
/// All successfully discovered or supplied game data sets.
|
||||||
|
final List<WolfensteinData> availableGames = [];
|
||||||
|
|
||||||
|
WolfensteinData? _activeGame;
|
||||||
|
|
||||||
|
/// Shared engine audio backend used by menus and gameplay sessions.
|
||||||
|
final EngineAudio audio;
|
||||||
|
|
||||||
|
/// Shared input adapter reused by hosts and gameplay screens.
|
||||||
|
final Wolf3dInput input;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
bool _debugEnabled = false;
|
||||||
|
|
||||||
|
/// Whether host-level debug affordances should be visible.
|
||||||
|
bool get isDebugEnabled => _debugEnabled;
|
||||||
|
|
||||||
|
/// Enables host-level debug affordances such as debug navigation UI.
|
||||||
|
Wolf3dEngine enableDebug() {
|
||||||
|
_debugEnabled = true;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_activeGame = game;
|
||||||
|
_activeEpisode = null;
|
||||||
|
audio.activeGame = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backward-compatible alias for the previous class name.
|
||||||
|
typedef Wolf3dSession = Wolf3dEngine;
|
||||||
@@ -19,4 +19,5 @@ export 'src/engine/save/default_save_game_persistence.dart';
|
|||||||
export 'src/engine/save/game_session_snapshot.dart';
|
export 'src/engine/save/game_session_snapshot.dart';
|
||||||
export 'src/engine/save/save_game_codec.dart';
|
export 'src/engine/save/save_game_codec.dart';
|
||||||
export 'src/engine/save/save_game_persistence.dart';
|
export 'src/engine/save/save_game_persistence.dart';
|
||||||
|
export 'src/engine/wolf_3d_engine.dart';
|
||||||
export 'src/engine/wolf_3d_engine_base.dart';
|
export 'src/engine/wolf_3d_engine_base.dart';
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class _AudioRow {
|
|||||||
/// Displays all decoded SFX and music tracks for the selected game data.
|
/// Displays all decoded SFX and music tracks for the selected game data.
|
||||||
class AudioGallery extends StatefulWidget {
|
class AudioGallery extends StatefulWidget {
|
||||||
/// Shared app facade used to access game assets and the audio backend.
|
/// 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});
|
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.
|
/// Presents debug-only navigation shortcuts for asset galleries.
|
||||||
class DebugToolsScreen extends StatelessWidget {
|
class DebugToolsScreen extends StatelessWidget {
|
||||||
/// Shared app facade used to access active game assets.
|
/// Shared app facade used to access active game assets.
|
||||||
final Wolf3d wolf3d;
|
final Wolf3dFlutterEngine wolf3d;
|
||||||
|
|
||||||
/// Creates the debug tools screen for [wolf3d].
|
/// Creates the debug tools screen for [wolf3d].
|
||||||
const DebugToolsScreen({super.key, required this.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/renderer/wolf_3d_glsl_renderer.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.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 {
|
class GameScreen extends StatefulWidget {
|
||||||
/// Shared application facade owning the engine, audio, and input.
|
/// Shared application facade owning the engine, audio, and input.
|
||||||
final Wolf3d wolf3d;
|
final Wolf3dFlutterEngine wolf3d;
|
||||||
|
|
||||||
/// Optional host-level shortcut override.
|
/// 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.
|
/// Displays every sprite frame in the active game along with enemy metadata.
|
||||||
class SpriteGallery extends StatefulWidget {
|
class SpriteGallery extends StatefulWidget {
|
||||||
/// Shared application facade used to access the active game's sprite set.
|
/// Shared application facade used to access the active game's sprite set.
|
||||||
final Wolf3d wolf3d;
|
final Wolf3dFlutterEngine wolf3d;
|
||||||
|
|
||||||
/// Creates the sprite gallery for [wolf3d].
|
/// Creates the sprite gallery for [wolf3d].
|
||||||
const SpriteGallery({super.key, required this.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.
|
/// Shows each VGA image extracted from the currently selected game data set.
|
||||||
class VgaGallery extends StatefulWidget {
|
class VgaGallery extends StatefulWidget {
|
||||||
/// Shared app facade used to access available game data sets.
|
/// 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.
|
/// Creates the gallery for the currently selected or browsed game.
|
||||||
const VgaGallery({super.key, required this.wolf3d});
|
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.
|
/// Selects which discovered game data set gallery screens should display.
|
||||||
class GalleryGameSelector extends StatelessWidget {
|
class GalleryGameSelector extends StatelessWidget {
|
||||||
final Wolf3d wolf3d;
|
final Wolf3dFlutterEngine wolf3d;
|
||||||
final WolfensteinData selectedGame;
|
final WolfensteinData selectedGame;
|
||||||
final ValueChanged<WolfensteinData> onSelected;
|
final ValueChanged<WolfensteinData> onSelected;
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ library;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.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 {
|
class Wolf3dApp extends StatelessWidget {
|
||||||
/// Shared initialized facade that owns game data, input, and audio services.
|
/// Shared initialized facade that owns game data, input, and audio services.
|
||||||
final Wolf3d wolf3d;
|
final Wolf3dFlutterEngine wolf3d;
|
||||||
|
|
||||||
const Wolf3dApp({
|
const Wolf3dApp({
|
||||||
super.key,
|
super.key,
|
||||||
|
|||||||
@@ -37,203 +37,40 @@ export 'widgets/gallery_game_selector.dart'
|
|||||||
export 'widgets/wolf3d_app.dart' show Wolf3dApp;
|
export 'widgets/wolf3d_app.dart' show Wolf3dApp;
|
||||||
export 'widgets/wolf_menu_shell.dart' show WolfMenuShell;
|
export 'widgets/wolf_menu_shell.dart' show WolfMenuShell;
|
||||||
|
|
||||||
/// Coordinates asset discovery, audio initialization, and input reuse for apps.
|
/// Flutter-specific host facade built on top of [Wolf3dEngine].
|
||||||
class Wolf3d {
|
///
|
||||||
|
/// 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].
|
/// Creates an empty facade that must be initialized with [init].
|
||||||
Wolf3d({EngineAudio? audioBackend})
|
Wolf3dFlutterEngine({
|
||||||
: audio = audioBackend ?? Wolf3dPlatformAudio();
|
EngineAudio? audioBackend,
|
||||||
|
Wolf3dFlutterInput? inputBackend,
|
||||||
/// All successfully discovered or bundled game data sets.
|
}) : super(
|
||||||
final List<WolfensteinData> availableGames = [];
|
audio: audioBackend ?? Wolf3dPlatformAudio(),
|
||||||
WolfensteinData? _activeGame;
|
input: inputBackend ?? Wolf3dFlutterInput(),
|
||||||
|
);
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// Shared Flutter input adapter reused by gameplay screens.
|
/// Shared Flutter input adapter reused by gameplay screens.
|
||||||
final Wolf3dFlutterInput input = Wolf3dFlutterInput();
|
@override
|
||||||
|
Wolf3dFlutterInput get input => super.input as Wolf3dFlutterInput;
|
||||||
bool _debugEnabled = false;
|
|
||||||
|
|
||||||
/// Whether host-level debug affordances should be visible.
|
|
||||||
bool get isDebugEnabled => _debugEnabled;
|
|
||||||
|
|
||||||
/// Enables host-level debug affordances such as debug navigation UI.
|
/// Enables host-level debug affordances such as debug navigation UI.
|
||||||
Wolf3d enableDebug() {
|
@override
|
||||||
_debugEnabled = true;
|
Wolf3dFlutterEngine enableDebug() {
|
||||||
|
super.enableDebug();
|
||||||
return this;
|
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.
|
/// Initializes the engine by loading available game data.
|
||||||
///
|
///
|
||||||
/// Set [debug] to `true` to explicitly enable host-level debug affordances.
|
/// 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) {
|
if (debug) {
|
||||||
_debugEnabled = true;
|
enableDebug();
|
||||||
}
|
}
|
||||||
await audio.init();
|
await audio.init();
|
||||||
availableGames.clear();
|
availableGames.clear();
|
||||||
@@ -288,24 +125,6 @@ class Wolf3d {
|
|||||||
return this;
|
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.
|
/// Loads an asset from the Flutter bundle, returning `null` when absent.
|
||||||
Future<ByteData?> _tryLoad(String path) async {
|
Future<ByteData?> _tryLoad(String path) async {
|
||||||
try {
|
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() {
|
void main() {
|
||||||
testWidgets('dispose path shuts down audio', (tester) async {
|
testWidgets('dispose path shuts down audio', (tester) async {
|
||||||
final audio = _CountingAudio();
|
final audio = _CountingAudio();
|
||||||
final wolf3d = Wolf3d(audioBackend: audio);
|
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|||||||
@@ -39,15 +39,15 @@ class _NoopAudio implements EngineAudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Wolf3d debug mode', () {
|
group('Wolf3dFlutterEngine debug mode', () {
|
||||||
test('is disabled by default', () {
|
test('is disabled by default', () {
|
||||||
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
|
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio());
|
||||||
|
|
||||||
expect(wolf3d.isDebugEnabled, isFalse);
|
expect(wolf3d.isDebugEnabled, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enableDebug toggles debug mode', () {
|
test('enableDebug toggles debug mode', () {
|
||||||
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
|
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio());
|
||||||
|
|
||||||
final returned = wolf3d.enableDebug();
|
final returned = wolf3d.enableDebug();
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('init(debug: true) enables debug mode', () async {
|
test('init(debug: true) enables debug mode', () async {
|
||||||
final wolf3d = Wolf3d(audioBackend: _NoopAudio());
|
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio());
|
||||||
|
|
||||||
await wolf3d.init(debug: true);
|
await wolf3d.init(debug: true);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TestWolf3d extends Wolf3d {
|
class _TestWolf3d extends Wolf3dFlutterEngine {
|
||||||
_TestWolf3d({required super.audioBackend});
|
_TestWolf3d({required super.audioBackend});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ class _CountingAudio implements EngineAudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Wolf3d.shutdownAudio', () {
|
group('Wolf3dFlutterEngine.shutdownAudio', () {
|
||||||
test('stops and disposes audio once', () async {
|
test('stops and disposes audio once', () async {
|
||||||
final audio = _CountingAudio();
|
final audio = _CountingAudio();
|
||||||
final wolf3d = Wolf3d(audioBackend: audio);
|
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
|
||||||
|
|
||||||
await wolf3d.shutdownAudio();
|
await wolf3d.shutdownAudio();
|
||||||
await wolf3d.shutdownAudio();
|
await wolf3d.shutdownAudio();
|
||||||
@@ -58,7 +58,7 @@ void main() {
|
|||||||
|
|
||||||
test('concurrent calls share the same shutdown work', () async {
|
test('concurrent calls share the same shutdown work', () async {
|
||||||
final audio = _CountingAudio();
|
final audio = _CountingAudio();
|
||||||
final wolf3d = Wolf3d(audioBackend: audio);
|
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
|
||||||
|
|
||||||
await Future.wait<void>([
|
await Future.wait<void>([
|
||||||
wolf3d.shutdownAudio(),
|
wolf3d.shutdownAudio(),
|
||||||
|
|||||||
Reference in New Issue
Block a user