Added dynamic discovery of available game data

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 11:23:41 +01:00
parent 34b167e03f
commit 2db9dad00d
9 changed files with 288 additions and 54 deletions

View File

@@ -1,16 +1,17 @@
import 'package:flutter/material.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/renderer/renderer.dart';
class DifficultyScreen extends StatefulWidget {
const DifficultyScreen({super.key});
class DifficultyScreen extends StatelessWidget {
const DifficultyScreen(
this.data, {
super.key,
});
@override
State<DifficultyScreen> createState() => _DifficultyScreenState();
}
final WolfensteinData data;
class _DifficultyScreenState extends State<DifficultyScreen> {
bool isShareware = true; // Default to Shareware (WL1)
bool get isShareware => data.version == GameVersion.shareware;
@override
Widget build(BuildContext context) {
@@ -22,6 +23,7 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => WolfRenderer(
data,
difficulty: Difficulty.bringEmOn,
isShareware: isShareware,
showSpriteGallery: true,
@@ -44,27 +46,6 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
fontFamily: 'Courier',
),
),
const SizedBox(height: 20),
// --- Version Toggle ---
Theme(
data: ThemeData(unselectedWidgetColor: Colors.grey),
child: CheckboxListTile(
title: const Text(
"Play Shareware Version (WL1)",
style: TextStyle(color: Colors.white),
),
value: isShareware,
onChanged: (bool? value) {
setState(() {
isShareware = value ?? true;
});
},
controlAffinity: ListTileControlAffinity.leading,
contentPadding: const EdgeInsets.symmetric(horizontal: 100),
),
),
const SizedBox(height: 20),
// --- Difficulty Buttons ---
ListView.builder(
@@ -87,6 +68,7 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => WolfRenderer(
data,
difficulty: difficulty,
isShareware: isShareware,
),

View File

@@ -2,7 +2,6 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
@@ -21,13 +20,15 @@ import 'package:wolf_dart/features/ui/hud.dart';
import 'package:wolf_dart/sprite_gallery.dart';
class WolfRenderer extends StatefulWidget {
const WolfRenderer({
const WolfRenderer(
this.data, {
super.key,
this.difficulty = Difficulty.bringEmOn,
this.showSpriteGallery = false,
this.isShareware = true,
});
final WolfensteinData data;
final Difficulty difficulty;
final bool showSpriteGallery;
final bool isShareware;
@@ -44,7 +45,6 @@ class _WolfRendererState extends State<WolfRenderer>
late Ticker _gameLoop;
final FocusNode _focusNode = FocusNode();
late WolfensteinData gameData;
late Level currentLevel;
late WolfLevel activeLevel;
@@ -61,22 +61,12 @@ class _WolfRendererState extends State<WolfRenderer>
@override
void initState() {
super.initState();
_initGame(widget.isShareware);
_initGame();
}
Future<void> _initGame(bool isShareware) async {
gameData = await WLParser.loadAsync(
(filename) => rootBundle.load(
'assets/${isShareware ? "shareware" : "retail"}/$filename',
),
);
print('Detected Game Version: ${gameData.version.name}');
print('Loaded ${gameData.levels.length} levels!');
print('Loaded ${gameData.vgaImages.length} images!');
Future<void> _initGame() async {
// Get the first level out of the data class
activeLevel = gameData.levels.first;
activeLevel = widget.data.levels.first;
// Set up your grids directly from the active level
currentLevel = activeLevel.wallGrid;
@@ -118,8 +108,8 @@ class _WolfRendererState extends State<WolfRenderer>
x + 0.5,
y + 0.5,
widget.difficulty,
gameData.sprites.length,
isSharewareMode: isShareware,
widget.data.sprites.length,
isSharewareMode: widget.isShareware,
);
if (newEntity != null) {
@@ -364,7 +354,7 @@ class _WolfRendererState extends State<WolfRenderer>
entity.x,
entity.y,
widget.difficulty,
gameData.sprites.length,
widget.data.sprites.length,
);
if (droppedAmmo != null) {
@@ -413,7 +403,7 @@ class _WolfRendererState extends State<WolfRenderer>
}
if (widget.showSpriteGallery) {
return SpriteGallery(sprites: gameData.sprites);
return SpriteGallery(sprites: widget.data.sprites);
}
return Scaffold(
@@ -439,12 +429,12 @@ class _WolfRendererState extends State<WolfRenderer>
),
painter: RaycasterPainter(
map: currentLevel,
textures: gameData.walls,
textures: widget.data.walls,
player: player,
fov: fov,
doorOffsets: doorManager.getOffsetsForRenderer(),
entities: entities,
sprites: gameData.sprites,
sprites: widget.data.sprites,
activePushwall: pushwallManager.activePushwall,
),
),
@@ -461,9 +451,10 @@ class _WolfRendererState extends State<WolfRenderer>
child: CustomPaint(
painter: WeaponPainter(
sprite:
gameData.sprites[player.currentWeapon
widget.data.sprites[player
.currentWeapon
.getCurrentSpriteIndex(
gameData.sprites.length,
widget.data.sprites.length,
)],
),
),

106
lib/game_select_screen.dart Normal file
View File

@@ -0,0 +1,106 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_dart/features/difficulty/difficulty_screen.dart';
class GameSelectScreen extends StatelessWidget {
const GameSelectScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: loadData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (!snapshot.hasData) {
return Text("Unable to load data");
}
final List<WolfensteinData> loadedGames = snapshot.data!;
if (loadedGames.length == 1) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DifficultyScreen(loadedGames.first),
),
);
});
}
return ListView.builder(
itemCount: loadedGames.length,
itemBuilder: (context, i) {
final WolfensteinData data = loadedGames[i];
final GameVersion version = data.version;
return Card(
child: ListTile(
title: Text(version.name),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DifficultyScreen(data),
),
);
},
),
);
},
);
},
),
);
}
Future<List<WolfensteinData>> loadData([
bool? isShareware,
]) async {
final List<WolfensteinData> loadedGames = [];
if (kIsWeb) {
switch (isShareware) {
case false:
loadedGames.add(
WolfensteinLoader.loadFromBytes(
version: GameVersion.retail,
vswap: await rootBundle.load('assets/retail/VSWAP.WL6'),
mapHead: await rootBundle.load('assets/retail/MAPHEAD.WL6'),
gameMaps: await rootBundle.load('assets/retail/GAMEMAPS.WL6'),
vgaDict: await rootBundle.load('assets/retail/VGADICT.WL6'),
vgaHead: await rootBundle.load('assets/retail/VGAHEAD.WL6'),
vgaGraph: await rootBundle.load('assets/retail/VGAGRAPH.WL6'),
),
);
break;
default:
loadedGames.add(
WolfensteinLoader.loadFromBytes(
version: GameVersion.shareware,
vswap: await rootBundle.load('assets/shareware/VSWAP.WL1'),
mapHead: await rootBundle.load('assets/shareware/MAPHEAD.WL1'),
gameMaps: await rootBundle.load('assets/shareware/GAMEMAPS.WL1'),
vgaDict: await rootBundle.load('assets/shareware/VGADICT.WL1'),
vgaHead: await rootBundle.load('assets/shareware/VGAHEAD.WL1'),
vgaGraph: await rootBundle.load('assets/shareware/VGAGRAPH.WL1'),
),
);
break;
}
} else {
final Map<GameVersion, WolfensteinData> discoveredVersions =
await WolfensteinLoader.discover(
directoryPath: 'assets',
recursive: true,
);
loadedGames.addAll(discoveredVersions.values);
}
return loadedGames;
}
}

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:wolf_dart/features/difficulty/difficulty_screen.dart';
import 'package:wolf_dart/game_select_screen.dart';
void main() {
runApp(
const MaterialApp(
home: DifficultyScreen(),
home: GameSelectScreen(),
),
);
}

