From 6608d0a975426012cb1edba09ad32243e7996f67 Mon Sep 17 00:00:00 2001 From: gem Date: Wed, 10 Jun 2026 11:31:17 +0800 Subject: [PATCH] feat: add Lua runtime storage API --- lib/flame_lua_runtime.dart | 1 + lib/runtime/game/flame_lua_game.dart | 6 ++ .../scripting/lua_dardo_script_engine.dart | 58 ++++++++++++ .../scripting/runtime_script_services.dart | 4 +- .../storage/runtime_storage_manager.dart | 94 +++++++++++++++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 lib/runtime/storage/runtime_storage_manager.dart diff --git a/lib/flame_lua_runtime.dart b/lib/flame_lua_runtime.dart index fb32c6b..3d8f0bd 100644 --- a/lib/flame_lua_runtime.dart +++ b/lib/flame_lua_runtime.dart @@ -21,3 +21,4 @@ export 'runtime/packages/game_package_repository.dart' export 'runtime/scripting/lua_dardo_script_engine.dart' show LuaDardoScriptEngine; export 'runtime/scripting/script_engine.dart' show ScriptEngine; +export 'runtime/storage/runtime_storage_manager.dart' show RuntimeStorageManager; diff --git a/lib/runtime/game/flame_lua_game.dart b/lib/runtime/game/flame_lua_game.dart index 2b77b2e..ee0a8f4 100644 --- a/lib/runtime/game/flame_lua_game.dart +++ b/lib/runtime/game/flame_lua_game.dart @@ -24,6 +24,7 @@ import '../display/runtime_viewport.dart'; import '../resources/game_resource_manager.dart'; import '../scripting/runtime_script_services.dart'; import '../scripting/script_engine.dart'; +import '../storage/runtime_storage_manager.dart'; import 'runtime_locale.dart'; import 'runtime_options.dart'; @@ -73,6 +74,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector { late final PositionComponent _viewportRoot; RuntimeNetworkManager? _network; RuntimeHostBridgeManager? _hostBridgeManager; + RuntimeStorageManager? _storage; RuntimeViewportConfig? _viewportConfig; late final CommandExecutor _commands; RuntimeSession? _session; @@ -117,6 +119,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector { 'initialized': true, 'images': _resources.imagesDebugJson(), 'audio': _audio.audioDebugJson(), + 'storage': _storage?.debugJson() ?? const {}, }; } @@ -142,6 +145,8 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector { diagnostics: diagnostics, ); _hostBridgeManager = hostBridgeManager; + final storage = await RuntimeStorageManager.create(gameId: gameId); + _storage = storage; final activation = await PackageActivationController( repository: _packageRepository, @@ -155,6 +160,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector { scriptServices: RuntimeScriptServices( network: network, hostBridge: hostBridgeManager, + storage: storage, ), store: StablePackageStore(runtimeOptions: runtimeOptions), assetFallback: AssetGamePackageRepository( diff --git a/lib/runtime/scripting/lua_dardo_script_engine.dart b/lib/runtime/scripting/lua_dardo_script_engine.dart index 6017397..0781b0b 100644 --- a/lib/runtime/scripting/lua_dardo_script_engine.dart +++ b/lib/runtime/scripting/lua_dardo_script_engine.dart @@ -8,6 +8,7 @@ import '../models/game_diff.dart'; import '../models/runtime_event.dart'; import '../network/runtime_network_manager.dart'; import '../packages/game_package.dart'; +import '../storage/runtime_storage_manager.dart'; import 'runtime_script_services.dart'; import 'script_engine.dart'; @@ -166,6 +167,18 @@ class LuaDardoScriptEngine implements ScriptEngine { _lua.pushDartFunction(_hostRespond); _lua.setField(-2, 'host_respond'); + _lua.pushDartFunction(_storageGet); + _lua.setField(-2, 'storage_get'); + + _lua.pushDartFunction(_storageSet); + _lua.setField(-2, 'storage_set'); + + _lua.pushDartFunction(_storageRemove); + _lua.setField(-2, 'storage_remove'); + + _lua.pushDartFunction(_storageClear); + _lua.setField(-2, 'storage_clear'); + _lua.setGlobal('runtime'); } @@ -272,6 +285,43 @@ class LuaDardoScriptEngine implements ScriptEngine { return 1; } + int _storageGet(LuaState lua) { + final storage = _requireStorage(); + final key = lua.toStr(1); + if (key == null || key.isEmpty) { + throw const FormatException('runtime.storage_get(key, defaultValue) requires key'); + } + final defaultValue = _readValue(2); + _pushValue(storage.getValue(key, defaultValue)); + return 1; + } + + int _storageSet(LuaState lua) { + final storage = _requireStorage(); + final key = lua.toStr(1); + if (key == null || key.isEmpty) { + throw const FormatException('runtime.storage_set(key, value) requires key'); + } + final value = _readValue(2); + lua.pushBoolean(storage.setValue(key, value)); + return 1; + } + + int _storageRemove(LuaState lua) { + final storage = _requireStorage(); + final key = lua.toStr(1); + if (key == null || key.isEmpty) { + throw const FormatException('runtime.storage_remove(key) requires key'); + } + lua.pushBoolean(storage.remove(key)); + return 1; + } + + int _storageClear(LuaState lua) { + lua.pushBoolean(_requireStorage().clear()); + return 1; + } + RuntimeHostBridgeManager _requireHostBridge() { final hostBridge = _services.hostBridge; if (hostBridge == null) { @@ -293,6 +343,14 @@ class LuaDardoScriptEngine implements ScriptEngine { return network; } + RuntimeStorageManager _requireStorage() { + final storage = _services.storage; + if (storage == null) { + throw StateError('Runtime storage service is not installed'); + } + return storage; + } + String _nextNetworkRequestId(String prefix) { _networkRequestCounter += 1; return '$prefix:$_networkRequestCounter'; diff --git a/lib/runtime/scripting/runtime_script_services.dart b/lib/runtime/scripting/runtime_script_services.dart index b683c81..ce10a7c 100644 --- a/lib/runtime/scripting/runtime_script_services.dart +++ b/lib/runtime/scripting/runtime_script_services.dart @@ -1,9 +1,11 @@ import '../host/runtime_host_bridge.dart'; import '../network/runtime_network_manager.dart'; +import '../storage/runtime_storage_manager.dart'; class RuntimeScriptServices { - const RuntimeScriptServices({this.network, this.hostBridge}); + const RuntimeScriptServices({this.network, this.hostBridge, this.storage}); final RuntimeNetworkManager? network; final RuntimeHostBridgeManager? hostBridge; + final RuntimeStorageManager? storage; } diff --git a/lib/runtime/storage/runtime_storage_manager.dart b/lib/runtime/storage/runtime_storage_manager.dart new file mode 100644 index 0000000..2c67d92 --- /dev/null +++ b/lib/runtime/storage/runtime_storage_manager.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class RuntimeStorageManager { + RuntimeStorageManager._(this._file, this._values); + + final File _file; + final Map _values; + + static Future create({required String gameId}) async { + final root = await getApplicationSupportDirectory(); + final directory = Directory(p.join(root.path, 'flame_lua_storage')); + if (!directory.existsSync()) { + directory.createSync(recursive: true); + } + + final file = File(p.join(directory.path, '$gameId.json')); + if (!file.existsSync()) { + return RuntimeStorageManager._(file, {}); + } + + try { + final raw = jsonDecode(file.readAsStringSync()); + if (raw is Map) { + return RuntimeStorageManager._( + file, + Map.from(raw), + ); + } + } catch (_) { + // Corrupt storage should not prevent a game from loading. + } + + return RuntimeStorageManager._(file, {}); + } + + Object? getValue(String key, [Object? defaultValue]) { + if (!_values.containsKey(key)) { + return defaultValue; + } + return _values[key]; + } + + bool setValue(String key, Object? value) { + _values[key] = _normalize(value); + _flush(); + return true; + } + + bool remove(String key) { + final removed = _values.remove(key) != null; + if (removed) { + _flush(); + } + return removed; + } + + bool clear() { + if (_values.isEmpty) { + return false; + } + _values.clear(); + _flush(); + return true; + } + + Map debugJson() => Map.from(_values); + + Object? _normalize(Object? value) { + if (value == null || value is bool || value is num || value is String) { + return value; + } + if (value is List) { + return value.map(_normalize).toList(growable: false); + } + if (value is Map) { + return { + for (final entry in value.entries) entry.key.toString(): _normalize(entry.value), + }; + } + return value.toString(); + } + + void _flush() { + final parent = _file.parent; + if (!parent.existsSync()) { + parent.createSync(recursive: true); + } + _file.writeAsStringSync(jsonEncode(_values)); + } +}