feat: Refactor MD5 hashing and update save game codec for compatibility with new payload format
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -2,10 +2,11 @@ import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:wolf_3d_dart/src/data/wl_parser.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
import '../md5_hash.dart';
|
||||
|
||||
/// dart:io implementation for directory discovery with version integrity checks.
|
||||
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
||||
String? directoryPath,
|
||||
@@ -73,7 +74,7 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
||||
final vswapBytes = await vswapFile.readAsBytes();
|
||||
|
||||
// 2. Generate Checksum and Resolve Identity
|
||||
final hash = md5.convert(vswapBytes).toString();
|
||||
final hash = md5HexLower(vswapBytes);
|
||||
final identity = DataVersion.fromChecksum(hash);
|
||||
|
||||
log('--- Found ${version.name} ---');
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
const int _mask32 = 0xFFFFFFFF;
|
||||
|
||||
const List<int> _s = <int>[
|
||||
7,
|
||||
12,
|
||||
17,
|
||||
22,
|
||||
7,
|
||||
12,
|
||||
17,
|
||||
22,
|
||||
7,
|
||||
12,
|
||||
17,
|
||||
22,
|
||||
7,
|
||||
12,
|
||||
17,
|
||||
22,
|
||||
5,
|
||||
9,
|
||||
14,
|
||||
20,
|
||||
5,
|
||||
9,
|
||||
14,
|
||||
20,
|
||||
5,
|
||||
9,
|
||||
14,
|
||||
20,
|
||||
5,
|
||||
9,
|
||||
14,
|
||||
20,
|
||||
4,
|
||||
11,
|
||||
16,
|
||||
23,
|
||||
4,
|
||||
11,
|
||||
16,
|
||||
23,
|
||||
4,
|
||||
11,
|
||||
16,
|
||||
23,
|
||||
4,
|
||||
11,
|
||||
16,
|
||||
23,
|
||||
6,
|
||||
10,
|
||||
15,
|
||||
21,
|
||||
6,
|
||||
10,
|
||||
15,
|
||||
21,
|
||||
6,
|
||||
10,
|
||||
15,
|
||||
21,
|
||||
6,
|
||||
10,
|
||||
15,
|
||||
21,
|
||||
];
|
||||
|
||||
const List<int> _k = <int>[
|
||||
0xd76aa478,
|
||||
0xe8c7b756,
|
||||
0x242070db,
|
||||
0xc1bdceee,
|
||||
0xf57c0faf,
|
||||
0x4787c62a,
|
||||
0xa8304613,
|
||||
0xfd469501,
|
||||
0x698098d8,
|
||||
0x8b44f7af,
|
||||
0xffff5bb1,
|
||||
0x895cd7be,
|
||||
0x6b901122,
|
||||
0xfd987193,
|
||||
0xa679438e,
|
||||
0x49b40821,
|
||||
0xf61e2562,
|
||||
0xc040b340,
|
||||
0x265e5a51,
|
||||
0xe9b6c7aa,
|
||||
0xd62f105d,
|
||||
0x02441453,
|
||||
0xd8a1e681,
|
||||
0xe7d3fbc8,
|
||||
0x21e1cde6,
|
||||
0xc33707d6,
|
||||
0xf4d50d87,
|
||||
0x455a14ed,
|
||||
0xa9e3e905,
|
||||
0xfcefa3f8,
|
||||
0x676f02d9,
|
||||
0x8d2a4c8a,
|
||||
0xfffa3942,
|
||||
0x8771f681,
|
||||
0x6d9d6122,
|
||||
0xfde5380c,
|
||||
0xa4beea44,
|
||||
0x4bdecfa9,
|
||||
0xf6bb4b60,
|
||||
0xbebfbc70,
|
||||
0x289b7ec6,
|
||||
0xeaa127fa,
|
||||
0xd4ef3085,
|
||||
0x04881d05,
|
||||
0xd9d4d039,
|
||||
0xe6db99e5,
|
||||
0x1fa27cf8,
|
||||
0xc4ac5665,
|
||||
0xf4292244,
|
||||
0x432aff97,
|
||||
0xab9423a7,
|
||||
0xfc93a039,
|
||||
0x655b59c3,
|
||||
0x8f0ccc92,
|
||||
0xffeff47d,
|
||||
0x85845dd1,
|
||||
0x6fa87e4f,
|
||||
0xfe2ce6e0,
|
||||
0xa3014314,
|
||||
0x4e0811a1,
|
||||
0xf7537e82,
|
||||
0xbd3af235,
|
||||
0x2ad7d2bb,
|
||||
0xeb86d391,
|
||||
];
|
||||
|
||||
String md5HexLower(List<int> input) {
|
||||
final source = Uint8List.fromList(input);
|
||||
final originalLength = source.length;
|
||||
final bitLength = originalLength * 8;
|
||||
|
||||
final paddingLength = (56 - ((originalLength + 1) % 64) + 64) % 64;
|
||||
final totalLength = originalLength + 1 + paddingLength + 8;
|
||||
|
||||
final bytes = Uint8List(totalLength);
|
||||
bytes.setRange(0, originalLength, source);
|
||||
bytes[originalLength] = 0x80;
|
||||
|
||||
for (var index = 0; index < 8; index++) {
|
||||
bytes[totalLength - 8 + index] = (bitLength >> (8 * index)) & 0xFF;
|
||||
}
|
||||
|
||||
var a0 = 0x67452301;
|
||||
var b0 = 0xEFCDAB89;
|
||||
var c0 = 0x98BADCFE;
|
||||
var d0 = 0x10325476;
|
||||
|
||||
for (var chunkOffset = 0; chunkOffset < bytes.length; chunkOffset += 64) {
|
||||
final m = Uint32List(16);
|
||||
|
||||
for (var j = 0; j < 16; j++) {
|
||||
final base = chunkOffset + j * 4;
|
||||
m[j] =
|
||||
bytes[base] |
|
||||
(bytes[base + 1] << 8) |
|
||||
(bytes[base + 2] << 16) |
|
||||
(bytes[base + 3] << 24);
|
||||
}
|
||||
|
||||
var a = a0;
|
||||
var b = b0;
|
||||
var c = c0;
|
||||
var d = d0;
|
||||
|
||||
for (var i = 0; i < 64; i++) {
|
||||
late int f;
|
||||
late int g;
|
||||
|
||||
if (i < 16) {
|
||||
f = ((b & c) | ((~b) & d)) & _mask32;
|
||||
g = i;
|
||||
} else if (i < 32) {
|
||||
f = ((d & b) | ((~d) & c)) & _mask32;
|
||||
g = (5 * i + 1) % 16;
|
||||
} else if (i < 48) {
|
||||
f = (b ^ c ^ d) & _mask32;
|
||||
g = (3 * i + 5) % 16;
|
||||
} else {
|
||||
f = (c ^ (b | (~d))) & _mask32;
|
||||
g = (7 * i) % 16;
|
||||
}
|
||||
|
||||
final temp = d;
|
||||
d = c;
|
||||
c = b;
|
||||
|
||||
final sum = (a + f + _k[i] + m[g]) & _mask32;
|
||||
b = (b + _leftRotate(sum, _s[i])) & _mask32;
|
||||
a = temp;
|
||||
}
|
||||
|
||||
a0 = (a0 + a) & _mask32;
|
||||
b0 = (b0 + b) & _mask32;
|
||||
c0 = (c0 + c) & _mask32;
|
||||
d0 = (d0 + d) & _mask32;
|
||||
}
|
||||
|
||||
final output = StringBuffer();
|
||||
_appendWordHex(output, a0);
|
||||
_appendWordHex(output, b0);
|
||||
_appendWordHex(output, c0);
|
||||
_appendWordHex(output, d0);
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
int _leftRotate(int value, int shift) {
|
||||
return ((value << shift) | ((value & _mask32) >> (32 - shift))) & _mask32;
|
||||
}
|
||||
|
||||
void _appendWordHex(StringBuffer output, int value) {
|
||||
for (var offset = 0; offset < 32; offset += 8) {
|
||||
final byte = (value >> offset) & 0xFF;
|
||||
output.write(byte.toRadixString(16).padLeft(2, '0'));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' show md5;
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
import 'md5_hash.dart';
|
||||
|
||||
/// The primary parser for Wolfenstein 3D data formats.
|
||||
///
|
||||
/// This abstract class serves as the extraction and decompression engine for
|
||||
@@ -45,7 +46,7 @@ abstract class WLParser {
|
||||
vswap.offsetInBytes,
|
||||
vswap.lengthInBytes,
|
||||
);
|
||||
final vswapHash = md5.convert(vswapBytes).toString();
|
||||
final vswapHash = md5HexLower(vswapBytes);
|
||||
final dataIdentity = DataVersion.fromChecksum(vswapHash);
|
||||
|
||||
// 3. Load other required files
|
||||
@@ -112,7 +113,7 @@ abstract class WLParser {
|
||||
vswap.offsetInBytes,
|
||||
vswap.lengthInBytes,
|
||||
);
|
||||
final vswapHash = md5.convert(vswapBytes).toString();
|
||||
final vswapHash = md5HexLower(vswapBytes);
|
||||
final dataIdentity = DataVersion.fromChecksum(vswapHash);
|
||||
|
||||
ByteData gameMapsData;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
import 'io/discovery_stub.dart'
|
||||
if (dart.library.io) 'io/discovery_io.dart'
|
||||
as platform;
|
||||
import 'md5_hash.dart';
|
||||
import 'wl_parser.dart';
|
||||
|
||||
/// The main entry point for loading Wolfenstein 3D data.
|
||||
@@ -76,7 +76,7 @@ class WolfensteinLoader {
|
||||
vswap.offsetInBytes,
|
||||
vswap.lengthInBytes,
|
||||
);
|
||||
final hash = md5.convert(vswapBytes).toString();
|
||||
final hash = md5HexLower(vswapBytes);
|
||||
final dataIdentity = DataVersion.fromChecksum(hash);
|
||||
|
||||
// 3. Pass-through to parser with the detected identity and optional override.
|
||||
|
||||
@@ -626,6 +626,12 @@ class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
|
||||
static const int _weapPistol = 1;
|
||||
static const int _weapMachineGun = 2;
|
||||
static const int _weapChainGun = 3;
|
||||
static const List<int> _extensionMagic = <int>[
|
||||
0x57,
|
||||
0x33,
|
||||
0x45,
|
||||
0x58,
|
||||
]; // W3EX
|
||||
|
||||
final SaveGameCodec _legacyCodec = SaveGameCodec();
|
||||
|
||||
@@ -686,6 +692,20 @@ class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
|
||||
checksum = doChecksum(pushwallState, checksum: checksum);
|
||||
|
||||
writer.writeUint32(checksum);
|
||||
|
||||
final Uint8List extendedPayload = super.encodeSnapshotPayload(
|
||||
file.snapshot,
|
||||
);
|
||||
final int extendedChecksum = doChecksum(extendedPayload);
|
||||
writer.writeBytes(_extensionMagic);
|
||||
writer.writeUint8(file.gameVersion.index);
|
||||
writer.writeUint8(file.slot & 0xFF);
|
||||
writer.writeInt64(file.createdAtMs);
|
||||
writer.writeUtf8(file.dataVersionName);
|
||||
writer.writeUint32(extendedPayload.length);
|
||||
writer.writeBytes(extendedPayload);
|
||||
writer.writeUint32(extendedChecksum);
|
||||
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
@@ -766,23 +786,22 @@ class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
|
||||
);
|
||||
}
|
||||
|
||||
if (reader.remaining != 0) {
|
||||
throw const FormatException(
|
||||
'Unexpected trailing bytes in DOS save file.',
|
||||
);
|
||||
}
|
||||
|
||||
final PlayerSaveState player = _decodeDosPlayer(gamestate, actors);
|
||||
final List<EntitySaveState> entities = _decodeDosEntities(actors);
|
||||
final List<DoorSaveState> doors = _decodeDosDoors(
|
||||
final PlayerSaveState dosPlayer = _decodeDosPlayer(gamestate, actors);
|
||||
final List<EntitySaveState> dosEntities = _decodeDosEntities(actors);
|
||||
final List<DoorSaveState> dosDoors = _decodeDosDoors(
|
||||
doorposition,
|
||||
doorobjlist,
|
||||
);
|
||||
final List<PushwallSaveState> pushwalls = _decodeDosPushwalls(
|
||||
final List<PushwallSaveState> dosPushwalls = _decodeDosPushwalls(
|
||||
pushwallState,
|
||||
);
|
||||
|
||||
final GameSessionSnapshot snapshot = GameSessionSnapshot(
|
||||
int slot = 0;
|
||||
GameVersion gameVersion = GameVersion.retail;
|
||||
String dataVersionName = 'dos-compat';
|
||||
int createdAtMs = 0;
|
||||
|
||||
GameSessionSnapshot snapshot = GameSessionSnapshot(
|
||||
currentGameIndex: 0,
|
||||
currentEpisodeIndex: gamestate.episode.clamp(0, 5),
|
||||
currentLevelIndex: gamestate.mapon.clamp(0, 9),
|
||||
@@ -792,21 +811,44 @@ class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
|
||||
lastAcousticAlertTime: 0,
|
||||
isMapOverlayVisible: false,
|
||||
isMenuOverlayVisible: false,
|
||||
player: player,
|
||||
player: dosPlayer,
|
||||
currentLevel: tilemap,
|
||||
areaGrid: _emptyAreaGrid(),
|
||||
areasByPlayer: areasByPlayer,
|
||||
entities: entities,
|
||||
doors: doors,
|
||||
pushwalls: pushwalls,
|
||||
entities: dosEntities,
|
||||
doors: dosDoors,
|
||||
pushwalls: dosPushwalls,
|
||||
);
|
||||
|
||||
if (reader.remaining > 0) {
|
||||
try {
|
||||
final Uint8List extMagic = reader.readBytes(_extensionMagic.length);
|
||||
if (_listEquals(extMagic, _extensionMagic)) {
|
||||
final int gameVersionIndex = reader.readUint8();
|
||||
if (gameVersionIndex >= 0 &&
|
||||
gameVersionIndex < GameVersion.values.length) {
|
||||
gameVersion = GameVersion.values[gameVersionIndex];
|
||||
}
|
||||
slot = reader.readUint8();
|
||||
createdAtMs = reader.readInt64();
|
||||
dataVersionName = reader.readUtf8();
|
||||
final int extendedLength = reader.readUint32();
|
||||
final Uint8List extendedPayload = reader.readBytes(extendedLength);
|
||||
final int extendedChecksum = reader.readUint32();
|
||||
final int computed = doChecksum(extendedPayload);
|
||||
if (computed == extendedChecksum) {
|
||||
snapshot = super.decodeSnapshotPayload(extendedPayload);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return SaveGameFile(
|
||||
slot: 0,
|
||||
gameVersion: GameVersion.retail,
|
||||
dataVersionName: 'dos-compat',
|
||||
slot: slot,
|
||||
gameVersion: gameVersion,
|
||||
dataVersionName: dataVersionName,
|
||||
description: description,
|
||||
createdAtMs: 0,
|
||||
createdAtMs: createdAtMs,
|
||||
snapshot: snapshot,
|
||||
checksum: storedChecksum,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user