Move initialization and access to a singleton

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 14:03:28 +01:00
parent 27713dbbfb
commit 070110adae
5 changed files with 124 additions and 117 deletions

View File

@@ -4,15 +4,13 @@ import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_synth/wolf_3d_synth.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/renderer/renderer.dart';
import 'package:wolf_dart/wolf_3d.dart';
class DifficultyScreen extends StatefulWidget {
const DifficultyScreen(
this.data, {
const DifficultyScreen({
super.key,
});
final WolfensteinData data;
@override
State<DifficultyScreen> createState() => _DifficultyScreenState();
}
@@ -21,7 +19,7 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
AudioSource? _menuMusicSource;
SoundHandle? _menuMusicHandle;
bool get isShareware => widget.data.version == GameVersion.shareware;
bool get isShareware => Wolf3d.I.activeGame.version == GameVersion.shareware;
@override
void initState() {
@@ -37,9 +35,9 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
}
// 2. We only want to play music if the IMF data actually exists
if (widget.data.music.isNotEmpty) {
if (Wolf3d.I.music.isNotEmpty) {
// Get the first track (usually the menu theme "Wondering About My Loved Ones")
final music = widget.data.music.first;
final music = Wolf3d.I.music.first;
// Render the hardware instructions into PCM and wrap in a WAV header
final pcmSamples = ImfRenderer.render(music);
@@ -85,7 +83,7 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => WolfRenderer(
widget.data,
Wolf3d.I.activeGame,
difficulty: difficulty,
isShareware: isShareware,
showSpriteGallery: showGallery,

View File

@@ -1,9 +1,7 @@
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_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_dart/features/difficulty/difficulty_screen.dart';
import 'package:wolf_dart/wolf_3d.dart';
class GameSelectScreen extends StatelessWidget {
const GameSelectScreen({super.key});
@@ -11,115 +9,27 @@ class GameSelectScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: loadData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
body: ListView.builder(
itemCount: Wolf3d.I.availableGames.length,
itemBuilder: (context, i) {
final WolfensteinData data = Wolf3d.I.availableGames[i];
final GameVersion version = data.version;
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),
),
);
},
),
);
},
return Card(
child: ListTile(
title: Text(version.name),
onTap: () {
Wolf3d.I.setActiveGame(data);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DifficultyScreen(),
),
);
},
),
);
},
),
);
}
Future<ByteData?> tryLoad(String path) async {
try {
return await rootBundle.load(path);
} catch (e) {
debugPrint("Asset not found: $path");
return null;
}
}
Future<List<WolfensteinData>> loadData({String? directory}) async {
final List<WolfensteinData> loadedGames = [];
// 1. Always attempt to load bundled assets first (works on ALL 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 = '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'),
);
loadedGames.add(data);
} catch (e) {
// The loader now provides the specific error:
// "ArgumentError: Cannot load retail: Missing files: VSWAP.WL6, ..."
debugPrint(e.toString());
}
}
// 2. On non-web, also check for external files in a specific "games" folder
// if you want to support side-loading.
if (!kIsWeb) {
try {
final externalGames = await WolfensteinLoader.discover(
directoryPath: directory,
recursive: true,
);
for (var entry in externalGames.entries) {
if (!loadedGames.any((g) => g.version == entry.key)) {
loadedGames.add(entry.value);
}
}
} catch (e) {
debugPrint("External discovery failed: $e");
}
}
return loadedGames;
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';
import 'package:wolf_dart/game_select_screen.dart';
import 'package:wolf_dart/wolf_3d.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -11,6 +12,8 @@ void main() async {
channels: Channels.stereo,
);
await Wolf3d.I.init();
runApp(
const MaterialApp(
home: GameSelectScreen(),

96
lib/wolf_3d.dart Normal file
View File

@@ -0,0 +1,96 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
class Wolf3d {
Wolf3d._();
static final Wolf3d _instance = Wolf3d._();
static Wolf3d get I => _instance;
// --- State ---
final List<WolfensteinData> availableGames = [];
WolfensteinData? _activeGame;
// --- Getters ---
WolfensteinData get activeGame {
if (_activeGame == null) {
throw StateError("No active game selected. Call setActiveGame() first.");
}
return _activeGame!;
}
// Convenience getters for the active game's assets
List<WolfLevel> get levels => activeGame.levels;
List<Sprite> get walls => activeGame.walls;
List<Sprite> get sprites => activeGame.sprites;
List<PcmSound> get sounds => activeGame.sounds;
List<AdLibSound> get adLibSounds => activeGame.adLibSounds;
List<ImfMusic> get music => activeGame.music;
List<VgaImage> get vgaImages => activeGame.vgaImages;
// --- Actions ---
void setActiveGame(WolfensteinData game) {
_activeGame = game;
}
/// Initializes the engine by loading available game data.
Future<void> init({String? directory}) async {
availableGames.clear();
// 1. Bundle asset loading (migrated from GameSelectScreen)
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 = '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());
}
}
// 2. External side-loading (non-web)
if (!kIsWeb) {
try {
final externalGames = await WolfensteinLoader.discover(
directoryPath: directory,
recursive: true,
);
for (var entry in externalGames.entries) {
if (!availableGames.any((g) => g.version == entry.key)) {
availableGames.add(entry.value);
}
}
} catch (e) {
debugPrint("External discovery failed: $e");
}
}
}
Future<ByteData?> _tryLoad(String path) async {
try {
return await rootBundle.load(path);
} catch (e) {
debugPrint("Asset not found: $path");
return null;
}
}
}