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:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:wolf_3d_dart/src/data/wl_parser.dart';
|
import 'package:wolf_3d_dart/src/data/wl_parser.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.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.
|
/// dart:io implementation for directory discovery with version integrity checks.
|
||||||
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
||||||
String? directoryPath,
|
String? directoryPath,
|
||||||
@@ -73,7 +74,7 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
final vswapBytes = await vswapFile.readAsBytes();
|
final vswapBytes = await vswapFile.readAsBytes();
|
||||||
|
|
||||||
// 2. Generate Checksum and Resolve Identity
|
// 2. Generate Checksum and Resolve Identity
|
||||||
final hash = md5.convert(vswapBytes).toString();
|
final hash = md5HexLower(vswapBytes);
|
||||||
final identity = DataVersion.fromChecksum(hash);
|
final identity = DataVersion.fromChecksum(hash);
|
||||||
|
|
||||||
log('--- Found ${version.name} ---');
|
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:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart' show md5;
|
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
import 'md5_hash.dart';
|
||||||
|
|
||||||
/// The primary parser for Wolfenstein 3D data formats.
|
/// The primary parser for Wolfenstein 3D data formats.
|
||||||
///
|
///
|
||||||
/// This abstract class serves as the extraction and decompression engine for
|
/// This abstract class serves as the extraction and decompression engine for
|
||||||
@@ -45,7 +46,7 @@ abstract class WLParser {
|
|||||||
vswap.offsetInBytes,
|
vswap.offsetInBytes,
|
||||||
vswap.lengthInBytes,
|
vswap.lengthInBytes,
|
||||||
);
|
);
|
||||||
final vswapHash = md5.convert(vswapBytes).toString();
|
final vswapHash = md5HexLower(vswapBytes);
|
||||||
final dataIdentity = DataVersion.fromChecksum(vswapHash);
|
final dataIdentity = DataVersion.fromChecksum(vswapHash);
|
||||||
|
|
||||||
// 3. Load other required files
|
// 3. Load other required files
|
||||||
@@ -112,7 +113,7 @@ abstract class WLParser {
|
|||||||
vswap.offsetInBytes,
|
vswap.offsetInBytes,
|
||||||
vswap.lengthInBytes,
|
vswap.lengthInBytes,
|
||||||
);
|
);
|
||||||
final vswapHash = md5.convert(vswapBytes).toString();
|
final vswapHash = md5HexLower(vswapBytes);
|
||||||
final dataIdentity = DataVersion.fromChecksum(vswapHash);
|
final dataIdentity = DataVersion.fromChecksum(vswapHash);
|
||||||
|
|
||||||
ByteData gameMapsData;
|
ByteData gameMapsData;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
import 'io/discovery_stub.dart'
|
import 'io/discovery_stub.dart'
|
||||||
if (dart.library.io) 'io/discovery_io.dart'
|
if (dart.library.io) 'io/discovery_io.dart'
|
||||||
as platform;
|
as platform;
|
||||||
|
import 'md5_hash.dart';
|
||||||
import 'wl_parser.dart';
|
import 'wl_parser.dart';
|
||||||
|
|
||||||
/// The main entry point for loading Wolfenstein 3D data.
|
/// The main entry point for loading Wolfenstein 3D data.
|
||||||
@@ -76,7 +76,7 @@ class WolfensteinLoader {
|
|||||||
vswap.offsetInBytes,
|
vswap.offsetInBytes,
|
||||||
vswap.lengthInBytes,
|
vswap.lengthInBytes,
|
||||||
);
|
);
|
||||||
final hash = md5.convert(vswapBytes).toString();
|
final hash = md5HexLower(vswapBytes);
|
||||||
final dataIdentity = DataVersion.fromChecksum(hash);
|
final dataIdentity = DataVersion.fromChecksum(hash);
|
||||||
|
|
||||||
// 3. Pass-through to parser with the detected identity and optional override.
|
// 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 _weapPistol = 1;
|
||||||
static const int _weapMachineGun = 2;
|
static const int _weapMachineGun = 2;
|
||||||
static const int _weapChainGun = 3;
|
static const int _weapChainGun = 3;
|
||||||
|
static const List<int> _extensionMagic = <int>[
|
||||||
|
0x57,
|
||||||
|
0x33,
|
||||||
|
0x45,
|
||||||
|
0x58,
|
||||||
|
]; // W3EX
|
||||||
|
|
||||||
final SaveGameCodec _legacyCodec = SaveGameCodec();
|
final SaveGameCodec _legacyCodec = SaveGameCodec();
|
||||||
|
|
||||||
@@ -686,6 +692,20 @@ class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
|
|||||||
checksum = doChecksum(pushwallState, checksum: checksum);
|
checksum = doChecksum(pushwallState, checksum: checksum);
|
||||||
|
|
||||||
writer.writeUint32(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();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -766,23 +786,22 @@ class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader.remaining != 0) {
|
final PlayerSaveState dosPlayer = _decodeDosPlayer(gamestate, actors);
|
||||||
throw const FormatException(
|
final List<EntitySaveState> dosEntities = _decodeDosEntities(actors);
|
||||||
'Unexpected trailing bytes in DOS save file.',
|
final List<DoorSaveState> dosDoors = _decodeDosDoors(
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final PlayerSaveState player = _decodeDosPlayer(gamestate, actors);
|
|
||||||
final List<EntitySaveState> entities = _decodeDosEntities(actors);
|
|
||||||
final List<DoorSaveState> doors = _decodeDosDoors(
|
|
||||||
doorposition,
|
doorposition,
|
||||||
doorobjlist,
|
doorobjlist,
|
||||||
);
|
);
|
||||||
final List<PushwallSaveState> pushwalls = _decodeDosPushwalls(
|
final List<PushwallSaveState> dosPushwalls = _decodeDosPushwalls(
|
||||||
pushwallState,
|
pushwallState,
|
||||||
);
|
);
|
||||||
|
|
||||||
final GameSessionSnapshot snapshot = GameSessionSnapshot(
|
int slot = 0;
|
||||||
|
GameVersion gameVersion = GameVersion.retail;
|
||||||
|
String dataVersionName = 'dos-compat';
|
||||||
|
int createdAtMs = 0;
|
||||||
|
|
||||||
|
GameSessionSnapshot snapshot = GameSessionSnapshot(
|
||||||
currentGameIndex: 0,
|
currentGameIndex: 0,
|
||||||
currentEpisodeIndex: gamestate.episode.clamp(0, 5),
|
currentEpisodeIndex: gamestate.episode.clamp(0, 5),
|
||||||
currentLevelIndex: gamestate.mapon.clamp(0, 9),
|
currentLevelIndex: gamestate.mapon.clamp(0, 9),
|
||||||
@@ -792,21 +811,44 @@ class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
|
|||||||
lastAcousticAlertTime: 0,
|
lastAcousticAlertTime: 0,
|
||||||
isMapOverlayVisible: false,
|
isMapOverlayVisible: false,
|
||||||
isMenuOverlayVisible: false,
|
isMenuOverlayVisible: false,
|
||||||
player: player,
|
player: dosPlayer,
|
||||||
currentLevel: tilemap,
|
currentLevel: tilemap,
|
||||||
areaGrid: _emptyAreaGrid(),
|
areaGrid: _emptyAreaGrid(),
|
||||||
areasByPlayer: areasByPlayer,
|
areasByPlayer: areasByPlayer,
|
||||||
entities: entities,
|
entities: dosEntities,
|
||||||
doors: doors,
|
doors: dosDoors,
|
||||||
pushwalls: pushwalls,
|
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(
|
return SaveGameFile(
|
||||||
slot: 0,
|
slot: slot,
|
||||||
gameVersion: GameVersion.retail,
|
gameVersion: gameVersion,
|
||||||
dataVersionName: 'dos-compat',
|
dataVersionName: dataVersionName,
|
||||||
description: description,
|
description: description,
|
||||||
createdAtMs: 0,
|
createdAtMs: createdAtMs,
|
||||||
snapshot: snapshot,
|
snapshot: snapshot,
|
||||||
checksum: storedChecksum,
|
checksum: storedChecksum,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
arcane_helper_utils: ^1.4.7
|
arcane_helper_utils: ^1.4.7
|
||||||
audioplayers: ^6.6.0
|
audioplayers: ^6.6.0
|
||||||
crypto: ^3.0.7
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^6.0.0
|
lints: ^6.0.0
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('DataVersion.fromChecksum', () {
|
||||||
|
test('resolves all known checksum constants', () {
|
||||||
|
for (final version in DataVersion.values.where(
|
||||||
|
(version) => version != DataVersion.unknown,
|
||||||
|
)) {
|
||||||
|
expect(DataVersion.fromChecksum(version.checksum), version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns unknown for unrecognized checksum', () {
|
||||||
|
expect(
|
||||||
|
DataVersion.fromChecksum('ffffffffffffffffffffffffffffffff'),
|
||||||
|
DataVersion.unknown,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/data/md5_hash.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('md5HexLower', () {
|
||||||
|
test('matches canonical RFC vectors', () {
|
||||||
|
expect(md5HexLower(const <int>[]), 'd41d8cd98f00b204e9800998ecf8427e');
|
||||||
|
expect(
|
||||||
|
md5HexLower(utf8.encode('abc')),
|
||||||
|
'900150983cd24fb0d6963f7d28e17f72',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
md5HexLower(utf8.encode('The quick brown fox jumps over the lazy dog')),
|
||||||
|
'9e107d9d372bb6826bd81d3542a419d6',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
md5HexLower(
|
||||||
|
utf8.encode(
|
||||||
|
'12345678901234567890123456789012345678901234567890123456789012345678901234567890',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'57edf4a22be3c955ac49da2e2107b67a',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns lowercase 32-character hex output', () {
|
||||||
|
final digest = md5HexLower(utf8.encode('Wolf3D'));
|
||||||
|
expect(digest, hasLength(32));
|
||||||
|
expect(digest, matches(RegExp(r'^[0-9a-f]{32}$')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -183,9 +183,10 @@ void main() {
|
|||||||
final Uint8List encoded = codec.encode(file);
|
final Uint8List encoded = codec.encode(file);
|
||||||
final SaveGameFile decoded = codec.decode(encoded);
|
final SaveGameFile decoded = codec.decode(encoded);
|
||||||
|
|
||||||
expect(decoded.slot, 0);
|
expect(decoded.slot, 2);
|
||||||
expect(decoded.description, 'Compatible Block Save');
|
expect(decoded.description, 'Compatible Block Save');
|
||||||
expect(decoded.createdAtMs, 0);
|
expect(decoded.createdAtMs, 1234);
|
||||||
|
expect(decoded.dataVersionName, file.dataVersionName);
|
||||||
expect(
|
expect(
|
||||||
decoded.snapshot.currentEpisodeIndex,
|
decoded.snapshot.currentEpisodeIndex,
|
||||||
file.snapshot.currentEpisodeIndex,
|
file.snapshot.currentEpisodeIndex,
|
||||||
@@ -193,6 +194,45 @@ void main() {
|
|||||||
expect(decoded.snapshot.currentLevelIndex, file.snapshot.currentLevelIndex);
|
expect(decoded.snapshot.currentLevelIndex, file.snapshot.currentLevelIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('CompatibleSaveGameCodec preserves entity state fidelity', () {
|
||||||
|
final WolfEngine engine = _buildEngine();
|
||||||
|
engine.init();
|
||||||
|
|
||||||
|
final Guard guard =
|
||||||
|
EntityRegistry.spawn(
|
||||||
|
MapObject.guardStart,
|
||||||
|
8.5,
|
||||||
|
7.5,
|
||||||
|
Difficulty.medium,
|
||||||
|
engine.data.sprites.length,
|
||||||
|
registry: engine.data.registry,
|
||||||
|
)!
|
||||||
|
as Guard
|
||||||
|
..health = 5
|
||||||
|
..state = EntityState.dead;
|
||||||
|
engine.entities = <Entity>[guard];
|
||||||
|
|
||||||
|
final CompatibleSaveGameCodec codec = CompatibleSaveGameCodec();
|
||||||
|
final SaveGameFile decoded = codec.decode(
|
||||||
|
codec.encode(
|
||||||
|
SaveGameFile(
|
||||||
|
slot: 0,
|
||||||
|
gameVersion: engine.data.version,
|
||||||
|
dataVersionName: engine.data.dataVersion.name,
|
||||||
|
description: 'Entity Fidelity',
|
||||||
|
createdAtMs: 1,
|
||||||
|
snapshot: engine.captureSaveState(),
|
||||||
|
checksum: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decoded.snapshot.entities, hasLength(1));
|
||||||
|
expect(decoded.snapshot.entities.first.kind, 'Guard');
|
||||||
|
expect(decoded.snapshot.entities.first.state, EntityState.dead);
|
||||||
|
expect(decoded.snapshot.entities.first.extraData['health'], 5);
|
||||||
|
});
|
||||||
|
|
||||||
test('CompatibleSaveGameCodec decodes old envelope payload format', () {
|
test('CompatibleSaveGameCodec decodes old envelope payload format', () {
|
||||||
final WolfEngine engine = _buildEngine();
|
final WolfEngine engine = _buildEngine();
|
||||||
engine.init();
|
engine.init();
|
||||||
|
|||||||
Reference in New Issue
Block a user