Added checksum and version checking

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 21:46:33 +01:00
parent 460552378a
commit 59fc530a1a
6 changed files with 131 additions and 29 deletions

View File

@@ -0,0 +1,27 @@
enum DataVersion {
/// V1.0 Retail (VSWAP.WL6)
version10Retail('a6d901dfb455dfac96db5e4705837cdb'),
/// v1.1 Retail (VSWAP.WL6)
version11Retail('a80904e0283a921d88d977b56c279b9d'),
/// v1.4 Shareware (VSWAP.WL1)
version14Shareware('6efa079414b817c97db779cecfb081c9'),
/// v1.4 Retail (VSWAP.WL6) - GOG/Steam version
version14Retail('b8ff4997461bafa5ef2a94c11f9de001'),
unknown('unknown'),
;
final String checksum;
const DataVersion(this.checksum);
static DataVersion fromChecksum(String hash) {
return DataVersion.values.firstWhere(
(v) => v.checksum == hash,
orElse: () => DataVersion.unknown,
);
}
}

View File

@@ -1,11 +1,12 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:wolf_3d_data/src/data_version.dart';
import 'package:wolf_3d_data/src/wl_parser.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import '../wl_parser.dart';
/// dart:io implementation for directory discovery.
/// dart:io implementation for directory discovery with version integrity checks.
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
String? directoryPath,
bool recursive = false,
@@ -31,11 +32,20 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
for (final requiredFile in GameFile.values) {
final expectedName = '${requiredFile.baseName}.$ext';
final match = allFiles.where((file) {
var match = allFiles.where((file) {
final fileName = file.uri.pathSegments.last.toUpperCase();
return fileName == expectedName;
}).firstOrNull;
// v1.0 FIX: Search for MAPTEMP if GAMEMAPS is missing
if (match == null && requiredFile == GameFile.gameMaps) {
final altName = 'MAPTEMP.$ext';
match = allFiles.where((file) {
final fileName = file.uri.pathSegments.last.toUpperCase();
return fileName == altName;
}).firstOrNull;
}
if (match != null) {
foundFiles[requiredFile] = match;
}
@@ -43,6 +53,7 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
if (foundFiles.isEmpty) continue;
// Validation for missing files
if (foundFiles.length < GameFile.values.length) {
final missingFiles = GameFile.values
.where((f) => !foundFiles.containsKey(f))
@@ -53,9 +64,27 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
}
try {
// 1. Read VSWAP to determine the precise DataVersion identity
final vswapFile = foundFiles[GameFile.vswap]!;
final vswapBytes = await vswapFile.readAsBytes();
// 2. Generate Checksum and Resolve Identity
final hash = md5.convert(vswapBytes).toString();
final identity = DataVersion.fromChecksum(hash);
print('--- Found ${version.name} ---');
print('MD5 Identity: ${identity.name} ($hash)');
if (identity == DataVersion.version10Retail) {
print(
'Note: Detected v1.0 specific file structure (MAPTEMP support active).',
);
}
// 3. Load the data using the parser
final data = WLParser.load(
version: version,
vswap: await _readFile(foundFiles[GameFile.vswap]!),
dataIdentity: identity,
vswap: vswapBytes.buffer.asByteData(),
mapHead: await _readFile(foundFiles[GameFile.mapHead]!),
gameMaps: await _readFile(foundFiles[GameFile.gameMaps]!),
vgaDict: await _readFile(foundFiles[GameFile.vgaDict]!),

View File

@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' show md5;
import 'package:wolf_3d_data/src/data_version.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
abstract class WLParser {
@@ -21,42 +23,72 @@ 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.
/// Asynchronously discovers the game version and loads all necessary files.
/// Asynchronously discovers the game version and loads all necessary files.
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
// 1. Probe the data source for VSWAP to determine the GameVersion
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
}
break;
} catch (_) {}
}
if (detectedVersion == null || vswap == null) {
throw Exception(
'Could not locate a valid VSWAP file for any game version.',
);
throw Exception('Could not locate a valid VSWAP file.');
}
final ext = detectedVersion.fileExtension;
// 2. Now that we know the version, confidently load the rest of the files
// 2. Determine DataIdentity (Checksum) immediately
final vswapBytes = vswap.buffer.asUint8List(
vswap.offsetInBytes,
vswap.lengthInBytes,
);
final vswapHash = md5.convert(vswapBytes).toString();
final dataIdentity = DataVersion.fromChecksum(vswapHash);
// 3. Load other required files
// Special Case: v1.0 Retail uses MAPTEMP instead of GAMEMAPS
ByteData gameMapsData;
if (dataIdentity == DataVersion.version10Retail) {
try {
gameMapsData = await fileFetcher('MAPTEMP.$ext');
} catch (_) {
// Fallback in case v1.0 files were renamed to standard convention
gameMapsData = await fileFetcher('GAMEMAPS.$ext');
}
} else {
gameMapsData = await fileFetcher('GAMEMAPS.$ext');
}
final rawFiles = {
'MAPHEAD.$ext': await fileFetcher('MAPHEAD.$ext'),
'VGADICT.$ext': await fileFetcher('VGADICT.$ext'),
'VGAHEAD.$ext': await fileFetcher('VGAHEAD.$ext'),
'VGAGRAPH.$ext': await fileFetcher('VGAGRAPH.$ext'),
'AUDIOHED.$ext': await fileFetcher('AUDIOHED.$ext'),
'AUDIOT.$ext': await fileFetcher('AUDIOT.$ext'),
};
// 4. Final call to load with the required dataIdentity
return load(
version: detectedVersion,
dataIdentity: dataIdentity, // Now correctly passed
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'),
audioHed: await fileFetcher('AUDIOHED.$ext'),
audioT: await fileFetcher('AUDIOT.$ext'),
mapHead: rawFiles['MAPHEAD.$ext']!,
gameMaps: gameMapsData,
vgaDict: rawFiles['VGADICT.$ext']!,
vgaHead: rawFiles['VGAHEAD.$ext']!,
vgaGraph: rawFiles['VGAGRAPH.$ext']!,
audioHed: rawFiles['AUDIOHED.$ext']!,
audioT: rawFiles['AUDIOT.$ext']!,
);
}
@@ -73,9 +105,15 @@ abstract class WLParser {
required ByteData vgaGraph,
required ByteData audioHed,
required ByteData audioT,
required DataVersion dataIdentity,
}) {
final isShareware = version == GameVersion.shareware;
// v1.0/1.1 used different HUD strings and had different secret wall bugs
final isLegacy =
dataIdentity == DataVersion.version10Retail ||
dataIdentity == DataVersion.version11Retail;
final audio = parseAudio(audioHed, audioT, version);
return WolfensteinData(

View File

@@ -1,7 +1,7 @@
import 'dart:typed_data';
// --- The Magic Conditional Import ---
// If dart:io is available, use the real scanner. Otherwise, use the stub.
import 'package:crypto/crypto.dart'; // Import for MD5
import 'package:wolf_3d_data/src/data_version.dart'; // Import your enum
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'io/discovery_stub.dart'
@@ -11,8 +11,6 @@ import 'wl_parser.dart';
class WolfensteinLoader {
/// Scans a directory for Wolfenstein 3D data files and loads all available versions.
///
/// NOTE: This will throw an [UnsupportedError] on Web platforms.
static Future<Map<GameVersion, WolfensteinData>> discover({
String? directoryPath,
bool recursive = false,
@@ -59,10 +57,20 @@ class WolfensteinLoader {
);
}
// 2. Pass-through to parser now that we are guaranteed non-null
// 2. Identify the DataVersion via Checksum
final vswapBytes = vswap!.buffer.asUint8List(
vswap.offsetInBytes,
vswap.lengthInBytes,
);
final hash = md5.convert(vswapBytes).toString();
final dataIdentity = DataVersion.fromChecksum(hash);
// 3. Pass-through to parser with the detected identity
return WLParser.load(
version: version,
vswap: vswap!,
// Correctly identifies v1.0/1.1/1.4
dataIdentity: dataIdentity,
vswap: vswap,
mapHead: mapHead!,
gameMaps: gameMaps!,
vgaDict: vgaDict!,

View File

@@ -11,6 +11,7 @@ resolution: workspace
publish_to: none
dependencies:
crypto: ^3.0.7
wolf_3d_data_types:
wolf_3d_entities:

View File

@@ -6,8 +6,7 @@ enum GameFile {
vgaHead('VGAHEAD'),
vgaGraph('VGAGRAPH'),
audioHed('AUDIOHED'),
audioT('AUDIOT')
;
audioT('AUDIOT');
final String baseName;