Moved data loading to parser. Added remaining shareware data.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 00:27:04 +01:00
parent fc5e07ea10
commit f6656ea9dc
30 changed files with 299 additions and 155 deletions

Binary file not shown.

BIN
assets/shareware/AUDIOT.WL1 Normal file

Binary file not shown.

BIN
assets/shareware/CONFIG.WL1 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:wolf_3d_data/wolf_3d_data.dart'; import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_dart/features/map/door.dart'; import 'package:wolf_dart/features/entities/door.dart';
class DoorManager { class DoorManager {
// Key is '$x,$y' // Key is '$x,$y'

View File

@@ -1,60 +0,0 @@
import 'package:flutter/services.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart';
class WolfMap {
/// The fully parsed and decompressed levels from the game files.
final List<WolfLevel> levels;
final List<Sprite> textures;
final List<Sprite> sprites;
// A private constructor so we can only instantiate this from the async loader
WolfMap._(
this.levels,
this.textures,
this.sprites,
);
/// Asynchronously loads the map files and parses them into a new WolfMap instance.
static Future<WolfMap> loadShareware() async {
// 1. Load the binary data
final mapHead = await rootBundle.load("assets/MAPHEAD.WL1");
final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL1");
final vswap = await rootBundle.load("assets/VSWAP.WL1");
// 2. Parse the data using the parser we just built
final parsedLevels = WLParser.parseMaps(
mapHead,
gameMaps,
isShareware: true,
);
final parsedTextures = WLParser.parseWalls(vswap);
final parsedSprites = WLParser.parseSprites(vswap);
// 3. Return the populated instance!
return WolfMap._(
parsedLevels,
parsedTextures,
parsedSprites,
);
}
/// Asynchronously loads the map files and parses them into a new WolfMap instance.
static Future<WolfMap> loadRetail() async {
// 1. Load the binary data
final mapHead = await rootBundle.load("assets/MAPHEAD.WL6");
final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL6");
final vswap = await rootBundle.load("assets/VSWAP.WL6");
// 2. Parse the data using the parser we just built
final parsedLevels = WLParser.parseMaps(mapHead, gameMaps);
final parsedTextures = WLParser.parseWalls(vswap);
final parsedSprites = WLParser.parseSprites(vswap);
// 3. Return the populated instance!
return WolfMap._(
parsedLevels,
parsedTextures,
parsedSprites,
);
}
}

View File

@@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart'; import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_dart/classes/coordinate_2d.dart'; import 'package:wolf_dart/classes/coordinate_2d.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart';
@@ -13,7 +14,6 @@ import 'package:wolf_dart/features/entities/entity_registry.dart';
import 'package:wolf_dart/features/entities/map_objects.dart'; import 'package:wolf_dart/features/entities/map_objects.dart';
import 'package:wolf_dart/features/entities/pushwall_manager.dart'; import 'package:wolf_dart/features/entities/pushwall_manager.dart';
import 'package:wolf_dart/features/input/input_manager.dart'; import 'package:wolf_dart/features/input/input_manager.dart';
import 'package:wolf_dart/features/map/wolf_map.dart';
import 'package:wolf_dart/features/player/player.dart'; import 'package:wolf_dart/features/player/player.dart';
import 'package:wolf_dart/features/renderer/raycast_painter.dart'; import 'package:wolf_dart/features/renderer/raycast_painter.dart';
import 'package:wolf_dart/features/renderer/weapon_painter.dart'; import 'package:wolf_dart/features/renderer/weapon_painter.dart';
@@ -44,8 +44,9 @@ class _WolfRendererState extends State<WolfRenderer>
late Ticker _gameLoop; late Ticker _gameLoop;
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
late WolfMap gameMap; late WolfensteinData gameData;
late Level currentLevel; late Level currentLevel;
late WolfLevel activeLevel;
final double fov = math.pi / 3; final double fov = math.pi / 3;
@@ -64,15 +65,22 @@ class _WolfRendererState extends State<WolfRenderer>
} }
Future<void> _initGame(bool isShareware) async { Future<void> _initGame(bool isShareware) async {
gameMap = isShareware gameData = await WLParser.loadAsync(
? await WolfMap.loadShareware() (filename) => rootBundle.load('assets/retail/$filename'),
: await WolfMap.loadRetail(); );
currentLevel = gameMap.levels[0].wallGrid;
print('Detected Game Version: ${gameData.version.name}');
print('Loaded ${gameData.levels.length} levels!');
print('Loaded ${gameData.vgaImages.length} images!');
// Get the first level out of the data class
activeLevel = gameData.levels.first;
// Set up your grids directly from the active level
currentLevel = activeLevel.wallGrid;
final Level objectLevel = activeLevel.objectGrid;
doorManager.initDoors(currentLevel); doorManager.initDoors(currentLevel);
final Level objectLevel = gameMap.levels[0].objectGrid;
pushwallManager.initPushwalls(currentLevel, objectLevel); pushwallManager.initPushwalls(currentLevel, objectLevel);
for (int y = 0; y < 64; y++) { for (int y = 0; y < 64; y++) {
@@ -108,7 +116,7 @@ class _WolfRendererState extends State<WolfRenderer>
x + 0.5, x + 0.5,
y + 0.5, y + 0.5,
widget.difficulty, widget.difficulty,
gameMap.sprites.length, gameData.sprites.length,
isSharewareMode: isShareware, isSharewareMode: isShareware,
); );
@@ -354,7 +362,7 @@ class _WolfRendererState extends State<WolfRenderer>
entity.x, entity.x,
entity.y, entity.y,
widget.difficulty, widget.difficulty,
gameMap.sprites.length, gameData.sprites.length,
); );
if (droppedAmmo != null) { if (droppedAmmo != null) {
@@ -403,7 +411,7 @@ class _WolfRendererState extends State<WolfRenderer>
} }
if (widget.showSpriteGallery) { if (widget.showSpriteGallery) {
return SpriteGallery(sprites: gameMap.sprites); return SpriteGallery(sprites: gameData.sprites);
} }
return Scaffold( return Scaffold(
@@ -429,12 +437,12 @@ class _WolfRendererState extends State<WolfRenderer>
), ),
painter: RaycasterPainter( painter: RaycasterPainter(
map: currentLevel, map: currentLevel,
textures: gameMap.textures, textures: gameData.walls,
player: player, player: player,
fov: fov, fov: fov,
doorOffsets: doorManager.getOffsetsForRenderer(), doorOffsets: doorManager.getOffsetsForRenderer(),
entities: entities, entities: entities,
sprites: gameMap.sprites, sprites: gameData.sprites,
activePushwall: pushwallManager.activePushwall, activePushwall: pushwallManager.activePushwall,
), ),
), ),
@@ -451,9 +459,9 @@ class _WolfRendererState extends State<WolfRenderer>
child: CustomPaint( child: CustomPaint(
painter: WeaponPainter( painter: WeaponPainter(
sprite: sprite:
gameMap.sprites[player.currentWeapon gameData.sprites[player.currentWeapon
.getCurrentSpriteIndex( .getCurrentSpriteIndex(
gameMap.sprites.length, gameData.sprites.length,
)], )],
), ),
), ),

