feat: Implement save game functionality with encoding/decoding
- Added SaveGameCodec for encoding and decoding save game files. - Introduced SaveGamePersistence interface for slot-based save game persistence. - Implemented FlutterSaveGamePersistence for file-based save management on Flutter. - Enhanced WolfEngine to support saving and loading game states. - Updated menu manager to include save/load game options. - Created tests for SaveGameCodec to ensure proper functionality. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -154,7 +154,7 @@ void main() {
|
||||
engine.menuManager.mainMenuEntries
|
||||
.map((entry) => entry.isEnabled)
|
||||
.toList(),
|
||||
[true, false, false, false, false, true, false, false, true, true],
|
||||
[true, false, false, true, false, true, false, false, true, true],
|
||||
);
|
||||
|
||||
input.isInteracting = true;
|
||||
@@ -194,7 +194,7 @@ void main() {
|
||||
engine.menuManager.mainMenuEntries
|
||||
.map((entry) => entry.isEnabled)
|
||||
.toList(),
|
||||
[true, false, false, false, false, true, false, false, true, true],
|
||||
[true, false, false, true, false, true, false, false, true, true],
|
||||
);
|
||||
|
||||
input.isInteracting = true;
|
||||
@@ -242,7 +242,7 @@ void main() {
|
||||
engine.menuManager.mainMenuEntries
|
||||
.map((entry) => entry.isEnabled)
|
||||
.toList(),
|
||||
[true, false, false, false, false, true, false, true, true, true],
|
||||
[true, false, false, true, true, true, false, true, true, true],
|
||||
);
|
||||
|
||||
input.isMovingForward = true;
|
||||
@@ -273,6 +273,10 @@ void main() {
|
||||
|
||||
expect(manager.selectedMainIndex, 0);
|
||||
|
||||
manager.updateMainMenu(const EngineInput(isMovingBackward: true));
|
||||
manager.updateMainMenu(const EngineInput());
|
||||
expect(manager.selectedMainIndex, 3);
|
||||
|
||||
manager.updateMainMenu(const EngineInput(isMovingBackward: true));
|
||||
manager.updateMainMenu(const EngineInput());
|
||||
expect(manager.selectedMainIndex, 5);
|
||||
@@ -363,6 +367,11 @@ void main() {
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
expect(engine.menuManager.selectedMainIndex, 9);
|
||||
|
||||
input.isInteracting = true;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||
|
||||
void main() {
|
||||
test('SaveGameCodec round-trips a captured engine session snapshot', () {
|
||||
final WolfEngine engine = _buildEngine();
|
||||
engine.init();
|
||||
|
||||
engine.player
|
||||
..health = 80
|
||||
..ammo = 40
|
||||
..score = 900
|
||||
..lives = 6
|
||||
..hasMachineGun = true
|
||||
..weapons[WeaponType.machineGun] = MachineGun()
|
||||
..currentWeapon = MachineGun();
|
||||
|
||||
final Guard guard =
|
||||
EntityRegistry.spawn(
|
||||
MapObject.guardStart,
|
||||
8.5,
|
||||
7.5,
|
||||
Difficulty.medium,
|
||||
engine.data.sprites.length,
|
||||
registry: engine.data.registry,
|
||||
)!
|
||||
as Guard
|
||||
..health = 11
|
||||
..state = EntityState.attacking;
|
||||
engine.entities = <Entity>[guard, SmallAmmoCollectible(x: 7.5, y: 6.5)];
|
||||
|
||||
final GameSessionSnapshot snapshot = engine.captureSaveState();
|
||||
|
||||
final SaveGameCodec codec = SaveGameCodec();
|
||||
final SaveGameFile file = SaveGameFile(
|
||||
slot: 0,
|
||||
gameVersion: engine.data.version,
|
||||
dataVersionName: engine.data.dataVersion.name,
|
||||
description: 'Unit Test Save',
|
||||
createdAtMs: 123456789,
|
||||
snapshot: snapshot,
|
||||
checksum: 0,
|
||||
);
|
||||
|
||||
final Uint8List encoded = codec.encode(file);
|
||||
final SaveGameFile decoded = codec.decode(encoded);
|
||||
|
||||
expect(decoded.slot, 0);
|
||||
expect(decoded.gameVersion, engine.data.version);
|
||||
expect(decoded.description, 'Unit Test Save');
|
||||
expect(decoded.snapshot.player.health, 80);
|
||||
expect(decoded.snapshot.player.currentWeaponType, WeaponType.machineGun);
|
||||
expect(decoded.snapshot.entities, hasLength(2));
|
||||
expect(decoded.snapshot.entities.first.kind, 'Guard');
|
||||
expect(decoded.snapshot.entities.first.state, EntityState.attacking);
|
||||
});
|
||||
|
||||
test('SaveGameCodec rejects payloads with invalid checksum', () {
|
||||
final WolfEngine engine = _buildEngine();
|
||||
engine.init();
|
||||
|
||||
final SaveGameCodec codec = SaveGameCodec();
|
||||
final Uint8List encoded = codec.encode(
|
||||
SaveGameFile(
|
||||
slot: 0,
|
||||
gameVersion: engine.data.version,
|
||||
dataVersionName: engine.data.dataVersion.name,
|
||||
description: 'Checksum Test',
|
||||
createdAtMs: 42,
|
||||
snapshot: engine.captureSaveState(),
|
||||
checksum: 0,
|
||||
),
|
||||
);
|
||||
|
||||
encoded[encoded.length - 1] ^= 0xFF;
|
||||
|
||||
expect(
|
||||
() => codec.decode(encoded),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _TestInput extends Wolf3dInput {
|
||||
@override
|
||||
void update() {}
|
||||
}
|
||||
|
||||
class _SilentAudio implements EngineAudio {
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
@override
|
||||
Future<void> debugSoundTest() async {}
|
||||
|
||||
@override
|
||||
void dispose() {}
|
||||
|
||||
@override
|
||||
Future<void> init() async {}
|
||||
|
||||
@override
|
||||
void playLevelMusic(Music music) {}
|
||||
|
||||
@override
|
||||
void playMenuMusic() {}
|
||||
|
||||
@override
|
||||
void playSoundEffect(SoundEffect effect) {}
|
||||
|
||||
@override
|
||||
void playSoundEffectId(int sfxId) {}
|
||||
|
||||
@override
|
||||
Future<void> stopAllAudio() async {}
|
||||
|
||||
@override
|
||||
void stopMusic() {}
|
||||
}
|
||||
|
||||
WolfEngine _buildEngine() {
|
||||
final SpriteMap wallGrid = _buildGrid();
|
||||
final SpriteMap objectGrid = _buildGrid();
|
||||
_fillBoundaries(wallGrid, 2);
|
||||
|
||||
objectGrid[2][2] = MapObject.playerEast;
|
||||
wallGrid[2][3] = 90;
|
||||
|
||||
return WolfEngine(
|
||||
data: WolfensteinData(
|
||||
version: GameVersion.retail,
|
||||
dataVersion: DataVersion.unknown,
|
||||
registry: RetailAssetRegistry(),
|
||||
walls: <Sprite>[
|
||||
_solidSprite(1),
|
||||
_solidSprite(1),
|
||||
_solidSprite(2),
|
||||
_solidSprite(2),
|
||||
],
|
||||
sprites: List<Sprite>.generate(436, (_) => _solidSprite(255)),
|
||||
sounds: List<PcmSound>.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||
adLibSounds: const <PcmSound>[],
|
||||
music: const <ImfMusic>[],
|
||||
vgaImages: const <VgaImage>[],
|
||||
episodes: <Episode>[
|
||||
Episode(
|
||||
name: 'Episode 1',
|
||||
levels: <WolfLevel>[
|
||||
WolfLevel(
|
||||
name: 'Level 1',
|
||||
wallGrid: wallGrid,
|
||||
areaGrid: List<List<int>>.generate(
|
||||
64,
|
||||
(_) => List<int>.filled(64, -1),
|
||||
growable: false,
|
||||
),
|
||||
objectGrid: objectGrid,
|
||||
music: Music.level01,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
difficulty: Difficulty.medium,
|
||||
startingEpisode: 0,
|
||||
frameBuffer: FrameBuffer(64, 64),
|
||||
input: _TestInput(),
|
||||
onGameWon: () {},
|
||||
engineAudio: _SilentAudio(),
|
||||
);
|
||||
}
|
||||
|
||||
SpriteMap _buildGrid() =>
|
||||
List<List<int>>.generate(64, (_) => List<int>.filled(64, 0));
|
||||
|
||||
void _fillBoundaries(SpriteMap grid, int wallId) {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
grid[0][i] = wallId;
|
||||
grid[63][i] = wallId;
|
||||
grid[i][0] = wallId;
|
||||
grid[i][63] = wallId;
|
||||
}
|
||||
}
|
||||
|
||||
Sprite _solidSprite(int paletteIndex) {
|
||||
return Sprite(Uint8List.fromList(<int>[paletteIndex]));
|
||||
}
|
||||
Reference in New Issue
Block a user