Added checksum and version checking
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
27
packages/wolf_3d_data/lib/src/data_version.dart
Normal file
27
packages/wolf_3d_data/lib/src/data_version.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
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 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
import '../wl_parser.dart';
|
/// dart:io implementation for directory discovery with version integrity checks.
|
||||||
|
|
||||||
/// dart:io implementation for directory discovery.
|
|
||||||
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
||||||
String? directoryPath,
|
String? directoryPath,
|
||||||
bool recursive = false,
|
bool recursive = false,
|
||||||
@@ -31,11 +32,20 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
for (final requiredFile in GameFile.values) {
|
for (final requiredFile in GameFile.values) {
|
||||||
final expectedName = '${requiredFile.baseName}.$ext';
|
final expectedName = '${requiredFile.baseName}.$ext';
|
||||||
|
|
||||||
final match = allFiles.where((file) {
|
var match = allFiles.where((file) {
|
||||||
final fileName = file.uri.pathSegments.last.toUpperCase();
|
final fileName = file.uri.pathSegments.last.toUpperCase();
|
||||||
return fileName == expectedName;
|
return fileName == expectedName;
|
||||||
}).firstOrNull;
|
}).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) {
|
if (match != null) {
|
||||||
foundFiles[requiredFile] = match;
|
foundFiles[requiredFile] = match;
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,7 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
|
|
||||||
if (foundFiles.isEmpty) continue;
|
if (foundFiles.isEmpty) continue;
|
||||||
|
|
||||||
|
// Validation for missing files
|
||||||
if (foundFiles.length < GameFile.values.length) {
|
if (foundFiles.length < GameFile.values.length) {
|
||||||
final missingFiles = GameFile.values
|
final missingFiles = GameFile.values
|
||||||
.where((f) => !foundFiles.containsKey(f))
|
.where((f) => !foundFiles.containsKey(f))
|
||||||
@@ -53,9 +64,27 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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(
|
final data = WLParser.load(
|
||||||
version: version,
|
version: version,
|
||||||
vswap: await _readFile(foundFiles[GameFile.vswap]!),
|
dataIdentity: identity,
|
||||||
|
vswap: vswapBytes.buffer.asByteData(),
|
||||||
mapHead: await _readFile(foundFiles[GameFile.mapHead]!),
|
mapHead: await _readFile(foundFiles[GameFile.mapHead]!),
|
||||||
gameMaps: await _readFile(foundFiles[GameFile.gameMaps]!),
|
gameMaps: await _readFile(foundFiles[GameFile.gameMaps]!),
|
||||||
vgaDict: await _readFile(foundFiles[GameFile.vgaDict]!),
|
vgaDict: await _readFile(foundFiles[GameFile.vgaDict]!),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
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';
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
abstract class WLParser {
|
abstract class WLParser {
|
||||||
@@ -21,42 +23,72 @@ abstract class WLParser {
|
|||||||
/// Asynchronously discovers the game version and loads all necessary files.
|
/// Asynchronously discovers the game version and loads all necessary files.
|
||||||
/// Provide a [fileFetcher] callback (e.g., Flutter's rootBundle.load) that
|
/// Provide a [fileFetcher] callback (e.g., Flutter's rootBundle.load) that
|
||||||
/// takes a filename and returns its ByteData.
|
/// 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(
|
static Future<WolfensteinData> loadAsync(
|
||||||
Future<ByteData> Function(String filename) fileFetcher,
|
Future<ByteData> Function(String filename) fileFetcher,
|
||||||
) async {
|
) async {
|
||||||
GameVersion? detectedVersion;
|
GameVersion? detectedVersion;
|
||||||
ByteData? vswap;
|
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) {
|
for (final version in GameVersion.values) {
|
||||||
try {
|
try {
|
||||||
vswap = await fileFetcher('VSWAP.${version.fileExtension}');
|
vswap = await fileFetcher('VSWAP.${version.fileExtension}');
|
||||||
detectedVersion = version;
|
detectedVersion = version;
|
||||||
break; // We found the version!
|
break;
|
||||||
} catch (_) {
|
} catch (_) {}
|
||||||
// File wasn't found, try the next version extension
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detectedVersion == null || vswap == null) {
|
if (detectedVersion == null || vswap == null) {
|
||||||
throw Exception(
|
throw Exception('Could not locate a valid VSWAP file.');
|
||||||
'Could not locate a valid VSWAP file for any game version.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final ext = detectedVersion.fileExtension;
|
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(
|
return load(
|
||||||
version: detectedVersion,
|
version: detectedVersion,
|
||||||
|
dataIdentity: dataIdentity, // Now correctly passed
|
||||||
vswap: vswap,
|
vswap: vswap,
|
||||||
mapHead: await fileFetcher('MAPHEAD.$ext'),
|
mapHead: rawFiles['MAPHEAD.$ext']!,
|
||||||
gameMaps: await fileFetcher('GAMEMAPS.$ext'),
|
gameMaps: gameMapsData,
|
||||||
vgaDict: await fileFetcher('VGADICT.$ext'),
|
vgaDict: rawFiles['VGADICT.$ext']!,
|
||||||
vgaHead: await fileFetcher('VGAHEAD.$ext'),
|
vgaHead: rawFiles['VGAHEAD.$ext']!,
|
||||||
vgaGraph: await fileFetcher('VGAGRAPH.$ext'),
|
vgaGraph: rawFiles['VGAGRAPH.$ext']!,
|
||||||
audioHed: await fileFetcher('AUDIOHED.$ext'),
|
audioHed: rawFiles['AUDIOHED.$ext']!,
|
||||||
audioT: await fileFetcher('AUDIOT.$ext'),
|
audioT: rawFiles['AUDIOT.$ext']!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +105,15 @@ abstract class WLParser {
|
|||||||
required ByteData vgaGraph,
|
required ByteData vgaGraph,
|
||||||
required ByteData audioHed,
|
required ByteData audioHed,
|
||||||
required ByteData audioT,
|
required ByteData audioT,
|
||||||
|
required DataVersion dataIdentity,
|
||||||
}) {
|
}) {
|
||||||
final isShareware = version == GameVersion.shareware;
|
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);
|
final audio = parseAudio(audioHed, audioT, version);
|
||||||
|
|
||||||
return WolfensteinData(
|
return WolfensteinData(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
// --- The Magic Conditional Import ---
|
import 'package:crypto/crypto.dart'; // Import for MD5
|
||||||
// If dart:io is available, use the real scanner. Otherwise, use the stub.
|
import 'package:wolf_3d_data/src/data_version.dart'; // Import your enum
|
||||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
import 'io/discovery_stub.dart'
|
import 'io/discovery_stub.dart'
|
||||||
@@ -11,8 +11,6 @@ import 'wl_parser.dart';
|
|||||||
|
|
||||||
class WolfensteinLoader {
|
class WolfensteinLoader {
|
||||||
/// Scans a directory for Wolfenstein 3D data files and loads all available versions.
|
/// 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({
|
static Future<Map<GameVersion, WolfensteinData>> discover({
|
||||||
String? directoryPath,
|
String? directoryPath,
|
||||||
bool recursive = false,
|
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(
|
return WLParser.load(
|
||||||
version: version,
|
version: version,
|
||||||
vswap: vswap!,
|
// Correctly identifies v1.0/1.1/1.4
|
||||||
|
dataIdentity: dataIdentity,
|
||||||
|
vswap: vswap,
|
||||||
mapHead: mapHead!,
|
mapHead: mapHead!,
|
||||||
gameMaps: gameMaps!,
|
gameMaps: gameMaps!,
|
||||||
vgaDict: vgaDict!,
|
vgaDict: vgaDict!,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ resolution: workspace
|
|||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
crypto: ^3.0.7
|
||||||
wolf_3d_data_types:
|
wolf_3d_data_types:
|
||||||
wolf_3d_entities:
|
wolf_3d_entities:
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ enum GameFile {
|
|||||||
vgaHead('VGAHEAD'),
|
vgaHead('VGAHEAD'),
|
||||||
vgaGraph('VGAGRAPH'),
|
vgaGraph('VGAGRAPH'),
|
||||||
audioHed('AUDIOHED'),
|
audioHed('AUDIOHED'),
|
||||||
audioT('AUDIOT')
|
audioT('AUDIOT');
|
||||||
;
|
|
||||||
|
|
||||||
final String baseName;
|
final String baseName;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user