feat: Refactor game persistence and rendering management for CLI and Flutter hosts

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 19:48:02 +01:00
parent 6158a92fb0
commit 6784d2dd16
14 changed files with 166 additions and 231 deletions
+1 -1
View File
@@ -7,11 +7,11 @@ library;
import 'dart:io';
import 'package:args/args.dart';
import 'package:wolf_3d_cli/cli_game_loop.dart';
import 'package:wolf_3d_dart/wolf_3d_audio.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';
import 'package:wolf_3d_dart/wolf_3d_host.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
/// Restores terminal state before exiting the process with [code].
@@ -1,58 +0,0 @@
/// CLI host adapter for persisting renderer settings to a local file.
library;
import 'dart:io';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Persists [WolfRendererSettings] as JSON to a local file.
///
/// The default path is `~/.wolf3d_cli_settings.json`.
/// An alternative [filePath] can be supplied at construction time.
class CliRendererSettingsPersistence extends RendererSettingsPersistence
with JsonRendererSettingsPersistence {
CliRendererSettingsPersistence({String? filePath})
: _filePath = filePath ?? '${_platformConfigDir()}/settings.json';
final String _filePath;
@override
Future<String?> readRaw() async {
try {
final File f = File(_filePath);
if (!f.existsSync()) {
return null;
}
return await f.readAsString();
} catch (_) {
return null;
}
}
@override
Future<void> writeRaw(String json) async {
try {
await File(_filePath).writeAsString(json, flush: true);
} catch (_) {
// Best-effort; never crash the loop on a write failure.
}
}
}
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';
}
@@ -1,84 +0,0 @@
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 ?? '${_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}';
}
}
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';
}
@@ -0,0 +1,3 @@
library;
export 'cli_game_loop_stub.dart' if (dart.library.io) 'cli_game_loop_io.dart';
@@ -26,7 +26,6 @@ class CliGameLoop {
'engine.input',
'CliGameLoop requires a CliInput instance.',
),
primaryRenderer = SixelRenderer(),
secondaryRenderer = AsciiRenderer(
mode: AsciiRendererMode.terminalAnsi,
@@ -0,0 +1,37 @@
/// Web-safe stub for CLI game loop APIs.
library;
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';
class CliGameLoop {
CliGameLoop({
required this.engine,
required this.onExit,
this.persistence,
this.initialSettings,
}) : input = engine.input is CliInput
? engine.input as CliInput
: throw ArgumentError.value(
engine.input,
'engine.input',
'CliGameLoop requires a CliInput instance.',
),
primaryRenderer = SixelRenderer(),
secondaryRenderer = AsciiRenderer(mode: AsciiRendererMode.terminalAnsi);
final WolfEngine engine;
final CliRendererBackend primaryRenderer;
final CliRendererBackend secondaryRenderer;
final CliInput input;
final void Function(int code) onExit;
final RendererSettingsPersistence? persistence;
final WolfRendererSettings? initialSettings;
Future<void> start() {
throw UnsupportedError('CliGameLoop is only available on dart:io hosts.');
}
void stop() {}
}
@@ -0,0 +1,42 @@
library;
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Coordinates gameplay persistence concerns for host applications.
class GamePersistenceManager {
/// Creates persistence manager dependencies with overridable adapters.
GamePersistenceManager({
RendererSettingsPersistence? rendererSettingsPersistence,
SaveGamePersistence? saveGamePersistence,
}) : rendererSettingsPersistence =
rendererSettingsPersistence ?? DefaultRendererSettingsPersistence(),
saveGamePersistence =
saveGamePersistence ?? DefaultSaveGamePersistence();
/// Persists and restores runtime renderer settings.
final RendererSettingsPersistence rendererSettingsPersistence;
/// Persists slot-based save game snapshots.
final SaveGamePersistence saveGamePersistence;
/// Loads previously persisted renderer settings.
Future<WolfRendererSettings?> loadRendererSettings() {
return rendererSettingsPersistence.load();
}
/// Loads persisted renderer settings and applies them to [engine].
Future<WolfRendererSettings?> restoreRendererSettings(
WolfEngine engine,
) async {
final WolfRendererSettings? saved = await loadRendererSettings();
if (saved != null) {
engine.updateRendererSettings(saved);
}
return saved;
}
/// Saves current renderer settings.
Future<void> saveRendererSettings(WolfRendererSettings settings) {
return rendererSettingsPersistence.save(settings);
}
}
@@ -0,0 +1,44 @@
library;
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Renderer presentation mode used by host widgets.
enum GameRendererMode {
/// Software pixel renderer presented via decoded framebuffer images.
software,
/// Text-mode renderer for debugging and retro terminal aesthetics.
ascii,
/// GLSL renderer with optional CRT-style post processing.
hardware,
}
/// Maps engine renderer settings to host renderer presentation mode.
GameRendererMode gameRendererModeFromSettings(WolfRendererSettings settings) {
return switch (settings.mode) {
WolfRendererMode.hardware => GameRendererMode.hardware,
WolfRendererMode.software => GameRendererMode.software,
WolfRendererMode.ascii || WolfRendererMode.sixel => GameRendererMode.ascii,
};
}
/// Falls back to software mode when GLSL rendering is unavailable at runtime.
void handleGlslUnavailable({
required bool isMounted,
required GameRendererMode rendererMode,
required WolfEngine? engine,
}) {
if (!isMounted || rendererMode != GameRendererMode.hardware) {
return;
}
final WolfEngine? activeEngine = engine;
if (activeEngine == null) {
return;
}
activeEngine.updateRendererSettings(
activeEngine.rendererSettings.copyWith(mode: WolfRendererMode.software),
);
}
@@ -0,0 +1,25 @@
library;
import 'dart:async';
import 'ascii_renderer.dart';
/// Web-safe stub used when dart:io is unavailable.
class SixelRenderer extends AsciiRenderer {
SixelRenderer() : super(mode: AsciiRendererMode.terminalAnsi);
bool isSixelSupported = false;
@override
bool isTerminalSizeSupported(int columns, int rows) => false;
@override
String get terminalSizeRequirement =>
'Sixel renderer is unavailable on this platform.';
static Future<bool> checkTerminalSixelSupport({
Stream<List<int>>? inputStream,
}) async {
return false;
}
}
@@ -0,0 +1,8 @@
/// Shared host-facing helpers for Wolf3D app shells.
library;
export 'src/host/cli_game_loop.dart' show CliGameLoop;
export 'src/host/managers/game_persistence_manager.dart'
show GamePersistenceManager;
export 'src/host/managers/game_renderer_mode_manager.dart'
show GameRendererMode, gameRendererModeFromSettings, handleGlslUnavailable;
@@ -6,5 +6,6 @@ export 'src/rendering/ascii_renderer.dart'
show AsciiRenderer, AsciiRendererMode, AsciiTheme, AsciiThemes, ColoredChar;
export 'src/rendering/cli_renderer_backend.dart';
export 'src/rendering/renderer_backend.dart';
export 'src/rendering/sixel_renderer.dart';
export 'src/rendering/sixel_renderer_stub.dart'
if (dart.library.io) 'src/rendering/sixel_renderer.dart';
export 'src/rendering/software_renderer.dart';
@@ -1,42 +1,3 @@
library;
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Coordinates gameplay persistence concerns for Flutter hosts.
class GamePersistenceManager {
/// Creates persistence manager dependencies with overridable adapters.
GamePersistenceManager({
RendererSettingsPersistence? rendererSettingsPersistence,
SaveGamePersistence? saveGamePersistence,
}) : rendererSettingsPersistence =
rendererSettingsPersistence ?? DefaultRendererSettingsPersistence(),
saveGamePersistence =
saveGamePersistence ?? DefaultSaveGamePersistence();
/// Persists and restores runtime renderer settings.
final RendererSettingsPersistence rendererSettingsPersistence;
/// Persists slot-based save game snapshots.
final SaveGamePersistence saveGamePersistence;
/// Loads previously persisted renderer settings.
Future<WolfRendererSettings?> loadRendererSettings() {
return rendererSettingsPersistence.load();
}
/// Loads persisted renderer settings and applies them to [engine].
Future<WolfRendererSettings?> restoreRendererSettings(
WolfEngine engine,
) async {
final WolfRendererSettings? saved = await loadRendererSettings();
if (saved != null) {
engine.updateRendererSettings(saved);
}
return saved;
}
/// Saves current renderer settings.
Future<void> saveRendererSettings(WolfRendererSettings settings) {
return rendererSettingsPersistence.save(settings);
}
}
export 'package:wolf_3d_dart/wolf_3d_host.dart' show GamePersistenceManager;
@@ -1,44 +1,4 @@
library;
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Renderer presentation mode used by Flutter host widgets.
enum GameRendererMode {
/// Software pixel renderer presented via decoded framebuffer images.
software,
/// Text-mode renderer for debugging and retro terminal aesthetics.
ascii,
/// GLSL renderer with optional CRT-style post processing.
hardware,
}
/// Maps engine renderer settings to host renderer presentation mode.
GameRendererMode gameRendererModeFromSettings(WolfRendererSettings settings) {
return switch (settings.mode) {
WolfRendererMode.hardware => GameRendererMode.hardware,
WolfRendererMode.software => GameRendererMode.software,
WolfRendererMode.ascii || WolfRendererMode.sixel => GameRendererMode.ascii,
};
}
/// Falls back to software mode when GLSL rendering is unavailable at runtime.
void handleGlslUnavailable({
required bool isMounted,
required GameRendererMode rendererMode,
required WolfEngine? engine,
}) {
if (!isMounted || rendererMode != GameRendererMode.hardware) {
return;
}
final WolfEngine? activeEngine = engine;
if (activeEngine == null) {
return;
}
activeEngine.updateRendererSettings(
activeEngine.rendererSettings.copyWith(mode: WolfRendererMode.software),
);
}
export 'package:wolf_3d_dart/wolf_3d_host.dart'
show GameRendererMode, gameRendererModeFromSettings, handleGlslUnavailable;
@@ -44,10 +44,7 @@ class _NoopAudio implements EngineAudio {
}
class _RecordingEngine extends Wolf3dFlutterEngine {
_RecordingEngine({required this.audio}) : super(audioBackend: audio);
@override
final _NoopAudio audio;
_RecordingEngine({required _NoopAudio audio}) : super(audioBackend: audio);
int initCallCount = 0;
String? lastDirectory;