Moved data loading to parser. Added remaining shareware data.
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
BIN
assets/shareware/AUDIOHED.WL1
Normal file
BIN
assets/shareware/AUDIOHED.WL1
Normal file
Binary file not shown.
BIN
assets/shareware/AUDIOT.WL1
Normal file
BIN
assets/shareware/AUDIOT.WL1
Normal file
Binary file not shown.
BIN
assets/shareware/CONFIG.WL1
Normal file
BIN
assets/shareware/CONFIG.WL1
Normal file
Binary file not shown.
BIN
assets/shareware/VGADICT.WL1
Normal file
BIN
assets/shareware/VGADICT.WL1
Normal file
Binary file not shown.
BIN
assets/shareware/VGAGRAPH.WL1
Normal file
BIN
assets/shareware/VGAGRAPH.WL1
Normal file
Binary file not shown.
BIN
assets/shareware/VGAHEAD.WL1
Normal file
BIN
assets/shareware/VGAHEAD.WL1
Normal file
Binary file not shown.
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
13
packages/wolf_3d_data/lib/src/classes/image.dart
Normal file
13
packages/wolf_3d_data/lib/src/classes/image.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
6
packages/wolf_3d_data/lib/src/classes/sound.dart
Normal file
6
packages/wolf_3d_data/lib/src/classes/sound.dart
Normal file
@@ -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]!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user