View File

@@ -0,0 +1,13 @@
enum GameFile {
vswap('VSWAP'),
mapHead('MAPHEAD'),
gameMaps('GAMEMAPS'),
vgaDict('VGADICT'),
vgaHead('VGAHEAD'),
vgaGraph('VGAGRAPH')
;
final String baseName;
const GameFile(this.baseName);
}

View File

@@ -0,0 +1,79 @@
import 'dart:io';
import 'dart:typed_data';
import '../classes/game_file.dart';
import '../classes/game_version.dart';
import '../classes/wolfenstein_data.dart';
import '../wl_parser.dart';
/// dart:io implementation for directory discovery.
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
String? directoryPath,
bool recursive = false,
}) async {
final dir = Directory(directoryPath ?? Directory.current.path);
if (!await dir.exists()) {
print('Warning: Directory does not exist -> ${dir.path}');
return {};
}
final allFiles = await dir
.list(recursive: recursive)
.where((entity) => entity is File)
.cast<File>()
.toList();
final Map<GameVersion, WolfensteinData> loadedVersions = {};
for (final version in GameVersion.values) {
final ext = version.fileExtension.toUpperCase();
final Map<GameFile, File> foundFiles = {};
for (final requiredFile in GameFile.values) {
final expectedName = '${requiredFile.baseName}.$ext';
final match = allFiles.where((file) {
final fileName = file.uri.pathSegments.last.toUpperCase();
return fileName == expectedName;
}).firstOrNull;
if (match != null) {
foundFiles[requiredFile] = match;
}
}
if (foundFiles.isEmpty) continue;
if (foundFiles.length < GameFile.values.length) {
final missingFiles = GameFile.values
.where((f) => !foundFiles.containsKey(f))
.map((f) => '${f.baseName}.$ext')
.join(', ');
print('Found partial data for ${version.name}. Missing: $missingFiles');
continue;
}
try {
final data = WLParser.load(
version: version,
vswap: await _readFile(foundFiles[GameFile.vswap]!),
mapHead: await _readFile(foundFiles[GameFile.mapHead]!),
gameMaps: await _readFile(foundFiles[GameFile.gameMaps]!),
vgaDict: await _readFile(foundFiles[GameFile.vgaDict]!),
vgaHead: await _readFile(foundFiles[GameFile.vgaHead]!),
vgaGraph: await _readFile(foundFiles[GameFile.vgaGraph]!),
);
loadedVersions[version] = data;
} catch (e) {
print('Error parsing data for ${version.name}: $e');
}
}
return loadedVersions;
}
Future<ByteData> _readFile(File file) async {
final bytes = await file.readAsBytes();
return bytes.buffer.asByteData();
}

