Refactor menu structure and add Flutter-specific input and persistence layers

- Moved menu-related classes to a new structure under `src/menu/`.
- Introduced `WolfMenuPresentation` to handle menu art and mappings.
- Added `MenuManager` tests to ensure menu state reflects game status.
- Implemented `FlutterRendererSettingsPersistence` and `FlutterSaveGamePersistence` for managing settings and save files on desktop platforms.
- Created `Wolf3dFlutterInput` to handle keyboard and mouse input in a Flutter environment.
- Updated README to reflect new package structure and usage instructions.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-24 18:45:34 +01:00
parent 9f3651b122
commit 5c309c2240
37 changed files with 2356 additions and 1565 deletions
@@ -0,0 +1,75 @@
/// Flutter host adapter for persisting renderer settings to a local file.
///
/// Uses `dart:io` for desktop targets where a writable path is available.
/// On web and other platforms that lack dart:io, persistence is silently
/// skipped so the rest of the app continues to work normally.
library;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Persists [WolfRendererSettings] as JSON to the app's support directory.
///
/// This implementation relies on `dart:io` and is therefore only active on
/// non-web platforms. Pass an explicit [filePath] during testing.
class FlutterRendererSettingsPersistence extends RendererSettingsPersistence
with JsonRendererSettingsPersistence {
FlutterRendererSettingsPersistence({String? filePath}) : _filePath = filePath;
final String? _filePath;
String? _resolvedPath;
String get filePath {
if (_resolvedPath != null) {
return _resolvedPath!;
}
if (_filePath != null) {
_resolvedPath = _filePath;
return _resolvedPath!;
}
_resolvedPath = '$platformConfigDir/settings.json';
return _resolvedPath!;
}
@override
Future<String?> readRaw() async {
if (kIsWeb) return null;
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 {
if (kIsWeb) return;
try {
await File(filePath).writeAsString(json, flush: true);
} catch (_) {
// Best-effort.
}
}
String get 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,112 @@
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!;
}
_resolvedDirectoryPath = '${_platformConfigDir()}/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<bool> exists({
required int slot,
required GameVersion version,
}) async {
if (kIsWeb) {
return false;
}
try {
final String dirPath = await _resolveDirectoryPath();
final File file = File(_slotPath(dirPath, 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 {
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}';
}
}
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';
}