diff --git a/packages/wolf_3d_data/lib/src/data_version.dart b/packages/wolf_3d_data/lib/src/data_version.dart new file mode 100644 index 0000000..90941b5 --- /dev/null +++ b/packages/wolf_3d_data/lib/src/data_version.dart @@ -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, + ); + } +} diff --git a/packages/wolf_3d_data/lib/src/io/discovery_io.dart b/packages/wolf_3d_data/lib/src/io/discovery_io.dart index 1dd6a98..c8cd9f4 100644 --- a/packages/wolf_3d_data/lib/src/io/discovery_io.dart +++ b/packages/wolf_3d_data/lib/src/io/discovery_io.dart @@ -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> discoverInDirectory({ String? directoryPath, bool recursive = false, @@ -31,11 +32,20 @@ Future> 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> 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> 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]!), diff --git a/packages/wolf_3d_data/lib/src/wl_parser.dart b/packages/wolf_3d_data/lib/src/wl_parser.dart index bea9f07..4ace6cb 100644 --- a/packages/wolf_3d_data/lib/src/wl_parser.dart +++ b/packages/wolf_3d_data/lib/src/wl_parser.dart @@ -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 loadAsync( Future 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( diff --git a/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart index 6c4ba54..b6e4c92 100644 --- a/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart +++ b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart @@ -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> 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!, diff --git a/packages/wolf_3d_data/pubspec.yaml b/packages/wolf_3d_data/pubspec.yaml index b17824f..ab12fad 100644 --- a/packages/wolf_3d_data/pubspec.yaml +++ b/packages/wolf_3d_data/pubspec.yaml @@ -11,6 +11,7 @@ resolution: workspace publish_to: none dependencies: + crypto: ^3.0.7 wolf_3d_data_types: wolf_3d_entities: diff --git a/packages/wolf_3d_data_types/lib/src/game_file.dart b/packages/wolf_3d_data_types/lib/src/game_file.dart index 28be8f0..be593f4 100644 --- a/packages/wolf_3d_data_types/lib/src/game_file.dart +++ b/packages/wolf_3d_data_types/lib/src/game_file.dart @@ -6,8 +6,7 @@ enum GameFile { vgaHead('VGAHEAD'), vgaGraph('VGAGRAPH'), audioHed('AUDIOHED'), - audioT('AUDIOT') - ; + audioT('AUDIOT'); final String baseName;