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:
2026-03-23 14:50:53 +01:00
parent 1a93b7d4a2
commit db06f5f5cb
12 changed files with 1205 additions and 9 deletions
+2
View File
@@ -8,6 +8,7 @@ import 'dart:io';
import 'package:wolf_3d_cli/cli_game_loop.dart';
import 'package:wolf_3d_cli/cli_renderer_settings_persistence.dart';
import 'package:wolf_3d_cli/cli_save_game_persistence.dart';
import 'package:wolf_3d_dart/wolf_3d_data.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
@@ -63,6 +64,7 @@ void main() async {
input: CliInput(),
onGameWon: () => stopAndExit(0),
onQuit: () => stopAndExit(0),
saveGamePersistence: CliSaveGamePersistence(),
);
engine.init();
@@ -0,0 +1,55 @@
library;
import 'dart:io';
import 'dart:typed_data';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// CLI host adapter for slot-based game save persistence.
///
/// Files are stored under `~/.wolf3d_saves` by default and named
/// `SAVEGAM{slot}.{ext}` where `{ext}` follows the active game version.
class CliSaveGamePersistence implements SaveGamePersistence {
CliSaveGamePersistence({String? directoryPath})
: _directoryPath =
directoryPath ??
'${Platform.environment['HOME'] ?? '.'}/.wolf3d_saves';
final String _directoryPath;
@override
Future<Uint8List?> load({
required int slot,
required GameVersion version,
}) async {
try {
final File file = File(_slotPath(slot, version));
if (!file.existsSync()) {
return null;
}
return await file.readAsBytes();
} catch (_) {
return null;
}
}
@override
Future<void> save({
required int slot,
required GameVersion version,
required Uint8List bytes,
}) async {
final Directory dir = Directory(_directoryPath);
if (!dir.existsSync()) {
await dir.create(recursive: true);
}
await File(_slotPath(slot, version)).writeAsBytes(bytes, flush: true);
}
String _slotPath(int slot, GameVersion version) {
final String normalizedSlot = slot.clamp(0, 9).toString();
return '$_directoryPath/SAVEGAM$normalizedSlot.${version.fileExtension}';
}
}
@@ -11,6 +11,7 @@ import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
import 'package:wolf_3d_flutter/renderer_settings_persistence_flutter.dart';
import 'package:wolf_3d_flutter/save_game_persistence_flutter.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
import 'package:wolf_3d_gui/screens/debug_tools_screen.dart';
@@ -142,6 +143,8 @@ class _GameScreenState extends State<GameScreen> {
late final WolfEngine _engine;
final FlutterRendererSettingsPersistence _persistence =
FlutterRendererSettingsPersistence();
final FlutterSaveGamePersistence _savePersistence =
FlutterSaveGamePersistence();
/// Mirrors [WolfRendererSettings.mode] into the Flutter renderer enum.
RendererMode _rendererMode = RendererMode.hardware;
@@ -181,6 +184,7 @@ class _GameScreenState extends State<GameScreen> {
onQuit: () {
SystemNavigator.pop();
},
saveGamePersistence: _savePersistence,
);
_syncRendererModeFrom(_engine.rendererSettings);
_loadPersistedSettings();