|
|
|
@@ -83,7 +83,7 @@ abstract class WLParser {
|
|
|
|
|
walls: parseWalls(vswap),
|
|
|
|
|
sprites: parseSprites(vswap),
|
|
|
|
|
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
|
|
|
|
levels: parseMaps(mapHead, gameMaps, isShareware: isShareware),
|
|
|
|
|
episodes: parseEpisodes(mapHead, gameMaps, isShareware: isShareware),
|
|
|
|
|
vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph),
|
|
|
|
|
adLibSounds: audio.adLib,
|
|
|
|
|
music: audio.music,
|
|
|
|
@@ -236,22 +236,39 @@ abstract class WLParser {
|
|
|
|
|
return images;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Episode Names (From the original C Executable) ---
|
|
|
|
|
static const List<String> _sharewareEpisodeNames = [
|
|
|
|
|
"Episode 1\nEscape from Wolfenstein",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
static const List<String> _retailEpisodeNames = [
|
|
|
|
|
"Episode 1\nEscape from Wolfenstein",
|
|
|
|
|
"Episode 2\nOperation: Eisenfaust",
|
|
|
|
|
"Episode 3\nDie, Fuhrer, Die!",
|
|
|
|
|
"Episode 4\nA Dark Secret",
|
|
|
|
|
"Episode 5\nTrail of the Madman",
|
|
|
|
|
"Episode 6\nConfrontation",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
|
|
|
|
|
static List<WolfLevel> parseMaps(
|
|
|
|
|
static List<Episode> parseEpisodes(
|
|
|
|
|
ByteData mapHead,
|
|
|
|
|
ByteData gameMaps, {
|
|
|
|
|
bool isShareware = true,
|
|
|
|
|
}) {
|
|
|
|
|
List<WolfLevel> levels = [];
|
|
|
|
|
|
|
|
|
|
// 1. Select the correct map based on the version
|
|
|
|
|
final activeMusicMap = isShareware ? _sharewareMusicMap : _retailMusicMap;
|
|
|
|
|
|
|
|
|
|
List<WolfLevel> allLevels = [];
|
|
|
|
|
int rlewTag = mapHead.getUint16(0, Endian.little);
|
|
|
|
|
|
|
|
|
|
// Select the correct music map based on the version
|
|
|
|
|
final activeMusicMap = isShareware ? _sharewareMusicMap : _retailMusicMap;
|
|
|
|
|
final episodeNames = isShareware
|
|
|
|
|
? _sharewareEpisodeNames
|
|
|
|
|
: _retailEpisodeNames;
|
|
|
|
|
|
|
|
|
|
// The game allows for up to 100 maps per file
|
|
|
|
|
for (int i = 0; i < 100; i++) {
|
|
|
|
|
int mapOffset = mapHead.getUint32(2 + (i * 4), Endian.little);
|
|
|
|
|
if (mapOffset == 0) continue;
|
|
|
|
|
if (mapOffset == 0) continue; // Empty map slot
|
|
|
|
|
|
|
|
|
|
int plane0Offset = gameMaps.getUint32(mapOffset + 0, Endian.little);
|
|
|
|
|
int plane1Offset = gameMaps.getUint32(mapOffset + 4, Endian.little);
|
|
|
|
@@ -259,13 +276,15 @@ abstract class WLParser {
|
|
|
|
|
int plane0Length = gameMaps.getUint16(mapOffset + 12, Endian.little);
|
|
|
|
|
int plane1Length = gameMaps.getUint16(mapOffset + 14, Endian.little);
|
|
|
|
|
|
|
|
|
|
// --- EXTRACT ACTUAL GAME DATA NAME ---
|
|
|
|
|
// The name is exactly 16 bytes long, starting at offset 22
|
|
|
|
|
List<int> nameBytes = [];
|
|
|
|
|
for (int n = 0; n < 16; n++) {
|
|
|
|
|
int charCode = gameMaps.getUint8(mapOffset + 22 + n);
|
|
|
|
|
if (charCode == 0) break;
|
|
|
|
|
if (charCode == 0) break; // Stop at the null-terminator
|
|
|
|
|
nameBytes.add(charCode);
|
|
|
|
|
}
|
|
|
|
|
String name = ascii.decode(nameBytes);
|
|
|
|
|
String parsedName = ascii.decode(nameBytes);
|
|
|
|
|
|
|
|
|
|
// --- DECOMPRESS PLANES ---
|
|
|
|
|
final compressedWallData = gameMaps.buffer.asUint8List(
|
|
|
|
@@ -282,7 +301,7 @@ abstract class WLParser {
|
|
|
|
|
Uint16List carmackExpandedObjects = _expandCarmack(compressedObjectData);
|
|
|
|
|
List<int> flatObjectGrid = _expandRlew(carmackExpandedObjects, rlewTag);
|
|
|
|
|
|
|
|
|
|
// --- BUILD GRIDS ---
|
|
|
|
|
// --- BUILD 64x64 GRIDS ---
|
|
|
|
|
List<List<int>> wallGrid = [];
|
|
|
|
|
List<List<int>> objectGrid = [];
|
|
|
|
|
|
|
|
|
@@ -297,17 +316,14 @@ abstract class WLParser {
|
|
|
|
|
objectGrid.add(objectRow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine music track index.
|
|
|
|
|
// We use 'i' because it represents the absolute map slot (e.g. Map 12).
|
|
|
|
|
// If a map exists past the bounds of our lookup table (custom maps),
|
|
|
|
|
// we wrap around safely.
|
|
|
|
|
// --- ASSIGN MUSIC ---
|
|
|
|
|
int trackIndex = (i < activeMusicMap.length)
|
|
|
|
|
? activeMusicMap[i]
|
|
|
|
|
: activeMusicMap[i % activeMusicMap.length];
|
|
|
|
|
|
|
|
|
|
levels.add(
|
|
|
|
|
allLevels.add(
|
|
|
|
|
WolfLevel(
|
|
|
|
|
name: name,
|
|
|
|
|
name: parsedName,
|
|
|
|
|
wallGrid: wallGrid,
|
|
|
|
|
objectGrid: objectGrid,
|
|
|
|
|
musicIndex: trackIndex,
|
|
|
|
@@ -315,7 +331,35 @@ abstract class WLParser {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return levels;
|
|
|
|
|
// 2. Group the parsed levels into Episodes!
|
|
|
|
|
List<Episode> episodes = [];
|
|
|
|
|
|
|
|
|
|
// Calculate how many episodes we need (10 levels per episode)
|
|
|
|
|
int totalEpisodes = (allLevels.length / 10).ceil();
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < totalEpisodes; i++) {
|
|
|
|
|
int startIndex = i * 10;
|
|
|
|
|
int endIndex = startIndex + 10;
|
|
|
|
|
|
|
|
|
|
// Safety clamp for incomplete episodes at the end of the file
|
|
|
|
|
if (endIndex > allLevels.length) {
|
|
|
|
|
endIndex = allLevels.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we run out of hardcoded id Software names, generate a custom one!
|
|
|
|
|
String epName = (i < episodeNames.length)
|
|
|
|
|
? episodeNames[i]
|
|
|
|
|
: "Episode ${i + 1}\nCustom Maps";
|
|
|
|
|
|
|
|
|
|
episodes.add(
|
|
|
|
|
Episode(
|
|
|
|
|
name: epName,
|
|
|
|
|
levels: allLevels.sublist(startIndex, endIndex),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return episodes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extracts AdLib sounds and IMF music tracks from the audio files.
|
|
|
|
|