Added WL1 map parsing

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-13 16:04:14 +01:00
parent 8f67d8de44
commit 8ecc8e2fd4
11 changed files with 612 additions and 337 deletions

View File

@@ -0,0 +1,15 @@
import 'package:wolf_dart/classes/matrix.dart';
class WolfLevel {
final String name;
final int width; // Always 64 in standard Wolf3D
final int height; // Always 64
final Matrix<int> wallGrid;
WolfLevel({
required this.name,
required this.width,
required this.height,
required this.wallGrid,
});
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/services.dart';
import 'package:wolf_dart/features/map/wolf_level.dart';
import 'package:wolf_dart/features/map/wolf_map_parser.dart';
class WolfMap {
/// The fully parsed and decompressed levels from the game files.
final List<WolfLevel> levels;
// A private constructor so we can only instantiate this from the async loader
WolfMap._(this.levels);
/// Asynchronously loads the map files and parses them into a new WolfMap instance.
static Future<WolfMap> load() async {
// 1. Load the binary data
final mapHead = await rootBundle.load("assets/MAPHEAD.WL1");
final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL1");
// 2. Parse the data using the parser we just built
final parsedLevels = WolfMapParser.parseMaps(mapHead, gameMaps);
// 3. Return the populated instance!
return WolfMap._(parsedLevels);
}
}

View File

@@ -0,0 +1,164 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:wolf_dart/classes/matrix.dart';
import 'package:wolf_dart/features/map/wolf_level.dart';
abstract class WolfMapParser {
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
static List<WolfLevel> parseMaps(ByteData mapHead, ByteData gameMaps) {
List<WolfLevel> levels = [];
// 1. READ MAPHEAD
// The very first 16-bit word in MAPHEAD is the RLEW tag (usually 0xABCD)
// We will need this later for decompression!
int rlewTag = mapHead.getUint16(0, Endian.little);
// MAPHEAD contains up to 100 levels.
// Starting at byte 2, there are 100 32-bit integers representing
// the byte offset of each level's header inside GAMEMAPS.
for (int i = 0; i < 100; i++) {
int mapOffset = mapHead.getUint32(2 + (i * 4), Endian.little);
// An offset of 0 means the level doesn't exist (end of the list)
if (mapOffset == 0) continue;
// 2. READ GAMEMAPS HEADER
// Jump to the offset in GAMEMAPS to read the 38-byte Level Header
// Pointers to the compressed data for the 3 planes (Walls, Objects, Extra)
int plane0Offset = gameMaps.getUint32(mapOffset + 0, Endian.little);
int plane1Offset = gameMaps.getUint32(mapOffset + 4, Endian.little);
// Plane 2 (offset + 8) is usually unused in standard Wolf3D
// Lengths of the compressed data for each plane
int plane0Length = gameMaps.getUint16(mapOffset + 12, Endian.little);
int plane1Length = gameMaps.getUint16(mapOffset + 14, Endian.little);
// Dimensions (Always 64x64, but we read it anyway for accuracy)
int width = gameMaps.getUint16(mapOffset + 18, Endian.little);
int height = gameMaps.getUint16(mapOffset + 20, Endian.little);
// Map Name (16 bytes of ASCII text)
List<int> nameBytes = [];
for (int n = 0; n < 16; n++) {
int charCode = gameMaps.getUint8(mapOffset + 22 + n);
if (charCode == 0) break; // Null terminator
nameBytes.add(charCode);
}
String name = ascii.decode(nameBytes);
// 3. EXTRACT AND DECOMPRESS THE WALL DATA
final compressedWallData = gameMaps.buffer.asUint8List(
plane0Offset,
plane0Length,
);
// 1st Pass: Un-Carmack
Uint16List carmackExpanded = _expandCarmack(compressedWallData);
// 2nd Pass: Un-RLEW
List<int> flatGrid = _expandRlew(carmackExpanded, rlewTag);
// Convert the flat List<int> (4096 items) into a Matrix<int> (64x64 grid)
Matrix<int> wallGrid = [];
for (int y = 0; y < height; y++) {
List<int> row = [];
for (int x = 0; x < width; x++) {
// Note: In original Wolf3D, empty space is usually ID 90 or 106,
// but we can map them down to 0 for your raycaster logic later.
row.add(flatGrid[y * width + x]);
}
wallGrid.add(row);
}
levels.add(
WolfLevel(
name: name,
width: width,
height: height,
wallGrid: wallGrid, // Pass the fully decompressed matrix!
),
);
}
return levels;
}
// --- ALGORITHM 1: CARMACK EXPANSION ---
static Uint16List _expandCarmack(Uint8List compressed) {
ByteData data = ByteData.sublistView(compressed);
// The first 16-bit word is the total length of the expanded data in BYTES.
int expandedLengthBytes = data.getUint16(0, Endian.little);
int expandedLengthWords = expandedLengthBytes ~/ 2;
Uint16List expanded = Uint16List(expandedLengthWords);
int inIdx = 2; // Skip the length word we just read
int outIdx = 0;
while (outIdx < expandedLengthWords && inIdx < compressed.length) {
int word = data.getUint16(inIdx, Endian.little);
inIdx += 2;
int highByte = word >> 8;
int lowByte = word & 0xFF;
// 0xA7 and 0xA8 are the Carmack Pointer Tags
if (highByte == 0xA7 || highByte == 0xA8) {
if (lowByte == 0) {
// Exception Rule: If the length (lowByte) is 0, it's not a pointer.
// It's literally just the tag byte followed by another byte.
int nextByte = data.getUint8(inIdx++);
expanded[outIdx++] = (nextByte << 8) | highByte;
} else if (highByte == 0xA7) {
// 0xA7 = Near Pointer (look back a few spaces)
int offset = data.getUint8(inIdx++);
int copyFrom = outIdx - offset;
for (int i = 0; i < lowByte; i++) {
expanded[outIdx++] = expanded[copyFrom++];
}
} else if (highByte == 0xA8) {
// 0xA8 = Far Pointer (absolute offset from the very beginning)
int offset = data.getUint16(inIdx, Endian.little);
inIdx += 2;
for (int i = 0; i < lowByte; i++) {
expanded[outIdx++] = expanded[offset++];
}
}
} else {
// Normal, uncompressed word
expanded[outIdx++] = word;
}
}
return expanded;
}
// --- ALGORITHM 2: RLEW EXPANSION ---
static List<int> _expandRlew(Uint16List carmackExpanded, int rlewTag) {
// The first word is the expanded length in BYTES
int expandedLengthBytes = carmackExpanded[0];
int expandedLengthWords = expandedLengthBytes ~/ 2;
List<int> rlewExpanded = List<int>.filled(expandedLengthWords, 0);
int inIdx = 1; // Skip the length word
int outIdx = 0;
while (outIdx < expandedLengthWords && inIdx < carmackExpanded.length) {
int word = carmackExpanded[inIdx++];
if (word == rlewTag) {
// We found an RLEW tag!
// The next word is the count, the word after that is the value.
int count = carmackExpanded[inIdx++];
int value = carmackExpanded[inIdx++];
for (int i = 0; i < count; i++) {
rlewExpanded[outIdx++] = value;
}
} else {
// Normal word
rlewExpanded[outIdx++] = word;
}
}
return rlewExpanded;
}
}