From e060aef3f1d5e724d39b08ba11cb01e0ec84f9eb Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 20 Mar 2026 10:04:01 +0100 Subject: [PATCH] feat: Add quit callback support to engine and UI components Signed-off-by: Hans Kokx --- apps/wolf_3d_cli/bin/main.dart | 1 + apps/wolf_3d_gui/lib/screens/game_screen.dart | 3 ++ .../lib/src/engine/wolf_3d_engine_base.dart | 16 ++++++++- .../level_state_and_pause_menu_test.dart | 36 ++++++++++++++++++- .../wolf_3d_flutter/lib/wolf_3d_flutter.dart | 6 +++- 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/apps/wolf_3d_cli/bin/main.dart b/apps/wolf_3d_cli/bin/main.dart index 0c4276f..67d8f34 100644 --- a/apps/wolf_3d_cli/bin/main.dart +++ b/apps/wolf_3d_cli/bin/main.dart @@ -61,6 +61,7 @@ void main() async { ), input: CliInput(), onGameWon: () => stopAndExit(0), + onQuit: () => stopAndExit(0), ); engine.init(); diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index 5fb6bbd..61e6516 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -45,6 +45,9 @@ class _GameScreenState extends State { widget.wolf3d.clearActiveDifficulty(); Navigator.of(context).pop(); }, + onQuit: () { + SystemNavigator.pop(); + }, ); } diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index ccd5382..68afd48 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -24,6 +24,7 @@ class WolfEngine { this.menuBackgroundRgb = 0x890000, this.menuPanelRgb = 0x590002, this.onMenuExit, + this.onQuit, this.onGameSelected, this.onEpisodeSelected, EngineAudio? engineAudio, @@ -95,6 +96,9 @@ class WolfEngine { /// Callback triggered when backing out of the top-level menu. final void Function()? onMenuExit; + /// Callback triggered when the player explicitly selects QUIT. + final void Function()? onQuit; + /// Callback triggered whenever the active game changes from menu flow. final void Function(WolfensteinData game)? onGameSelected; @@ -330,8 +334,10 @@ class WolfEngine { case WolfMenuMainAction.backToGame: _resumeGame(); break; - case WolfMenuMainAction.backToDemo: case WolfMenuMainAction.quit: + _quitProgram(); + break; + case WolfMenuMainAction.backToDemo: _exitTopLevelMenu(); break; case WolfMenuMainAction.sound: @@ -445,6 +451,14 @@ class WolfEngine { onGameWon(); } + void _quitProgram() { + if (onQuit != null) { + onQuit!.call(); + return; + } + _exitTopLevelMenu(); + } + /// Wipes the current world state and builds a new floor from map data. void _loadLevel({required bool preservePlayerState}) { entities.clear(); diff --git a/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart b/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart index 7337d9a..30d78b8 100644 --- a/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart +++ b/packages/wolf_3d_dart/test/engine/level_state_and_pause_menu_test.dart @@ -216,12 +216,16 @@ void main() { expect(manager.selectedMainIndex, 0); }); - test('quit selection triggers top-level menu exit callback', () { + test('quit selection triggers dedicated quit callback', () { final input = _TestInput(); + int quitCalls = 0; int exitCalls = 0; final engine = _buildEngine( input: input, difficulty: null, + onQuit: () { + quitCalls++; + }, onMenuExit: () { exitCalls++; }, @@ -245,6 +249,32 @@ void main() { engine.tick(const Duration(milliseconds: 16)); input.isInteracting = false; + expect(quitCalls, 1); + expect(exitCalls, 0); + }); + + test('backing out of the top-level menu uses menu-exit callback', () { + final input = _TestInput(); + int quitCalls = 0; + int exitCalls = 0; + final engine = _buildEngine( + input: input, + difficulty: null, + onQuit: () { + quitCalls++; + }, + onMenuExit: () { + exitCalls++; + }, + ); + + engine.init(); + + input.isBack = true; + engine.tick(const Duration(milliseconds: 16)); + input.isBack = false; + + expect(quitCalls, 0); expect(exitCalls, 1); }); }); @@ -254,6 +284,7 @@ WolfEngine _buildMultiGameEngine({ required _TestInput input, required Difficulty? difficulty, void Function()? onMenuExit, + void Function()? onQuit, }) { final WolfensteinData retail = _buildTestData( gameVersion: GameVersion.retail, @@ -271,6 +302,7 @@ WolfEngine _buildMultiGameEngine({ engineAudio: _SilentAudio(), onGameWon: () {}, onMenuExit: onMenuExit, + onQuit: onQuit, ); } @@ -278,6 +310,7 @@ WolfEngine _buildEngine({ required _TestInput input, required Difficulty? difficulty, void Function()? onMenuExit, + void Function()? onQuit, }) { return WolfEngine( data: _buildTestData(gameVersion: GameVersion.retail), @@ -288,6 +321,7 @@ WolfEngine _buildEngine({ engineAudio: _SilentAudio(), onGameWon: () {}, onMenuExit: onMenuExit, + onQuit: onQuit, ); } diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart index 60018e1..8c0519d 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart @@ -82,7 +82,10 @@ class Wolf3d { /// 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}) { + WolfEngine launchEngine({ + required void Function() onGameWon, + void Function()? onQuit, + }) { if (availableGames.isEmpty) { throw StateError( 'No game data was discovered. Add game files before launching the engine.', @@ -102,6 +105,7 @@ class Wolf3d { // 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, onGameSelected: (game) { _activeGame = game; audio.activeGame = game;