View File

@@ -0,0 +1,13 @@
import '../classes/game_version.dart';
import '../classes/wolfenstein_data.dart';
/// Web-safe stub for directory discovery.
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
String? directoryPath,
bool recursive = false,
}) async {
throw UnsupportedError(
'Directory scanning is not supported on Web. '
'Please load the files manually using WolfensteinLoader.loadFromBytes().',
);
}

View File

@@ -0,0 +1,48 @@
import 'dart:typed_data';
import 'classes/game_version.dart';
import 'classes/wolfenstein_data.dart';
// --- The Magic Conditional Import ---
// If dart:io is available, use the real scanner. Otherwise, use the stub.
import 'io/discovery_stub.dart'
if (dart.library.io) 'io/discovery_io.dart'
as platform;
import 'wl_parser.dart';
class WolfensteinLoader {
/// Scans a directory for Wolfenstein 3D data files and loads all available versions.
///
/// NOTE: This will throw an [UnsupportedError] on Web platforms.
static Future<Map<GameVersion, WolfensteinData>> discover({
String? directoryPath,
bool recursive = false,
}) {
return platform.discoverInDirectory(
directoryPath: directoryPath,
recursive: recursive,
);
}
/// Parses WolfensteinData directly from raw ByteData.
/// This is 100% pure Dart and is safe to use on all platforms, including Web.
static WolfensteinData loadFromBytes({
required GameVersion version,
required ByteData vswap,
required ByteData mapHead,
required ByteData gameMaps,
required ByteData vgaDict,
required ByteData vgaHead,
required ByteData vgaGraph,
}) {
// We just act as a clean pass-through to the core parser
return WLParser.load(
version: version,
vswap: vswap,
mapHead: mapHead,
gameMaps: gameMaps,
vgaDict: vgaDict,
vgaHead: vgaHead,
vgaGraph: vgaGraph,
);
}
}

View File

@@ -3,6 +3,7 @@
/// More dartdocs go here.
library;
export 'src/classes/game_file.dart' show GameFile;
export 'src/classes/game_version.dart' show GameVersion;
export 'src/classes/image.dart' show VgaImage;
export 'src/classes/sound.dart' show PcmSound;
@@ -10,3 +11,4 @@ export 'src/classes/sprite.dart' hide Matrix;
export 'src/classes/wolf_level.dart' show WolfLevel;
export 'src/classes/wolfenstein_data.dart' show WolfensteinData;
export 'src/wl_parser.dart' show WLParser;
export 'src/wolfenstein_loader.dart' show WolfensteinLoader;