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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
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'
-60
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,
);
}
}
+24 -16
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,
)], )],
), ),
), ),
@@ -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;
@@ -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,
});
}
@@ -0,0 +1,6 @@
import 'dart:typed_data';
class PcmSound {
final Uint8List bytes;
PcmSound(this.bytes);
}
@@ -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]!;
}
} }
+236 -1
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);
}
@@ -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;
+2 -1
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