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
@@ -0,0 +1,78 @@
library;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Flutter desktop adapter for slot-based game save persistence.
class FlutterSaveGamePersistence implements SaveGamePersistence {
FlutterSaveGamePersistence({String? directoryPath})
: _directoryPath = directoryPath;
final String? _directoryPath;
String? _resolvedDirectoryPath;
Future<String> _resolveDirectoryPath() async {
if (_resolvedDirectoryPath != null) {
return _resolvedDirectoryPath!;
}
if (_directoryPath != null) {
_resolvedDirectoryPath = _directoryPath;
return _resolvedDirectoryPath!;
}
final String home =
Platform.environment['HOME'] ?? Platform.environment['APPDATA'] ?? '.';
_resolvedDirectoryPath = '$home/.wolf3d_saves';
return _resolvedDirectoryPath!;
}
@override
Future<Uint8List?> load({
required int slot,
required GameVersion version,
}) async {
if (kIsWeb) {
return null;
}
try {
final String dirPath = await _resolveDirectoryPath();
final File file = File(_slotPath(dirPath, 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 {
if (kIsWeb) {
return;
}
final String dirPath = await _resolveDirectoryPath();
final Directory dir = Directory(dirPath);
if (!dir.existsSync()) {
await dir.create(recursive: true);
}
await File(
_slotPath(dirPath, slot, version),
).writeAsBytes(bytes, flush: true);
}
String _slotPath(String dirPath, int slot, GameVersion version) {
final String normalizedSlot = slot.clamp(0, 9).toString();
return '$dirPath/SAVEGAM$normalizedSlot.${version.fileExtension}';
}
}
@@ -85,6 +85,7 @@ class Wolf3d {
WolfEngine launchEngine({
required void Function() onGameWon,
void Function()? onQuit,
SaveGamePersistence? saveGamePersistence,
WolfRendererCapabilities? rendererCapabilities,
WolfRendererSettings? rendererSettings,
void Function(WolfRendererSettings settings)? onRendererSettingsChanged,
@@ -109,6 +110,7 @@ class Wolf3d {
// so backing out of the top-level menu should not pop the route.
onMenuExit: () {},
onQuit: onQuit,
saveGamePersistence: saveGamePersistence,
rendererCapabilities: rendererCapabilities,
rendererSettings: rendererSettings,
onRendererSettingsChanged: onRendererSettingsChanged,