feat: Implement platform-specific persistence for renderer settings and save games

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 16:28:35 +01:00
parent dcfb2e8e02
commit 26c738b702
14 changed files with 291 additions and 28 deletions
@@ -0,0 +1,5 @@
/// Routes to the native or stub implementation based on platform.
library;
export 'default_renderer_settings_persistence_stub.dart'
if (dart.library.io) 'default_renderer_settings_persistence_io.dart';
@@ -0,0 +1,49 @@
/// Native (dart:io) renderer-settings persistence.
library;
import 'dart:io';
import 'package:wolf_3d_dart/src/engine/rendering/renderer_settings.dart';
import 'package:wolf_3d_dart/src/engine/rendering/renderer_settings_persistence.dart';
import 'package:wolf_3d_dart/src/platform/platform_config_dir.dart';
/// Persists [WolfRendererSettings] as JSON to the platform config directory.
///
/// Pass an explicit [filePath] to override the default location (useful in tests).
class DefaultRendererSettingsPersistence extends RendererSettingsPersistence
with JsonRendererSettingsPersistence {
DefaultRendererSettingsPersistence({String? filePath}) : _filePath = filePath;
final String? _filePath;
String? _resolvedPath;
Future<String> _getFilePath() async {
if (_resolvedPath != null) return _resolvedPath!;
_resolvedPath = _filePath ?? '${platformConfigDir()}/settings.json';
return _resolvedPath!;
}
@override
Future<String?> readRaw() async {
try {
final String path = await _getFilePath();
final File f = File(path);
if (!f.existsSync()) return null;
return await f.readAsString();
} catch (_) {
return null;
}
}
@override
Future<void> writeRaw(String json) async {
try {
final String path = await _getFilePath();
final Directory dir = File(path).parent;
if (!dir.existsSync()) await dir.create(recursive: true);
await File(path).writeAsString(json, flush: true);
} catch (_) {
// Best-effort.
}
}
}
@@ -0,0 +1,17 @@
/// Web stub for renderer-settings persistence: silently skips all I/O.
library;
import 'package:wolf_3d_dart/src/engine/rendering/renderer_settings.dart';
import 'package:wolf_3d_dart/src/engine/rendering/renderer_settings_persistence.dart';
/// No-op implementation used on web, where dart:io is unavailable.
class DefaultRendererSettingsPersistence extends RendererSettingsPersistence {
// ignore: avoid_unused_constructor_parameters
DefaultRendererSettingsPersistence({String? filePath});
@override
Future<WolfRendererSettings?> load() async => null;
@override
Future<void> save(WolfRendererSettings settings) async {}
}
@@ -0,0 +1,5 @@
/// Routes to the native or stub implementation based on platform.
library;
export 'default_save_game_persistence_stub.dart'
if (dart.library.io) 'default_save_game_persistence_io.dart';
@@ -0,0 +1,65 @@
/// Native (dart:io) slot-based save-game persistence.
library;
import 'dart:io';
import 'dart:typed_data';
import 'package:wolf_3d_dart/src/engine/save/save_game_persistence.dart';
import 'package:wolf_3d_dart/src/platform/platform_config_dir.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
/// Persists save-game slots as raw bytes under the platform config directory.
///
/// Files are stored in `<configDir>/saves/` and named
/// `SAVEGAM{slot}.{ext}` where `{ext}` follows the active game version.
///
/// Pass an explicit [directoryPath] to override the default (useful in tests).
class DefaultSaveGamePersistence extends SaveGamePersistence {
DefaultSaveGamePersistence({String? directoryPath})
: _directoryPath = directoryPath ?? '${platformConfigDir()}/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<bool> exists({
required int slot,
required GameVersion version,
}) async {
try {
final File file = File(_slotPath(slot, version));
return file.existsSync() && file.lengthSync() > 0;
} catch (_) {
return false;
}
}
@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}';
}
}
@@ -0,0 +1,32 @@
/// Web stub for save-game persistence: silently skips all I/O.
library;
import 'dart:typed_data';
import 'package:wolf_3d_dart/src/engine/save/save_game_persistence.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
/// No-op implementation used on web, where dart:io is unavailable.
class DefaultSaveGamePersistence extends SaveGamePersistence {
// ignore: avoid_unused_constructor_parameters
DefaultSaveGamePersistence({String? directoryPath});
@override
Future<Uint8List?> load({
required int slot,
required GameVersion version,
}) async => null;
@override
Future<bool> exists({
required int slot,
required GameVersion version,
}) async => false;
@override
Future<void> save({
required int slot,
required GameVersion version,
required Uint8List bytes,
}) async {}
}
@@ -0,0 +1,31 @@
/// Returns the platform-appropriate Wolf3D config directory path.
///
/// This file is only ever imported by native (dart:io) code paths and must
/// never be loaded on web.
library;
import 'dart:io';
/// Returns the Wolf3D config directory for the current platform.
///
/// - Linux: `$XDG_CONFIG_HOME/wolf3d` (defaults to `~/.config/wolf3d`)
/// - macOS: `~/Library/Application Support/wolf3d`
/// - Windows: `%APPDATA%/wolf3d`
/// - Other: `~/.config/wolf3d`
String platformConfigDir() {
if (Platform.isLinux) {
final String xdg = Platform.environment['XDG_CONFIG_HOME'] ?? '';
final String home = Platform.environment['HOME'] ?? '.';
return xdg.isNotEmpty ? '$xdg/wolf3d' : '$home/.config/wolf3d';
}
if (Platform.isMacOS) {
final String home = Platform.environment['HOME'] ?? '.';
return '$home/Library/Application Support/wolf3d';
}
if (Platform.isWindows) {
final String appData = Platform.environment['APPDATA'] ?? '.';
return '$appData/wolf3d';
}
final String home = Platform.environment['HOME'] ?? '.';
return '$home/.config/wolf3d';
}