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:
@@ -7,11 +7,11 @@ library;
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
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_audio.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data.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_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.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';
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
|
||||||
/// Restores terminal state before exiting the process with [code].
|
/// 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';
|
||||||
-1
@@ -26,7 +26,6 @@ class CliGameLoop {
|
|||||||
'engine.input',
|
'engine.input',
|
||||||
'CliGameLoop requires a CliInput instance.',
|
'CliGameLoop requires a CliInput instance.',
|
||||||
),
|
),
|
||||||
|
|
||||||
primaryRenderer = SixelRenderer(),
|
primaryRenderer = SixelRenderer(),
|
||||||
secondaryRenderer = AsciiRenderer(
|
secondaryRenderer = AsciiRenderer(
|
||||||
mode: AsciiRendererMode.terminalAnsi,
|
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;
|
show AsciiRenderer, AsciiRendererMode, AsciiTheme, AsciiThemes, ColoredChar;
|
||||||
export 'src/rendering/cli_renderer_backend.dart';
|
export 'src/rendering/cli_renderer_backend.dart';
|
||||||
export 'src/rendering/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';
|
export 'src/rendering/software_renderer.dart';
|
||||||
|
|||||||
@@ -1,42 +1,3 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
export 'package:wolf_3d_dart/wolf_3d_host.dart' show GamePersistenceManager;
|
||||||
|
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,44 +1,4 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
export 'package:wolf_3d_dart/wolf_3d_host.dart'
|
||||||
|
show GameRendererMode, gameRendererModeFromSettings, handleGlslUnavailable;
|
||||||
/// 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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ class _NoopAudio implements EngineAudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RecordingEngine extends Wolf3dFlutterEngine {
|
class _RecordingEngine extends Wolf3dFlutterEngine {
|
||||||
_RecordingEngine({required this.audio}) : super(audioBackend: audio);
|
_RecordingEngine({required _NoopAudio audio}) : super(audioBackend: audio);
|
||||||
|
|
||||||
@override
|
|
||||||
final _NoopAudio audio;
|
|
||||||
|
|
||||||
int initCallCount = 0;
|
int initCallCount = 0;
|
||||||
String? lastDirectory;
|
String? lastDirectory;
|
||||||
|
|||||||
Reference in New Issue
Block a user