Moved data loading to parser. Added remaining shareware data.
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,11 +1,79 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:wolf_3d_data/src/classes/game_version.dart';
|
||||
import 'package:wolf_3d_data/src/classes/image.dart';
|
||||
import 'package:wolf_3d_data/src/classes/sound.dart';
|
||||
import 'package:wolf_3d_data/src/classes/wolf_level.dart';
|
||||
import 'package:wolf_3d_data/src/classes/wolfenstein_data.dart';
|
||||
|
||||
import 'classes/sprite.dart';
|
||||
|
||||
class WLParser {
|
||||
abstract class WLParser {
|
||||
/// Asynchronously discovers the game version and loads all necessary files.
|
||||
/// Provide a [fileFetcher] callback (e.g., Flutter's rootBundle.load) that
|
||||
/// takes a filename and returns its ByteData.
|
||||
static Future<WolfensteinData> loadAsync(
|
||||
Future<ByteData> Function(String filename) fileFetcher,
|
||||
) async {
|
||||
GameVersion? detectedVersion;
|
||||
ByteData? vswap;
|
||||
|
||||
// 1. Probe the data source to figure out which version we have
|
||||
for (final version in GameVersion.values) {
|
||||
try {
|
||||
vswap = await fileFetcher('VSWAP.${version.fileExtension}');
|
||||
detectedVersion = version;
|
||||
break; // We found the version!
|
||||
} catch (_) {
|
||||
// File wasn't found, try the next version extension
|
||||
}
|
||||
}
|
||||
|
||||
if (detectedVersion == null || vswap == null) {
|
||||
throw Exception(
|
||||
'Could not locate a valid VSWAP file for any game version.',
|
||||
);
|
||||
}
|
||||
|
||||
final ext = detectedVersion.fileExtension;
|
||||
|
||||
// 2. Now that we know the version, confidently load the rest of the files
|
||||
return load(
|
||||
version: detectedVersion,
|
||||
vswap: vswap,
|
||||
mapHead: await fileFetcher('MAPHEAD.$ext'),
|
||||
gameMaps: await fileFetcher('GAMEMAPS.$ext'),
|
||||
vgaDict: await fileFetcher('VGADICT.$ext'),
|
||||
vgaHead: await fileFetcher('VGAHEAD.$ext'),
|
||||
vgaGraph: await fileFetcher('VGAGRAPH.$ext'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses all raw ByteData upfront and returns a fully populated
|
||||
/// WolfensteinData object. By using named parameters, the compiler
|
||||
/// guarantees no files are missing or misnamed.
|
||||
static WolfensteinData load({
|
||||
required GameVersion version,
|
||||
required ByteData vswap,
|
||||
required ByteData mapHead,
|
||||
required ByteData gameMaps,
|
||||
required ByteData vgaDict,
|
||||
required ByteData vgaHead,
|
||||
required ByteData vgaGraph,
|
||||
}) {
|
||||
final isShareware = version == GameVersion.shareware;
|
||||
|
||||
return WolfensteinData(
|
||||
version: version,
|
||||
walls: parseWalls(vswap),
|
||||
sprites: parseSprites(vswap),
|
||||
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
||||
levels: parseMaps(mapHead, gameMaps, isShareware: isShareware),
|
||||
vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph),
|
||||
);
|
||||
}
|
||||
|
||||
/// Extracts the 64x64 wall textures from VSWAP.WL1
|
||||
static List<Sprite> parseWalls(ByteData vswap) {
|
||||
final header = _VswapHeader(vswap);
|
||||
@@ -114,6 +182,44 @@ class WLParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts and decodes the UI graphics, Title Screens, and Menu items
|
||||
static List<VgaImage> parseVgaImages(
|
||||
ByteData vgaDict,
|
||||
ByteData vgaHead,
|
||||
ByteData vgaGraph,
|
||||
) {
|
||||
// 1. Get all raw decompressed chunks using the Huffman algorithm
|
||||
List<Uint8List> rawChunks = _parseVgaRaw(vgaDict, vgaHead, vgaGraph);
|
||||
|
||||
// 2. Chunk 0 is the Picture Table (Dimensions for all images)
|
||||
// It contains consecutive 16-bit widths and 16-bit heights
|
||||
ByteData picTable = ByteData.sublistView(rawChunks[0]);
|
||||
int numPics = picTable.lengthInBytes ~/ 4;
|
||||
|
||||
List<VgaImage> images = [];
|
||||
|
||||
// 3. In Wolf3D, Chunk 1 and 2 are fonts. Pictures start at Chunk 3!
|
||||
int picStartIndex = 3;
|
||||
|
||||
for (int i = 0; i < numPics; i++) {
|
||||
int width = picTable.getUint16(i * 4, Endian.little);
|
||||
int height = picTable.getUint16(i * 4 + 2, Endian.little);
|
||||
|
||||
// Safety check: ensure we don't read out of bounds
|
||||
if (picStartIndex + i < rawChunks.length) {
|
||||
images.add(
|
||||
VgaImage(
|
||||
width: width,
|
||||
height: height,
|
||||
pixels: rawChunks[picStartIndex + i],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
|
||||
static List<WolfLevel> parseMaps(
|
||||
ByteData mapHead,
|
||||
@@ -260,6 +366,128 @@ class WLParser {
|
||||
}
|
||||
return rlewExpanded;
|
||||
}
|
||||
|
||||
/// Extracts decompressed VGA data chunks (UI, Fonts, Pictures)
|
||||
static List<Uint8List> _parseVgaRaw(
|
||||
ByteData vgaDict,
|
||||
ByteData vgaHead,
|
||||
ByteData vgaGraph,
|
||||
) {
|
||||
List<Uint8List> vgaChunks = [];
|
||||
|
||||
// 1. Parse the Huffman Dictionary from VGADICT
|
||||
List<_HuffmanNode> dict = [];
|
||||
for (int i = 0; i < 255; i++) {
|
||||
dict.add(
|
||||
_HuffmanNode(
|
||||
vgaDict.getUint16(i * 4, Endian.little),
|
||||
vgaDict.getUint16(i * 4 + 2, Endian.little),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Read VGAHEAD to get the offsets for VGAGRAPH
|
||||
List<int> offsets = [];
|
||||
int numChunks = vgaHead.lengthInBytes ~/ 3;
|
||||
int lastOffset = -1;
|
||||
|
||||
for (int i = 0; i < numChunks; i++) {
|
||||
int offset =
|
||||
vgaHead.getUint8(i * 3) |
|
||||
(vgaHead.getUint8(i * 3 + 1) << 8) |
|
||||
(vgaHead.getUint8(i * 3 + 2) << 16);
|
||||
|
||||
if (offset == 0x00FFFFFF) break;
|
||||
|
||||
// --- SAFETY FIX ---
|
||||
// 1. Offsets cannot point beyond the file length.
|
||||
// 2. Offsets must not go backward (this means we hit the junk padding).
|
||||
if (offset > vgaGraph.lengthInBytes || offset < lastOffset) {
|
||||
break;
|
||||
}
|
||||
|
||||
offsets.add(offset);
|
||||
lastOffset = offset;
|
||||
}
|
||||
|
||||
// 3. Decompress the chunks from VGAGRAPH
|
||||
for (int i = 0; i < offsets.length; i++) {
|
||||
int offset = offsets[i];
|
||||
|
||||
// --- BOUNDARY CHECK ---
|
||||
// If the offset is exactly at the end of the file, or doesn't leave
|
||||
// enough room for a 4-byte length header, it's an empty chunk.
|
||||
if (offset + 4 > vgaGraph.lengthInBytes) {
|
||||
vgaChunks.add(Uint8List(0));
|
||||
continue;
|
||||
}
|
||||
|
||||
// The first 4 bytes of a compressed chunk specify its decompressed size
|
||||
int expandedLength = vgaGraph.getUint32(offset, Endian.little);
|
||||
|
||||
if (expandedLength == 0) {
|
||||
vgaChunks.add(Uint8List(0));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decompress starting immediately after the 4-byte length header.
|
||||
// We pass the exact slice of bytes remaining so we don't read out of bounds.
|
||||
Uint8List expandedData = _expandHuffman(
|
||||
vgaGraph.buffer.asUint8List(
|
||||
vgaGraph.offsetInBytes + offset + 4,
|
||||
vgaGraph.lengthInBytes - (offset + 4),
|
||||
),
|
||||
dict,
|
||||
expandedLength,
|
||||
);
|
||||
|
||||
vgaChunks.add(expandedData);
|
||||
}
|
||||
|
||||
return vgaChunks;
|
||||
}
|
||||
|
||||
// --- ALGORITHM 3: HUFFMAN EXPANSION ---
|
||||
static Uint8List _expandHuffman(
|
||||
Uint8List compressed,
|
||||
List<_HuffmanNode> dict,
|
||||
int expandedLength,
|
||||
) {
|
||||
Uint8List expanded = Uint8List(expandedLength);
|
||||
|
||||
int outIdx = 0;
|
||||
int byteIdx = 0;
|
||||
int bitMask = 1;
|
||||
int currentNode = 254; // id Software's Huffman root is always node 254
|
||||
|
||||
while (outIdx < expandedLength && byteIdx < compressed.length) {
|
||||
// Read the current bit (LSB to MSB)
|
||||
int bit = (compressed[byteIdx] & bitMask) == 0 ? 0 : 1;
|
||||
|
||||
// Advance to the next bit/byte
|
||||
bitMask <<= 1;
|
||||
if (bitMask > 128) {
|
||||
bitMask = 1;
|
||||
byteIdx++;
|
||||
}
|
||||
|
||||
// Traverse the tree
|
||||
int nextVal = bit == 0 ? dict[currentNode].bit0 : dict[currentNode].bit1;
|
||||
|
||||
if (nextVal < 256) {
|
||||
// If the value is < 256, we've hit a leaf node (an actual character byte!)
|
||||
expanded[outIdx++] = nextVal;
|
||||
currentNode =
|
||||
254; // Reset to the root of the tree for the next character
|
||||
} else {
|
||||
// If the value is >= 256, it's a pointer to the next internal node.
|
||||
// Node indexes are offset by 256.
|
||||
currentNode = nextVal - 256;
|
||||
}
|
||||
}
|
||||
|
||||
return expanded;
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper class to parse and store redundant VSWAP header data
|
||||
@@ -278,3 +506,10 @@ class _VswapHeader {
|
||||
(i) => vswap.getUint32(6 + (i * 4), Endian.little),
|
||||
);
|
||||
}
|
||||
|
||||
class _HuffmanNode {
|
||||
final int bit0;
|
||||
final int bit1;
|
||||
|
||||
_HuffmanNode(this.bit0, this.bit1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user