View File

@@ -1,8 +1,8 @@
enum GameVersion { enum GameVersion {
shareware("WL1"), shareware("WL1"),
retail("WL6"), retail("WL6"),
spearOfDestinyDemo("SDM"),
spearOfDestiny("SOD"), spearOfDestiny("SOD"),
spearOfDestinyDemo("SDM"),
; ;
final String fileExtension; final String fileExtension;

View File

@@ -0,0 +1,13 @@
import 'dart:typed_data';
class VgaImage {
final int width;
final int height;
final Uint8List pixels; // 8-bit paletted pixel data
VgaImage({
required this.width,
required this.height,
required this.pixels,
});
}

View File

@@ -0,0 +1,6 @@
import 'dart:typed_data';
class PcmSound {
final Uint8List bytes;
PcmSound(this.bytes);
}

View File

@@ -1,81 +1,19 @@
import 'dart:typed_data'; import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_3d_data/src/wl_parser.dart';
import 'game_version.dart';
import 'sprite.dart';
import 'wolf_level.dart';
class WolfensteinData { class WolfensteinData {
final GameVersion version; final GameVersion version;
final List<Sprite> walls;
final List<Sprite> sprites;
final List<PcmSound> sounds;
final List<WolfLevel> levels;
final List<VgaImage> vgaImages;
// Raw file data references const WolfensteinData({
final ByteData _vswap;
final ByteData _mapHead;
final ByteData _gameMaps;
// Backing fields for lazy loading
List<Sprite>? _walls;
List<Sprite>? _sprites;
List<WolfLevel>? _levels;
WolfensteinData._({
required this.version, required this.version,
required ByteData vswap, required this.walls,
required ByteData mapHead, required this.sprites,
required ByteData gameMaps, required this.sounds,
}) : _vswap = vswap, required this.levels,
_mapHead = mapHead, required this.vgaImages,
_gameMaps = gameMaps; });
/// Initializes the data from a map of filenames to their byte contents.
/// Automatically detects the game version from the file extensions.
factory WolfensteinData.fromFiles(Map<String, ByteData> files) {
if (files.isEmpty) throw ArgumentError('File map cannot be empty');
// 1. Detect Game Version from the first file's extension
final sampleFilename = files.keys.first.toUpperCase();
final extension = sampleFilename.split('.').last;
final version = GameVersion.values.firstWhere(
(v) => v.fileExtension == extension,
orElse: () =>
throw FormatException('Unsupported file extension: $extension'),
);
// 2. Extract the required files using the detected extension
final vswap = _getFile(files, 'VSWAP.$extension');
final mapHead = _getFile(files, 'MAPHEAD.$extension');
final gameMaps = _getFile(files, 'GAMEMAPS.$extension');
return WolfensteinData._(
version: version,
vswap: vswap,
mapHead: mapHead,
gameMaps: gameMaps,
);
}
// --- Lazy Getters ---
List<Sprite> get walls => _walls ??= WLParser.parseWalls(_vswap);
List<Sprite> get sprites => _sprites ??= WLParser.parseSprites(_vswap);
List<WolfLevel> get levels => _levels ??= WLParser.parseMaps(
_mapHead,
_gameMaps,
isShareware: version == GameVersion.shareware,
);
// --- Helpers ---
static ByteData _getFile(Map<String, ByteData> files, String name) {
// Case-insensitive lookup
final key = files.keys.firstWhere(
(k) => k.toUpperCase() == name.toUpperCase(),
orElse: () => throw FormatException('Missing required file: $name'),
);
return files[key]!;
}
} }

