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 parseMaps(ByteData mapHead, ByteData gameMaps) { List 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 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 flatGrid = _expandRlew(carmackExpanded, rlewTag); // Convert the flat List (4096 items) into a Matrix (64x64 grid) Matrix wallGrid = []; for (int y = 0; y < height; y++) { List 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 _expandRlew(Uint16List carmackExpanded, int rlewTag) { // The first word is the expanded length in BYTES int expandedLengthBytes = carmackExpanded[0]; int expandedLengthWords = expandedLengthBytes ~/ 2; List rlewExpanded = List.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; } }