feat: Enhance CLI and GUI to support configurable game data directory persistence
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ library;
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:args/args.dart';
|
||||||
import 'package:wolf_3d_cli/cli_game_loop.dart';
|
import 'package:wolf_3d_cli/cli_game_loop.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_audio.dart';
|
import 'package:wolf_3d_dart/wolf_3d_audio.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
||||||
@@ -21,28 +22,66 @@ void exitCleanly(int code) {
|
|||||||
exit(code);
|
exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Launches the CLI renderer against the bundled retail asset set.
|
/// Launches the CLI renderer using discoverable or user-provided game data.
|
||||||
void main() async {
|
void main(List<String> arguments) async {
|
||||||
stdout.write("Discovering game data...");
|
final argParser = ArgParser()
|
||||||
// Resolve the asset package relative to this executable so the CLI can run
|
..addOption(
|
||||||
// from the repo without additional configuration.
|
'data-directory',
|
||||||
final scriptUri = Platform.script;
|
abbr: 'd',
|
||||||
|
valueHelp: 'path',
|
||||||
|
help: 'Directory containing Wolf3D data files.',
|
||||||
|
)
|
||||||
|
..addFlag(
|
||||||
|
'help',
|
||||||
|
abbr: 'h',
|
||||||
|
negatable: false,
|
||||||
|
help: 'Show usage information.',
|
||||||
|
);
|
||||||
|
|
||||||
final targetUri = scriptUri.resolve(
|
late final ArgResults parsedArgs;
|
||||||
'../../../packages/wolf_3d_assets/assets/retail',
|
try {
|
||||||
);
|
parsedArgs = argParser.parse(arguments);
|
||||||
final targetPath = targetUri.toFilePath();
|
} on FormatException catch (e) {
|
||||||
|
stderr.writeln(e.message);
|
||||||
|
stderr.writeln('Usage: wolf_3d_cli [options]');
|
||||||
|
stderr.writeln(argParser.usage);
|
||||||
|
exitCleanly(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedArgs.flag('help')) {
|
||||||
|
stdout.writeln('Usage: wolf_3d_cli [options]');
|
||||||
|
stdout.writeln(argParser.usage);
|
||||||
|
exitCleanly(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? rawPath = parsedArgs.option('data-directory');
|
||||||
|
final String? dataDirectory = rawPath != null && rawPath.trim().isNotEmpty
|
||||||
|
? rawPath.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (dataDirectory != null) {
|
||||||
|
stdout.write('Discovering game data in "$dataDirectory"...');
|
||||||
|
} else {
|
||||||
|
stdout.write('Discovering game data...');
|
||||||
|
}
|
||||||
|
|
||||||
final availableGames = await WolfensteinLoader.discover(
|
final availableGames = await WolfensteinLoader.discover(
|
||||||
directoryPath: targetPath,
|
directoryPath: dataDirectory,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (availableGames.isEmpty) {
|
if (availableGames.isEmpty) {
|
||||||
stderr.writeln('\nNo Wolf3D game files were found at: $targetPath');
|
if (dataDirectory == null) {
|
||||||
stderr.writeln(
|
stderr.writeln('\nNo Wolf3D game data was discovered.');
|
||||||
'Please provide valid game data files before starting the CLI host.',
|
stderr.writeln('Provide a game-data directory with one of these flags:');
|
||||||
);
|
stderr.writeln(' --data-directory <path>');
|
||||||
|
stderr.writeln(' -d <path>');
|
||||||
|
} else {
|
||||||
|
stderr.writeln('\nNo Wolf3D game files were found at: $dataDirectory');
|
||||||
|
stderr.writeln(
|
||||||
|
'Please provide valid game data files before starting the CLI host.',
|
||||||
|
);
|
||||||
|
}
|
||||||
exitCleanly(1);
|
exitCleanly(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ environment:
|
|||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
args: ^2.7.0
|
||||||
wolf_3d_dart:
|
wolf_3d_dart:
|
||||||
wolf_3d_assets:
|
|
||||||
|
|||||||
@@ -7,24 +7,19 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
|
|
||||||
/// Creates the application shell after loading available Wolf3D data sets.
|
/// Creates the application shell after loading available Wolf3D data sets.
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
if (supportsDesktopWindowing) {
|
final Wolf3dFlutterEngine wolf3d = await Wolf3dFlutterEngine(
|
||||||
await windowManager.ensureInitialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
final Wolf3dFlutterEngine wolf3d = await Wolf3dFlutterEngine().init(
|
|
||||||
debug: kDebugMode,
|
debug: kDebugMode,
|
||||||
);
|
).init();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: Wolf3dApp(wolf3d: wolf3d),
|
home: Wolf3dApp(engine: wolf3d),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ environment:
|
|||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
wolf_3d_dart:
|
wolf_3d_flutter:
|
||||||
wolf_3d_flutter: any
|
|
||||||
window_manager: ^0.5.1
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
/// Whether desktop window-management APIs are expected to work on this host.
|
/// Whether desktop window-management APIs are expected to work on this host.
|
||||||
bool get supportsDesktopWindowing {
|
bool get supportsDesktopWindowing {
|
||||||
@@ -15,3 +17,21 @@ bool get supportsDesktopWindowing {
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures desktop windowing plugin state is initialized when supported.
|
||||||
|
///
|
||||||
|
/// This safely no-ops on non-desktop targets and in hosts where the plugin is
|
||||||
|
/// not registered.
|
||||||
|
Future<void> ensureDesktopWindowingInitialized({
|
||||||
|
WindowManager? windowing,
|
||||||
|
}) async {
|
||||||
|
if (!supportsDesktopWindowing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (windowing ?? windowManager).ensureInitialized();
|
||||||
|
} on MissingPluginException {
|
||||||
|
// No-op on hosts where the window manager plugin is unavailable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
export 'game_data_directory_persistence_stub.dart'
|
||||||
|
if (dart.library.io) 'game_data_directory_persistence_io.dart';
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Persists the configured game-data directory path as JSON.
|
||||||
|
class DefaultGameDataDirectoryPersistence {
|
||||||
|
DefaultGameDataDirectoryPersistence({String? filePath})
|
||||||
|
: _filePath = filePath;
|
||||||
|
|
||||||
|
final String? _filePath;
|
||||||
|
String? _resolvedPath;
|
||||||
|
|
||||||
|
/// Absolute JSON file path used for persistence.
|
||||||
|
String get filePath {
|
||||||
|
if (_resolvedPath != null) {
|
||||||
|
return _resolvedPath!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_filePath != null && _filePath.isNotEmpty) {
|
||||||
|
_resolvedPath = _filePath;
|
||||||
|
return _resolvedPath!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolvedPath = '$platformConfigDir/data_source.json';
|
||||||
|
return _resolvedPath!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a previously persisted data-directory path.
|
||||||
|
Future<String?> loadDataDirectory() async {
|
||||||
|
try {
|
||||||
|
final file = File(filePath);
|
||||||
|
if (!await file.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final raw = await file.readAsString();
|
||||||
|
if (raw.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final decoded = jsonDecode(raw);
|
||||||
|
if (decoded is! Map<String, Object?>) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object? value = decoded['dataDirectory'];
|
||||||
|
if (value is! String) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = value.trim();
|
||||||
|
return normalized.isEmpty ? null : normalized;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists [directoryPath] for future startups.
|
||||||
|
Future<void> saveDataDirectory(String? directoryPath) async {
|
||||||
|
try {
|
||||||
|
final normalized = directoryPath?.trim();
|
||||||
|
final file = File(filePath);
|
||||||
|
|
||||||
|
await file.parent.create(recursive: true);
|
||||||
|
if (normalized == null || normalized.isEmpty) {
|
||||||
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final payload = jsonEncode(<String, Object>{
|
||||||
|
'dataDirectory': normalized,
|
||||||
|
});
|
||||||
|
await file.writeAsString(payload, flush: true);
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort only.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get platformConfigDir {
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
final String xdg = Platform.environment['XDG_CONFIG_HOME'] ?? '';
|
||||||
|
final String home = Platform.environment['HOME'] ?? '.';
|
||||||
|
return xdg.isNotEmpty ? '$xdg/wolf3d' : '$home/.config/wolf3d';
|
||||||
|
}
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
final String home = Platform.environment['HOME'] ?? '.';
|
||||||
|
return '$home/Library/Application Support/wolf3d';
|
||||||
|
}
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
final String appData = Platform.environment['APPDATA'] ?? '.';
|
||||||
|
return '$appData/wolf3d';
|
||||||
|
}
|
||||||
|
final String home = Platform.environment['HOME'] ?? '.';
|
||||||
|
return '$home/.config/wolf3d';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
/// No-op persistence used on platforms without `dart:io` support.
|
||||||
|
class DefaultGameDataDirectoryPersistence {
|
||||||
|
DefaultGameDataDirectoryPersistence({String? filePath});
|
||||||
|
|
||||||
|
/// Loads a previously persisted data-directory path.
|
||||||
|
Future<String?> loadDataDirectory() async {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists [directoryPath] for future startups.
|
||||||
|
Future<void> saveDataDirectory(String? directoryPath) async {}
|
||||||
|
}
|
||||||
@@ -1,15 +1,42 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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.
|
/// Fallback screen shown when no Wolf3D game data files are discovered.
|
||||||
class NoGameDataScreen extends StatelessWidget {
|
class NoGameDataScreen extends StatelessWidget {
|
||||||
const NoGameDataScreen({super.key});
|
const NoGameDataScreen({
|
||||||
|
super.key,
|
||||||
|
this.configuredDataDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Previously configured external game-data directory, if any.
|
||||||
|
final String? configuredDataDirectory;
|
||||||
|
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF140000),
|
backgroundColor: _backgroundColor,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
@@ -17,10 +44,10 @@ class NoGameDataScreen extends StatelessWidget {
|
|||||||
constraints: const BoxConstraints(maxWidth: 640),
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF590002),
|
color: _panelColor,
|
||||||
border: Border.all(color: const Color(0xFFB00000), width: 2),
|
border: Border.all(color: _borderColor, width: 2),
|
||||||
),
|
),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(20),
|
padding: EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -29,25 +56,44 @@ class NoGameDataScreen extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'WOLF3D DATA NOT FOUND',
|
'WOLF3D DATA NOT FOUND',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFFFFF700),
|
color: _titleColor,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No game files were discovered.\n\n'
|
'No game files were discovered.\n\n'
|
||||||
'Add Wolfenstein 3D data files to one of these locations:\n'
|
'Select a game-data directory in setup.',
|
||||||
'- packages/wolf_3d_assets/assets/retail\n'
|
|
||||||
'- packages/wolf_3d_assets/assets/shareware\n'
|
|
||||||
'- or a discoverable local game-data folder.\n\n'
|
|
||||||
'Restart the app after adding the files.',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: _bodyColor,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
height: 1.4,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
|||||||
/// host screens.
|
/// 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 Wolf3dFlutterEngine wolf3d;
|
final Wolf3dFlutterEngine engine;
|
||||||
|
|
||||||
const Wolf3dApp({
|
const Wolf3dApp({
|
||||||
super.key,
|
super.key,
|
||||||
required this.wolf3d,
|
required this.engine,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return wolf3d.availableGames.isEmpty
|
return engine.availableGames.isEmpty
|
||||||
? const NoGameDataScreen()
|
? NoGameDataScreen(
|
||||||
: GameScreen(wolf3d: wolf3d);
|
configuredDataDirectory: engine.configuredDataDirectory,
|
||||||
|
)
|
||||||
|
: GameScreen(wolf3d: engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ 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_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.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/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';
|
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
||||||
|
|
||||||
export 'package:wolf_3d_dart/wolf_3d_audio.dart' show DebugMusicPlayer;
|
export 'package:wolf_3d_dart/wolf_3d_audio.dart' show DebugMusicPlayer;
|
||||||
|
|
||||||
export 'audio/wolf3d_platform_audio.dart' show Wolf3dPlatformAudio;
|
export 'audio/wolf3d_platform_audio.dart' show Wolf3dPlatformAudio;
|
||||||
export 'managers/desktop_windowing_support.dart' show supportsDesktopWindowing;
|
|
||||||
export 'managers/game_app_lifecycle_manager.dart' show GameAppLifecycleManager;
|
export 'managers/game_app_lifecycle_manager.dart' show GameAppLifecycleManager;
|
||||||
|
export 'managers/game_data_directory_persistence.dart'
|
||||||
|
show DefaultGameDataDirectoryPersistence;
|
||||||
export 'managers/game_display_manager.dart' show GameDisplayManager;
|
export 'managers/game_display_manager.dart' show GameDisplayManager;
|
||||||
export 'managers/game_persistence_manager.dart' show GamePersistenceManager;
|
export 'managers/game_persistence_manager.dart' show GamePersistenceManager;
|
||||||
export 'managers/game_renderer_mode_manager.dart'
|
export 'managers/game_renderer_mode_manager.dart'
|
||||||
@@ -44,12 +48,26 @@ export 'widgets/wolf_menu_shell.dart' show WolfMenuShell;
|
|||||||
class Wolf3dFlutterEngine extends Wolf3dEngine {
|
class Wolf3dFlutterEngine extends Wolf3dEngine {
|
||||||
/// Creates an empty facade that must be initialized with [init].
|
/// Creates an empty facade that must be initialized with [init].
|
||||||
Wolf3dFlutterEngine({
|
Wolf3dFlutterEngine({
|
||||||
|
bool debug = false,
|
||||||
EngineAudio? audioBackend,
|
EngineAudio? audioBackend,
|
||||||
Wolf3dFlutterInput? inputBackend,
|
Wolf3dFlutterInput? inputBackend,
|
||||||
}) : super(
|
DefaultGameDataDirectoryPersistence? dataDirectoryPersistence,
|
||||||
|
}) : dataDirectoryPersistence =
|
||||||
|
dataDirectoryPersistence ?? DefaultGameDataDirectoryPersistence(),
|
||||||
|
super(
|
||||||
audio: audioBackend ?? Wolf3dPlatformAudio(),
|
audio: audioBackend ?? Wolf3dPlatformAudio(),
|
||||||
input: inputBackend ?? Wolf3dFlutterInput(),
|
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.
|
/// Shared Flutter input adapter reused by gameplay screens.
|
||||||
@override
|
@override
|
||||||
@@ -63,18 +81,24 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
|
||||||
Future<Wolf3dFlutterEngine> init({
|
Future<Wolf3dFlutterEngine> init({
|
||||||
String? directory,
|
String? directory,
|
||||||
bool debug = false,
|
|
||||||
}) async {
|
}) async {
|
||||||
if (debug) {
|
await desktop_windowing_support.ensureDesktopWindowingInitialized();
|
||||||
enableDebug();
|
|
||||||
}
|
|
||||||
await audio.init();
|
await audio.init();
|
||||||
availableGames.clear();
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Bundled assets let the GUI work out of the box on supported platforms.
|
// Bundled assets let the GUI work out of the box on supported platforms.
|
||||||
final versionsToTry = [
|
final versionsToTry = [
|
||||||
(version: GameVersion.retail, path: 'retail'),
|
(version: GameVersion.retail, path: 'retail'),
|
||||||
@@ -106,10 +130,10 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
|||||||
|
|
||||||
// On non-web platforms, also scan the local filesystem for user-supplied
|
// On non-web platforms, also scan the local filesystem for user-supplied
|
||||||
// data folders so the host can pick up extra versions automatically.
|
// data folders so the host can pick up extra versions automatically.
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb && resolvedDirectory != null) {
|
||||||
try {
|
try {
|
||||||
final externalGames = await WolfensteinLoader.discover(
|
final externalGames = await WolfensteinLoader.discover(
|
||||||
directoryPath: directory,
|
directoryPath: resolvedDirectory,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
);
|
);
|
||||||
for (var entry in externalGames.entries) {
|
for (var entry in externalGames.entries) {
|
||||||
@@ -135,9 +159,3 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ dependencies:
|
|||||||
wolf_3d_dart:
|
wolf_3d_dart:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
wolf_3d_assets: any
|
|
||||||
audioplayers: ^6.6.0
|
audioplayers: ^6.6.0
|
||||||
window_manager: ^0.5.1
|
window_manager: ^0.5.1
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('DefaultGameDataDirectoryPersistence', () {
|
||||||
|
test('saves and loads configured directory', () async {
|
||||||
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
|
'wolf3d-data-config-',
|
||||||
|
);
|
||||||
|
addTearDown(() async {
|
||||||
|
if (await tempDir.exists()) {
|
||||||
|
await tempDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final persistence = DefaultGameDataDirectoryPersistence(
|
||||||
|
filePath: '${tempDir.path}/data_source.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
await persistence.saveDataDirectory('/tmp/wolf-data');
|
||||||
|
|
||||||
|
final loaded = await persistence.loadDataDirectory();
|
||||||
|
expect(loaded, '/tmp/wolf-data');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears persisted path when saving null', () async {
|
||||||
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
|
'wolf3d-data-config-',
|
||||||
|
);
|
||||||
|
addTearDown(() async {
|
||||||
|
if (await tempDir.exists()) {
|
||||||
|
await tempDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final path = '${tempDir.path}/data_source.json';
|
||||||
|
final persistence = DefaultGameDataDirectoryPersistence(filePath: path);
|
||||||
|
|
||||||
|
await persistence.saveDataDirectory('/tmp/wolf-data');
|
||||||
|
await persistence.saveDataDirectory(null);
|
||||||
|
|
||||||
|
expect(await File(path).exists(), isFalse);
|
||||||
|
expect(await persistence.loadDataDirectory(), 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -55,10 +55,13 @@ void main() {
|
|||||||
expect(wolf3d.isDebugEnabled, isTrue);
|
expect(wolf3d.isDebugEnabled, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('init(debug: true) enables debug mode', () async {
|
test('constructor(debug: true) enables debug mode', () async {
|
||||||
final wolf3d = Wolf3dFlutterEngine(audioBackend: _NoopAudio());
|
final wolf3d = Wolf3dFlutterEngine(
|
||||||
|
audioBackend: _NoopAudio(),
|
||||||
|
debug: true,
|
||||||
|
);
|
||||||
|
|
||||||
await wolf3d.init(debug: true);
|
await wolf3d.init();
|
||||||
|
|
||||||
expect(wolf3d.isDebugEnabled, isTrue);
|
expect(wolf3d.isDebugEnabled, isTrue);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user