View File

@@ -1,11 +1,79 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; 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/wolf_level.dart';
import 'package:wolf_3d_data/src/classes/wolfenstein_data.dart';
import 'classes/sprite.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 /// Extracts the 64x64 wall textures from VSWAP.WL1
static List<Sprite> parseWalls(ByteData vswap) { static List<Sprite> parseWalls(ByteData vswap) {
final header = _VswapHeader(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. /// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
static List<WolfLevel> parseMaps( static List<WolfLevel> parseMaps(
ByteData mapHead, ByteData mapHead,
@@ -260,6 +366,128 @@ class WLParser {
} }
return rlewExpanded; 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 /// Helper class to parse and store redundant VSWAP header data
@@ -278,3 +506,10 @@ class _VswapHeader {
(i) => vswap.getUint32(6 + (i * 4), Endian.little), (i) => vswap.getUint32(6 + (i * 4), Endian.little),
); );
} }
class _HuffmanNode {
final int bit0;
final int bit1;
_HuffmanNode(this.bit0, this.bit1);
}

View File

@@ -4,6 +4,9 @@
library; library;
export 'src/classes/game_version.dart' show GameVersion; 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/sprite.dart' hide Matrix; export 'src/classes/sprite.dart' hide Matrix;
export 'src/classes/wolf_level.dart' show WolfLevel; export 'src/classes/wolf_level.dart' show WolfLevel;
export 'src/classes/wolfenstein_data.dart' show WolfensteinData;
export 'src/wl_parser.dart' show WLParser; export 'src/wl_parser.dart' show WLParser;

View File

@@ -19,7 +19,8 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/ - assets/retail/
- assets/shareware/
workspace: workspace:
- packages/wolf_3d_data - packages/wolf_3d_data