From b980174905e6b8547e6bce38ff60d887b876a26f Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 23 Mar 2026 19:54:22 +0100 Subject: [PATCH] feat: Enhance DefaultRendererSettingsPersistence to support scoped settings for CLI and Flutter Signed-off-by: Hans Kokx --- apps/wolf_3d_cli/bin/main.dart | 4 +- ...ault_renderer_settings_persistence_io.dart | 60 ++++++++++++++++- ...lt_renderer_settings_persistence_stub.dart | 8 ++- .../managers/game_persistence_manager.dart | 5 +- .../game_data_directory_persistence_test.dart | 64 +++++++++++++++++++ 5 files changed, 135 insertions(+), 6 deletions(-) diff --git a/apps/wolf_3d_cli/bin/main.dart b/apps/wolf_3d_cli/bin/main.dart index 57ec2dc..91bde3f 100644 --- a/apps/wolf_3d_cli/bin/main.dart +++ b/apps/wolf_3d_cli/bin/main.dart @@ -111,7 +111,9 @@ void main(List arguments) async { await engine.audio.init(); engine.init(); - final persistence = DefaultRendererSettingsPersistence(); + final persistence = DefaultRendererSettingsPersistence( + hostKey: rendererSettingsHostCli, + ); final WolfRendererSettings? saved = await persistence.load(); gameLoop = CliGameLoop( diff --git a/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_io.dart b/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_io.dart index 0698047..f5e06ba 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_io.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_io.dart @@ -1,20 +1,29 @@ /// Native (dart:io) renderer-settings persistence. library; +import 'dart:convert'; 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'; +const String rendererSettingsHostCli = 'cli'; +const String rendererSettingsHostFlutter = 'flutter'; + /// 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; + DefaultRendererSettingsPersistence({ + String? filePath, + String hostKey = rendererSettingsHostFlutter, + }) : _filePath = filePath, + _hostKey = hostKey; final String? _filePath; + final String _hostKey; String? _resolvedPath; Future _getFilePath() async { @@ -29,7 +38,23 @@ class DefaultRendererSettingsPersistence extends RendererSettingsPersistence final String path = await _getFilePath(); final File f = File(path); if (!f.existsSync()) return null; - return await f.readAsString(); + final String raw = await f.readAsString(); + final Object? decoded = jsonDecode(raw); + if (decoded is! Map) { + return null; + } + + final Object? rendererSettings = decoded['rendererSettings']; + if (rendererSettings is! Map) { + return null; + } + + final Object? scoped = rendererSettings[_hostKey]; + if (scoped is! Map) { + return null; + } + + return jsonEncode(scoped); } catch (_) { return null; } @@ -41,7 +66,36 @@ class DefaultRendererSettingsPersistence extends RendererSettingsPersistence 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); + + final File f = File(path); + Map root = {}; + if (f.existsSync()) { + try { + final Object? existing = jsonDecode(await f.readAsString()); + if (existing is Map) { + root = Map.from(existing); + } + } catch (_) { + root = {}; + } + } + + final Object? scopedDecoded = jsonDecode(json); + if (scopedDecoded is! Map) { + return; + } + + final Map rendererSettings = + root['rendererSettings'] is Map + ? Map.from( + root['rendererSettings']! as Map, + ) + : {}; + + rendererSettings[_hostKey] = Map.from(scopedDecoded); + root['rendererSettings'] = rendererSettings; + + await f.writeAsString(jsonEncode(root), flush: true); } catch (_) { // Best-effort. } diff --git a/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_stub.dart b/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_stub.dart index 9f1f5f8..119b0c5 100644 --- a/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_stub.dart +++ b/packages/wolf_3d_dart/lib/src/engine/rendering/default_renderer_settings_persistence_stub.dart @@ -4,10 +4,16 @@ library; import 'package:wolf_3d_dart/src/engine/rendering/renderer_settings.dart'; import 'package:wolf_3d_dart/src/engine/rendering/renderer_settings_persistence.dart'; +const String rendererSettingsHostCli = 'cli'; +const String rendererSettingsHostFlutter = 'flutter'; + /// No-op implementation used on web, where dart:io is unavailable. class DefaultRendererSettingsPersistence extends RendererSettingsPersistence { // ignore: avoid_unused_constructor_parameters - DefaultRendererSettingsPersistence({String? filePath}); + DefaultRendererSettingsPersistence({ + String? filePath, + String hostKey = 'flutter', + }); @override Future load() async => null; diff --git a/packages/wolf_3d_dart/lib/src/host/managers/game_persistence_manager.dart b/packages/wolf_3d_dart/lib/src/host/managers/game_persistence_manager.dart index c185e62..732f7fc 100644 --- a/packages/wolf_3d_dart/lib/src/host/managers/game_persistence_manager.dart +++ b/packages/wolf_3d_dart/lib/src/host/managers/game_persistence_manager.dart @@ -9,7 +9,10 @@ class GamePersistenceManager { RendererSettingsPersistence? rendererSettingsPersistence, SaveGamePersistence? saveGamePersistence, }) : rendererSettingsPersistence = - rendererSettingsPersistence ?? DefaultRendererSettingsPersistence(), + rendererSettingsPersistence ?? + DefaultRendererSettingsPersistence( + hostKey: rendererSettingsHostFlutter, + ), saveGamePersistence = saveGamePersistence ?? DefaultSaveGamePersistence(); diff --git a/packages/wolf_3d_flutter/test/game_data_directory_persistence_test.dart b/packages/wolf_3d_flutter/test/game_data_directory_persistence_test.dart index e06422e..33fa6b8 100644 --- a/packages/wolf_3d_flutter/test/game_data_directory_persistence_test.dart +++ b/packages/wolf_3d_flutter/test/game_data_directory_persistence_test.dart @@ -154,6 +154,70 @@ void main() { }); }); + group('DefaultRendererSettingsPersistence', () { + test('stores separate scoped settings for flutter and cli', () async { + final tempDir = await Directory.systemTemp.createTemp( + 'wolf3d-renderer-config-', + ); + addTearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + final String path = '${tempDir.path}/settings.json'; + final flutterPersistence = DefaultRendererSettingsPersistence( + filePath: path, + hostKey: rendererSettingsHostFlutter, + ); + final cliPersistence = DefaultRendererSettingsPersistence( + filePath: path, + hostKey: rendererSettingsHostCli, + ); + + const flutterSettings = WolfRendererSettings( + mode: WolfRendererMode.hardware, + ); + const cliSettings = WolfRendererSettings(mode: WolfRendererMode.sixel); + + await flutterPersistence.save(flutterSettings); + await cliPersistence.save(cliSettings); + + final loadedFlutter = await flutterPersistence.load(); + final loadedCli = await cliPersistence.load(); + expect(loadedFlutter, isNotNull); + expect(loadedCli, isNotNull); + expect(loadedFlutter!.mode, flutterSettings.mode); + expect(loadedCli!.mode, cliSettings.mode); + + final String raw = await File(path).readAsString(); + expect(raw, contains('"rendererSettings"')); + expect(raw, contains('"flutter"')); + expect(raw, contains('"cli"')); + }); + + test('does not fall back to legacy unscoped renderer payload', () async { + final tempDir = await Directory.systemTemp.createTemp( + 'wolf3d-renderer-config-', + ); + addTearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + final String path = '${tempDir.path}/settings.json'; + await File(path).writeAsString('{"mode":"hardware"}'); + + final persistence = DefaultRendererSettingsPersistence( + filePath: path, + hostKey: rendererSettingsHostFlutter, + ); + + expect(await persistence.load(), isNull); + }); + }); + testWidgets('Wolf3dApp forwards configured directory to no-data screen', ( tester, ) async {