Added audio loading and decompressing
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -58,47 +58,68 @@ class GameSelectScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<WolfensteinData>> loadData([
|
||||
bool? isShareware,
|
||||
]) async {
|
||||
Future<List<WolfensteinData>> loadData([String? directory]) 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);
|
||||
// 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 config in versionsToTry) {
|
||||
try {
|
||||
final data = WolfensteinLoader.loadFromBytes(
|
||||
version: config.version,
|
||||
vswap: await rootBundle.load(
|
||||
'assets/${config.path}/VSWAP.${config.version.fileExtension}',
|
||||
),
|
||||
mapHead: await rootBundle.load(
|
||||
'assets/${config.path}/MAPHEAD.${config.version.fileExtension}',
|
||||
),
|
||||
gameMaps: await rootBundle.load(
|
||||
'assets/${config.path}/GAMEMAPS.${config.version.fileExtension}',
|
||||
),
|
||||
vgaDict: await rootBundle.load(
|
||||
'assets/${config.path}/VGADICT.${config.version.fileExtension}',
|
||||
),
|
||||
vgaHead: await rootBundle.load(
|
||||
'assets/${config.path}/VGAHEAD.${config.version.fileExtension}',
|
||||
),
|
||||
vgaGraph: await rootBundle.load(
|
||||
'assets/${config.path}/VGAGRAPH.${config.version.fileExtension}',
|
||||
),
|
||||
audioHed: await rootBundle.load(
|
||||
'assets/${config.path}/AUDIOHED.${config.version.fileExtension}',
|
||||
),
|
||||
audioT: await rootBundle.load(
|
||||
'assets/${config.path}/AUDIOT.${config.version.fileExtension}',
|
||||
),
|
||||
);
|
||||
loadedGames.add(data);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
"Bundled ${config.version.name} not found or failed to load.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -4,7 +4,9 @@ enum GameFile {
|
||||
gameMaps('GAMEMAPS'),
|
||||
vgaDict('VGADICT'),
|
||||
vgaHead('VGAHEAD'),
|
||||
vgaGraph('VGAGRAPH')
|
||||
vgaGraph('VGAGRAPH'),
|
||||
audioHed('AUDIOHED'),
|
||||
audioT('AUDIOT')
|
||||
;
|
||||
|
||||
final String baseName;
|
||||
|
||||
@@ -4,3 +4,13 @@ class PcmSound {
|
||||
final Uint8List bytes;
|
||||
PcmSound(this.bytes);
|
||||
}
|
||||
|
||||
class AdLibSound {
|
||||
final Uint8List bytes;
|
||||
AdLibSound(this.bytes);
|
||||
}
|
||||
|
||||
class ImfMusic {
|
||||
final Uint8List bytes;
|
||||
ImfMusic(this.bytes);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ class WolfensteinData {
|
||||
final List<Sprite> walls;
|
||||
final List<Sprite> sprites;
|
||||
final List<PcmSound> sounds;
|
||||
final List<AdLibSound> adLibSounds;
|
||||
final List<ImfMusic> music;
|
||||
final List<WolfLevel> levels;
|
||||
final List<VgaImage> vgaImages;
|
||||
|
||||
@@ -13,6 +15,8 @@ class WolfensteinData {
|
||||
required this.walls,
|
||||
required this.sprites,
|
||||
required this.sounds,
|
||||
required this.adLibSounds,
|
||||
required this.music,
|
||||
required this.levels,
|
||||
required this.vgaImages,
|
||||
});
|
||||
|
||||
@@ -62,6 +62,8 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
||||
vgaDict: await _readFile(foundFiles[GameFile.vgaDict]!),
|
||||
vgaHead: await _readFile(foundFiles[GameFile.vgaHead]!),
|
||||
vgaGraph: await _readFile(foundFiles[GameFile.vgaGraph]!),
|
||||
audioHed: await _readFile(foundFiles[GameFile.audioHed]!),
|
||||
audioT: await _readFile(foundFiles[GameFile.audioT]!),
|
||||
);
|
||||
|
||||
loadedVersions[version] = data;
|
||||
|
||||
@@ -47,6 +47,8 @@ abstract class WLParser {
|
||||
vgaDict: await fileFetcher('VGADICT.$ext'),
|
||||
vgaHead: await fileFetcher('VGAHEAD.$ext'),
|
||||
vgaGraph: await fileFetcher('VGAGRAPH.$ext'),
|
||||
audioHed: await fileFetcher('AUDIOHED.$ext'),
|
||||
audioT: await fileFetcher('AUDIOT.$ext'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,9 +63,13 @@ abstract class WLParser {
|
||||
required ByteData vgaDict,
|
||||
required ByteData vgaHead,
|
||||
required ByteData vgaGraph,
|
||||
required ByteData audioHed,
|
||||
required ByteData audioT,
|
||||
}) {
|
||||
final isShareware = version == GameVersion.shareware;
|
||||
|
||||
final audio = parseAudio(audioHed, audioT);
|
||||
|
||||
return WolfensteinData(
|
||||
version: version,
|
||||
walls: parseWalls(vswap),
|
||||
@@ -71,6 +77,8 @@ abstract class WLParser {
|
||||
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
||||
levels: parseMaps(mapHead, gameMaps, isShareware: isShareware),
|
||||
vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph),
|
||||
adLibSounds: audio.adLib,
|
||||
music: audio.music,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,6 +297,56 @@ abstract class WLParser {
|
||||
return levels;
|
||||
}
|
||||
|
||||
/// Extracts AdLib sounds and IMF music tracks from the audio files.
|
||||
static ({List<AdLibSound> adLib, List<ImfMusic> music}) parseAudio(
|
||||
ByteData audioHed,
|
||||
ByteData audioT,
|
||||
) {
|
||||
List<int> offsets = [];
|
||||
// AUDIOHED is a series of 32-bit unsigned integers
|
||||
for (int i = 0; i < audioHed.lengthInBytes ~/ 4; i++) {
|
||||
offsets.add(audioHed.getUint32(i * 4, Endian.little));
|
||||
}
|
||||
|
||||
List<Uint8List> allAudioChunks = [];
|
||||
|
||||
for (int i = 0; i < offsets.length - 1; i++) {
|
||||
int start = offsets[i];
|
||||
int next = offsets[i + 1];
|
||||
|
||||
// 0xFFFFFFFF (or 4294967295) marks an empty slot
|
||||
if (start == 0xFFFFFFFF || start >= audioT.lengthInBytes) {
|
||||
allAudioChunks.add(Uint8List(0));
|
||||
continue;
|
||||
}
|
||||
|
||||
int length = next - start;
|
||||
if (length <= 0) {
|
||||
allAudioChunks.add(Uint8List(0));
|
||||
} else {
|
||||
allAudioChunks.add(
|
||||
audioT.buffer.asUint8List(audioT.offsetInBytes + start, length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wolfenstein 3D split:
|
||||
// Chunks 0-299: AdLib Sounds
|
||||
// Chunks 300+: IMF Music
|
||||
List<AdLibSound> adLib = allAudioChunks
|
||||
.take(300)
|
||||
.map((bytes) => AdLibSound(bytes))
|
||||
.toList();
|
||||
|
||||
List<ImfMusic> music = allAudioChunks
|
||||
.skip(300)
|
||||
.where((chunk) => chunk.isNotEmpty)
|
||||
.map((bytes) => ImfMusic(bytes))
|
||||
.toList();
|
||||
|
||||
return (adLib: adLib, music: music);
|
||||
}
|
||||
|
||||
// --- ALGORITHM 1: CARMACK EXPANSION ---
|
||||
static Uint16List _expandCarmack(Uint8List compressed) {
|
||||
ByteData data = ByteData.sublistView(compressed);
|
||||
|
||||
@@ -33,6 +33,8 @@ class WolfensteinLoader {
|
||||
required ByteData vgaDict,
|
||||
required ByteData vgaHead,
|
||||
required ByteData vgaGraph,
|
||||
required ByteData audioHed,
|
||||
required ByteData audioT,
|
||||
}) {
|
||||
// We just act as a clean pass-through to the core parser
|
||||
return WLParser.load(
|
||||
@@ -43,6 +45,8 @@ class WolfensteinLoader {
|
||||
vgaDict: vgaDict,
|
||||
vgaHead: vgaHead,
|
||||
vgaGraph: vgaGraph,
|
||||
audioHed: audioHed,
|
||||
audioT: audioT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ 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;
|
||||
export 'src/classes/sound.dart' show PcmSound, AdLibSound, ImfMusic;
|
||||
export 'src/classes/sprite.dart' hide Matrix;
|
||||
export 'src/classes/wolf_level.dart' show WolfLevel;
|
||||
export 'src/classes/wolfenstein_data.dart' show WolfensteinData;
|
||||
|
||||
Reference in New Issue
Block a user