feat: Add tests for Wolf3dGuiApp and refactor game data picker management
- Introduced unit tests for the Wolf3dGuiApp to ensure proper directory configuration and audio management. - Refactored the GameDataPickerManager to streamline directory and file selection processes. - Removed obsolete GameDataPickerManager and Wolf3dAppManager classes to simplify the architecture. - Enhanced the Wolf3dFlutterEngine to improve game version handling during save loading. - Updated existing tests to reflect changes in the game data management structure. - Removed unnecessary dependencies and cleaned up the codebase for better maintainability. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,144 +0,0 @@
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||
|
||||
/// Callback used to open a directory picker.
|
||||
typedef PickDirectoryCallback =
|
||||
Future<String?> Function({
|
||||
String? confirmButtonText,
|
||||
});
|
||||
|
||||
/// Callback used to open a multi-file picker.
|
||||
typedef PickFilesCallback = Future<List<XFile>> Function();
|
||||
|
||||
/// Coordinates game-data selection flows for the no-data setup screen.
|
||||
class GameDataPickerManager {
|
||||
/// Creates a picker workflow manager bound to [engine].
|
||||
///
|
||||
/// [pickDirectory] and [pickFiles] are injectable test seams. Production
|
||||
/// usage should rely on their defaults from `file_selector`.
|
||||
GameDataPickerManager({
|
||||
required this.engine,
|
||||
PickDirectoryCallback? pickDirectory,
|
||||
PickFilesCallback? pickFiles,
|
||||
}) : _pickDirectory = pickDirectory ?? getDirectoryPath,
|
||||
_pickFiles = pickFiles ?? openFiles;
|
||||
|
||||
/// Engine facade reloaded when users choose new data locations.
|
||||
final Wolf3dFlutterEngine engine;
|
||||
|
||||
final PickDirectoryCallback _pickDirectory;
|
||||
final PickFilesCallback _pickFiles;
|
||||
|
||||
bool _isLoadingGameData = false;
|
||||
String? _pickerError;
|
||||
|
||||
/// Whether a picker/reload operation is currently in progress.
|
||||
bool get isLoadingGameData => _isLoadingGameData;
|
||||
|
||||
/// Last picker/reload error, if any.
|
||||
String? get pickerError => _pickerError;
|
||||
|
||||
/// Prompts the user for a game-data directory and reloads discovery.
|
||||
///
|
||||
/// Calls [notifyChanged] before and after the async workflow so UIs can
|
||||
/// update loading/error state.
|
||||
Future<void> pickGameDataDirectory({
|
||||
required void Function() notifyChanged,
|
||||
}) async {
|
||||
_isLoadingGameData = true;
|
||||
_pickerError = null;
|
||||
notifyChanged();
|
||||
|
||||
try {
|
||||
final String? directoryPath = await _pickDirectory(
|
||||
confirmButtonText: 'Use this folder',
|
||||
);
|
||||
|
||||
if (directoryPath == null || directoryPath.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await engine.init(directory: directoryPath.trim());
|
||||
} catch (error) {
|
||||
_pickerError = 'Unable to load selected directory: $error';
|
||||
} finally {
|
||||
_isLoadingGameData = false;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts the user for one or more game-data files and reloads discovery.
|
||||
///
|
||||
/// Selected file paths are collapsed into unique parent directories, then
|
||||
/// passed to [Wolf3dFlutterEngine.init] as one primary directory plus
|
||||
/// [Wolf3dFlutterEngine.init] `additionalDirectories`.
|
||||
Future<void> pickGameDataFiles({
|
||||
required void Function() notifyChanged,
|
||||
}) async {
|
||||
_isLoadingGameData = true;
|
||||
_pickerError = null;
|
||||
notifyChanged();
|
||||
|
||||
try {
|
||||
final List<XFile> selectedFiles = await _pickFiles();
|
||||
|
||||
if (selectedFiles.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final LinkedHashSet<String> directories = LinkedHashSet<String>();
|
||||
for (final XFile file in selectedFiles) {
|
||||
final String directory = _directoryFromFilePath(file.path);
|
||||
if (directory.isNotEmpty) {
|
||||
directories.add(directory);
|
||||
}
|
||||
}
|
||||
|
||||
if (directories.isEmpty) {
|
||||
_pickerError = 'Selected files do not expose local filesystem paths.';
|
||||
return;
|
||||
}
|
||||
|
||||
final List<String> orderedDirectories = directories.toList(
|
||||
growable: false,
|
||||
);
|
||||
await engine.init(
|
||||
directory: orderedDirectories.first,
|
||||
additionalDirectories: orderedDirectories.skip(1),
|
||||
);
|
||||
} catch (error) {
|
||||
_pickerError = 'Unable to load selected files: $error';
|
||||
} finally {
|
||||
_isLoadingGameData = false;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the parent directory component from [path].
|
||||
String _directoryFromFilePath(String path) {
|
||||
final String trimmedPath = path.trim();
|
||||
if (trimmedPath.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
final int slashIndex = trimmedPath.lastIndexOf('/');
|
||||
final int backslashIndex = trimmedPath.lastIndexOf(r'\');
|
||||
final int separatorIndex = slashIndex > backslashIndex
|
||||
? slashIndex
|
||||
: backslashIndex;
|
||||
|
||||
if (separatorIndex < 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (separatorIndex == 0) {
|
||||
return trimmedPath[0];
|
||||
}
|
||||
|
||||
return trimmedPath.substring(0, separatorIndex);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||
|
||||
/// Coordinates app-shell setup actions and shutdown behavior.
|
||||
class Wolf3dAppManager {
|
||||
/// Creates an app-shell manager bound to [engine].
|
||||
///
|
||||
/// [pickerManager] can be provided in tests to control picker behavior.
|
||||
Wolf3dAppManager({
|
||||
required this.engine,
|
||||
GameDataPickerManager? pickerManager,
|
||||
}) : _pickerManager = pickerManager ?? GameDataPickerManager(engine: engine);
|
||||
|
||||
/// Engine facade managed by the app shell.
|
||||
final Wolf3dFlutterEngine engine;
|
||||
final GameDataPickerManager _pickerManager;
|
||||
Future<void>? _shutdownFuture;
|
||||
|
||||
/// Forwarded picker loading flag for UI binding.
|
||||
bool get isLoadingGameData => _pickerManager.isLoadingGameData;
|
||||
|
||||
/// Forwarded picker error text for UI binding.
|
||||
String? get pickerError => _pickerManager.pickerError;
|
||||
|
||||
/// Shuts down engine audio exactly once and reuses in-flight shutdown calls.
|
||||
Future<void> ensureAudioShutdown() {
|
||||
final existing = _shutdownFuture;
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
final shutdown = engine.shutdownAudio();
|
||||
_shutdownFuture = shutdown;
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
/// Delegates directory picker + reload behavior.
|
||||
Future<void> pickGameDataDirectory({
|
||||
required void Function() notifyChanged,
|
||||
}) async {
|
||||
await _pickerManager.pickGameDataDirectory(notifyChanged: notifyChanged);
|
||||
}
|
||||
|
||||
/// Delegates file picker + reload behavior.
|
||||
Future<void> pickGameDataFiles({
|
||||
required void Function() notifyChanged,
|
||||
}) async {
|
||||
await _pickerManager.pickGameDataFiles(notifyChanged: notifyChanged);
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
||||
|
||||
/// Fallback screen shown when no Wolf3D game data files are discovered.
|
||||
class NoGameDataScreen extends StatelessWidget {
|
||||
const NoGameDataScreen({
|
||||
super.key,
|
||||
this.configuredDataDirectory,
|
||||
this.onPickGameDataDirectory,
|
||||
this.onPickGameDataFiles,
|
||||
this.isLoadingGameData = false,
|
||||
this.pickerError,
|
||||
});
|
||||
|
||||
/// Previously configured external game-data directory, if any.
|
||||
final String? configuredDataDirectory;
|
||||
|
||||
/// Invoked when the user requests selecting a game-data directory.
|
||||
final Future<void> Function()? onPickGameDataDirectory;
|
||||
|
||||
/// Invoked when the user requests selecting one or more data files.
|
||||
final Future<void> Function()? onPickGameDataFiles;
|
||||
|
||||
/// Whether the host is currently reloading after picker selection.
|
||||
final bool isLoadingGameData;
|
||||
|
||||
/// Optional picker/reload error shown to the user.
|
||||
final String? pickerError;
|
||||
|
||||
static Color _colorFromVgaIndex(int index) {
|
||||
final int packed = ColorPalette.vga32Bit[index]; // 0xAABBGGRR
|
||||
final int r = packed & 0xFF;
|
||||
final int g = (packed >> 8) & 0xFF;
|
||||
final int b = (packed >> 16) & 0xFF;
|
||||
return Color((0xFF << 24) | (r << 16) | (g << 8) | b);
|
||||
}
|
||||
|
||||
static final Color _backgroundColor = _colorFromVgaIndex(111);
|
||||
static final Color _panelColor = _colorFromVgaIndex(103);
|
||||
static final Color _borderColor = _colorFromVgaIndex(87);
|
||||
static final Color _titleColor = _colorFromVgaIndex(
|
||||
WolfMenuPalette.headerTextIndex,
|
||||
);
|
||||
static final Color _bodyColor = _colorFromVgaIndex(
|
||||
WolfMenuPalette.unselectedTextIndex,
|
||||
);
|
||||
static final Color _emphasisColor = _colorFromVgaIndex(10);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _backgroundColor,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _panelColor,
|
||||
border: Border.all(color: _borderColor, width: 2),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'WOLF3D DATA NOT FOUND',
|
||||
style: TextStyle(
|
||||
color: _titleColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No game files were discovered.\n\n'
|
||||
'Select a game-data directory, or select one or more game-data files.',
|
||||
style: TextStyle(
|
||||
color: _bodyColor,
|
||||
fontSize: 15,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Once all required files are present, the game starts automatically.',
|
||||
style: TextStyle(
|
||||
color: _emphasisColor,
|
||||
fontSize: 14,
|
||||
height: 1.35,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: isLoadingGameData
|
||||
? null
|
||||
: onPickGameDataDirectory,
|
||||
child: Text(
|
||||
isLoadingGameData
|
||||
? 'Loading data...'
|
||||
: 'Select data directory',
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: isLoadingGameData
|
||||
? null
|
||||
: onPickGameDataFiles,
|
||||
child: Text(
|
||||
isLoadingGameData
|
||||
? 'Loading data...'
|
||||
: 'Select data files',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isLoadingGameData)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
'Scanning selected locations...',
|
||||
style: TextStyle(
|
||||
color: _bodyColor,
|
||||
fontSize: 13,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (pickerError != null && pickerError!.trim().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
pickerError!.trim(),
|
||||
style: TextStyle(
|
||||
color: _emphasisColor,
|
||||
fontSize: 13,
|
||||
height: 1.3,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (configuredDataDirectory != null &&
|
||||
configuredDataDirectory!.trim().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
'Configured data directory: ${configuredDataDirectory!.trim()}',
|
||||
style: TextStyle(
|
||||
color: _bodyColor,
|
||||
fontSize: 13,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||
|
||||
/// Minimal app shell that binds a prepared [Wolf3dFlutterEngine] instance to
|
||||
/// host screens.
|
||||
class Wolf3dApp extends StatefulWidget {
|
||||
/// Shared initialized facade that owns game data, input, and audio services.
|
||||
final Wolf3dFlutterEngine engine;
|
||||
|
||||
const Wolf3dApp({
|
||||
super.key,
|
||||
required this.engine,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Wolf3dApp> createState() => _Wolf3dAppState();
|
||||
}
|
||||
|
||||
class _Wolf3dAppState extends State<Wolf3dApp> with WidgetsBindingObserver {
|
||||
late final Wolf3dAppManager _manager;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_manager = Wolf3dAppManager(engine: widget.engine);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
unawaited(_manager.ensureAudioShutdown());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.detached) {
|
||||
unawaited(_manager.ensureAudioShutdown());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickGameDataDirectory() {
|
||||
return _manager.pickGameDataDirectory(
|
||||
notifyChanged: () {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickGameDataFiles() {
|
||||
return _manager.pickGameDataFiles(
|
||||
notifyChanged: () {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.engine.availableGames.isEmpty
|
||||
? NoGameDataScreen(
|
||||
configuredDataDirectory: widget.engine.configuredDataDirectory,
|
||||
onPickGameDataDirectory: _pickGameDataDirectory,
|
||||
onPickGameDataFiles: _pickGameDataFiles,
|
||||
isLoadingGameData: _manager.isLoadingGameData,
|
||||
pickerError: _manager.pickerError,
|
||||
)
|
||||
: GameScreen(wolf3d: widget.engine);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.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';
|
||||
@@ -18,7 +17,6 @@ export 'audio/wolf3d_platform_audio.dart' show Wolf3dPlatformAudio;
|
||||
export 'managers/game_app_lifecycle_manager.dart' show GameAppLifecycleManager;
|
||||
export 'managers/game_data_directory_persistence.dart'
|
||||
show DefaultGameDataDirectoryPersistence;
|
||||
export 'managers/game_data_picker_manager.dart' show GameDataPickerManager;
|
||||
export 'managers/game_display_manager.dart' show GameDisplayManager;
|
||||
export 'managers/game_persistence_manager.dart' show GamePersistenceManager;
|
||||
export 'managers/game_renderer_mode_manager.dart'
|
||||
@@ -31,22 +29,19 @@ export 'managers/game_screen_input_manager.dart'
|
||||
HostShortcutRegistry,
|
||||
GameScreenInputManager,
|
||||
isAltEnterShortcut;
|
||||
export 'managers/wolf3d_app_manager.dart' show Wolf3dAppManager;
|
||||
export 'screens/audio_gallery.dart' show AudioGallery;
|
||||
export 'screens/debug_tools_screen.dart' show DebugToolsScreen;
|
||||
export 'screens/game_screen.dart' show GameScreen;
|
||||
export 'screens/no_game_data_screen.dart' show NoGameDataScreen;
|
||||
export 'screens/sprite_gallery.dart' show SpriteGallery;
|
||||
export 'screens/vga_gallery.dart' show VgaGallery;
|
||||
export 'widgets/gallery_game_selector.dart'
|
||||
show GalleryGameSelector, formatGalleryGameTitle;
|
||||
export 'widgets/wolf3d_app.dart' show Wolf3dApp;
|
||||
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 bundle loading and discovery.
|
||||
/// 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({
|
||||
@@ -89,8 +84,9 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
||||
/// used when available. [additionalDirectories] are scanned after the
|
||||
/// primary directory and are not persisted.
|
||||
///
|
||||
/// This method merges bundled package data (when present) with discovered
|
||||
/// external directories, deduplicating versions by [GameVersion].
|
||||
/// 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,
|
||||
@@ -110,35 +106,6 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
||||
await dataDirectoryPersistence.saveDataDirectory(requestedDirectory);
|
||||
}
|
||||
|
||||
// Bundled assets let the GUI work out of the box on supported platforms.
|
||||
final versionsToTry = [
|
||||
(version: GameVersion.retail, path: 'retail'),
|
||||
(version: GameVersion.shareware, path: 'shareware'),
|
||||
];
|
||||
|
||||
for (final version in versionsToTry) {
|
||||
try {
|
||||
final ext = version.version.fileExtension;
|
||||
final folder = 'packages/wolf_3d_assets/assets/${version.path}';
|
||||
|
||||
final data = WolfensteinLoader.loadFromBytes(
|
||||
version: version.version,
|
||||
vswap: await _tryLoad('$folder/VSWAP.$ext'),
|
||||
mapHead: await _tryLoad('$folder/MAPHEAD.$ext'),
|
||||
gameMaps: await _tryLoad('$folder/GAMEMAPS.$ext'),
|
||||
vgaDict: await _tryLoad('$folder/VGADICT.$ext'),
|
||||
vgaHead: await _tryLoad('$folder/VGAHEAD.$ext'),
|
||||
vgaGraph: await _tryLoad('$folder/VGAGRAPH.$ext'),
|
||||
audioHed: await _tryLoad('$folder/AUDIOHED.$ext'),
|
||||
audioT: await _tryLoad('$folder/AUDIOT.$ext'),
|
||||
);
|
||||
|
||||
availableGames.add(data);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 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>{};
|
||||
@@ -175,14 +142,4 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Loads an asset from the Flutter bundle, returning `null` when absent.
|
||||
Future<ByteData?> _tryLoad(String path) async {
|
||||
try {
|
||||
return await rootBundle.load(path);
|
||||
} catch (e) {
|
||||
debugPrint("Asset not found: $path");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
audioplayers: ^6.6.0
|
||||
file_selector: ^1.0.3
|
||||
window_manager: ^0.5.1
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter_test/flutter_test.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/wolf_3d_flutter.dart';
|
||||
|
||||
class _NoopAudio implements EngineAudio {
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
int stopAllAudioCallCount = 0;
|
||||
int disposeCallCount = 0;
|
||||
|
||||
@override
|
||||
Future<void> debugSoundTest() async {}
|
||||
|
||||
@override
|
||||
Future<void> init() async {}
|
||||
|
||||
@override
|
||||
void playLevelMusic(Music music) {}
|
||||
|
||||
@override
|
||||
void playMenuMusic() {}
|
||||
|
||||
@override
|
||||
void playSoundEffect(SoundEffect effect) {}
|
||||
|
||||
@override
|
||||
void playSoundEffectId(int sfxId) {}
|
||||
|
||||
@override
|
||||
Future<void> stopAllAudio() async {
|
||||
stopAllAudioCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void stopMusic() {}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeCallCount++;
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingEngine extends Wolf3dFlutterEngine {
|
||||
_RecordingEngine({required _NoopAudio audio}) : super(audioBackend: audio);
|
||||
|
||||
int initCallCount = 0;
|
||||
String? lastDirectory;
|
||||
List<String> lastAdditionalDirectories = <String>[];
|
||||
|
||||
@override
|
||||
Future<Wolf3dFlutterEngine> init({
|
||||
String? directory,
|
||||
Iterable<String>? additionalDirectories,
|
||||
}) async {
|
||||
initCallCount++;
|
||||
lastDirectory = directory;
|
||||
lastAdditionalDirectories =
|
||||
additionalDirectories?.toList(growable: false) ?? <String>[];
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('GameDataPickerManager', () {
|
||||
test('pickGameDataDirectory reloads selected directory', () async {
|
||||
final engine = _RecordingEngine(audio: _NoopAudio());
|
||||
int notifyCount = 0;
|
||||
final manager = GameDataPickerManager(
|
||||
engine: engine,
|
||||
pickDirectory: ({String? confirmButtonText}) async => ' /tmp/wolf ',
|
||||
);
|
||||
|
||||
await manager.pickGameDataDirectory(
|
||||
notifyChanged: () => notifyCount++,
|
||||
);
|
||||
|
||||
expect(notifyCount, 2);
|
||||
expect(manager.isLoadingGameData, isFalse);
|
||||
expect(manager.pickerError, isNull);
|
||||
expect(engine.initCallCount, 1);
|
||||
expect(engine.lastDirectory, '/tmp/wolf');
|
||||
expect(engine.lastAdditionalDirectories, isEmpty);
|
||||
});
|
||||
|
||||
test('pickGameDataDirectory ignores empty selection', () async {
|
||||
final engine = _RecordingEngine(audio: _NoopAudio());
|
||||
final manager = GameDataPickerManager(
|
||||
engine: engine,
|
||||
pickDirectory: ({String? confirmButtonText}) async => ' ',
|
||||
);
|
||||
|
||||
await manager.pickGameDataDirectory(notifyChanged: () {});
|
||||
|
||||
expect(engine.initCallCount, 0);
|
||||
expect(manager.pickerError, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'pickGameDataFiles deduplicates directories and forwards extras',
|
||||
() async {
|
||||
final engine = _RecordingEngine(audio: _NoopAudio());
|
||||
final manager = GameDataPickerManager(
|
||||
engine: engine,
|
||||
pickFiles: () async => <XFile>[
|
||||
XFile('/a/one/VSWAP.WL6'),
|
||||
XFile('/a/one/MAPHEAD.WL6'),
|
||||
XFile('/a/two/VSWAP.WL1'),
|
||||
],
|
||||
);
|
||||
|
||||
await manager.pickGameDataFiles(notifyChanged: () {});
|
||||
|
||||
expect(engine.initCallCount, 1);
|
||||
expect(engine.lastDirectory, '/a/one');
|
||||
expect(engine.lastAdditionalDirectories, <String>['/a/two']);
|
||||
expect(manager.pickerError, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('pickGameDataFiles reports missing directory paths', () async {
|
||||
final engine = _RecordingEngine(audio: _NoopAudio());
|
||||
final manager = GameDataPickerManager(
|
||||
engine: engine,
|
||||
pickFiles: () async => <XFile>[
|
||||
XFile('VSWAP.WL6'),
|
||||
XFile('MAPHEAD.WL6'),
|
||||
],
|
||||
);
|
||||
|
||||
await manager.pickGameDataFiles(notifyChanged: () {});
|
||||
|
||||
expect(engine.initCallCount, 0);
|
||||
expect(
|
||||
manager.pickerError,
|
||||
'Selected files do not expose local filesystem paths.',
|
||||
);
|
||||
});
|
||||
|
||||
test('pickGameDataDirectory captures thrown picker error', () async {
|
||||
final engine = _RecordingEngine(audio: _NoopAudio());
|
||||
final manager = GameDataPickerManager(
|
||||
engine: engine,
|
||||
pickDirectory: ({String? confirmButtonText}) async {
|
||||
throw StateError('picker failed');
|
||||
},
|
||||
);
|
||||
|
||||
await manager.pickGameDataDirectory(notifyChanged: () {});
|
||||
|
||||
expect(engine.initCallCount, 0);
|
||||
expect(
|
||||
manager.pickerError,
|
||||
contains('Unable to load selected directory'),
|
||||
);
|
||||
expect(manager.isLoadingGameData, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Wolf3dAppManager', () {
|
||||
test('ensureAudioShutdown only executes once', () async {
|
||||
final audio = _NoopAudio();
|
||||
final engine = _RecordingEngine(audio: audio);
|
||||
final manager = Wolf3dAppManager(engine: engine);
|
||||
|
||||
await Future.wait<void>(<Future<void>>[
|
||||
manager.ensureAudioShutdown(),
|
||||
manager.ensureAudioShutdown(),
|
||||
]);
|
||||
|
||||
expect(audio.stopAllAudioCallCount, 1);
|
||||
expect(audio.disposeCallCount, 1);
|
||||
});
|
||||
|
||||
test('forwards picker state from injected picker manager', () async {
|
||||
final engine = _RecordingEngine(audio: _NoopAudio());
|
||||
final pickerManager = GameDataPickerManager(
|
||||
engine: engine,
|
||||
pickDirectory: ({String? confirmButtonText}) async => '/tmp/wolf',
|
||||
);
|
||||
final manager = Wolf3dAppManager(
|
||||
engine: engine,
|
||||
pickerManager: pickerManager,
|
||||
);
|
||||
|
||||
await manager.pickGameDataDirectory(notifyChanged: () {});
|
||||
|
||||
expect(manager.isLoadingGameData, isFalse);
|
||||
expect(manager.pickerError, isNull);
|
||||
expect(engine.lastDirectory, '/tmp/wolf');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,83 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.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/wolf_3d_flutter.dart';
|
||||
|
||||
class _NoopAudio implements EngineAudio {
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
@override
|
||||
Future<void> debugSoundTest() async {}
|
||||
|
||||
@override
|
||||
Future<void> init() async {}
|
||||
|
||||
@override
|
||||
void playLevelMusic(Music music) {}
|
||||
|
||||
@override
|
||||
void playMenuMusic() {}
|
||||
|
||||
@override
|
||||
void playSoundEffect(SoundEffect effect) {}
|
||||
|
||||
@override
|
||||
void playSoundEffectId(int sfxId) {}
|
||||
|
||||
@override
|
||||
Future<void> stopAllAudio() async {}
|
||||
|
||||
@override
|
||||
void stopMusic() {}
|
||||
|
||||
@override
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
class _CountingAudio implements EngineAudio {
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
int stopAllAudioCallCount = 0;
|
||||
int disposeCallCount = 0;
|
||||
|
||||
@override
|
||||
Future<void> debugSoundTest() async {}
|
||||
|
||||
@override
|
||||
Future<void> init() async {}
|
||||
|
||||
@override
|
||||
void playLevelMusic(Music music) {}
|
||||
|
||||
@override
|
||||
void playMenuMusic() {}
|
||||
|
||||
@override
|
||||
void playSoundEffect(SoundEffect effect) {}
|
||||
|
||||
@override
|
||||
void playSoundEffectId(int sfxId) {}
|
||||
|
||||
@override
|
||||
Future<void> stopAllAudio() async {
|
||||
stopAllAudioCallCount++;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
void stopMusic() {}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeCallCount++;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('DefaultGameDataDirectoryPersistence', () {
|
||||
test('saves and loads configured directory', () async {
|
||||
@@ -217,37 +143,4 @@ void main() {
|
||||
expect(await persistence.load(), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Wolf3dApp forwards configured directory to no-data screen', (
|
||||
tester,
|
||||
) async {
|
||||
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio())
|
||||
..configuredDataDirectory = '/tmp/wolf-data';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Wolf3dApp(engine: wolf3d),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.textContaining('Configured data directory:'), findsOneWidget);
|
||||
expect(find.textContaining('/tmp/wolf-data'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Wolf3dApp dispose path shuts down audio', (tester) async {
|
||||
final audio = _CountingAudio();
|
||||
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Wolf3dApp(engine: wolf3d),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
|
||||
expect(audio.stopAllAudioCallCount, 1);
|
||||
expect(audio.disposeCallCount, 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -66,6 +67,33 @@ void main() {
|
||||
expect(wolf3d.isDebugEnabled, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'init without configured external data does not load bundled games',
|
||||
() async {
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'wolf3d-empty-config-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
final persistence = DefaultGameDataDirectoryPersistence(
|
||||
filePath: '${tempDir.path}/settings.json',
|
||||
);
|
||||
final wolf3d = Wolf3dFlutterEngine(
|
||||
audioBackend: _NoopAudio(),
|
||||
dataDirectoryPersistence: persistence,
|
||||
);
|
||||
|
||||
await wolf3d.init();
|
||||
|
||||
expect(wolf3d.configuredDataDirectory, isNull);
|
||||
expect(wolf3d.availableGames, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('GameScreen hides debug FAB when debug mode is disabled', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user