feat: add Lua runtime storage API

This commit is contained in:
gem
2026-06-10 11:31:17 +08:00
parent 8ddc3be3a7
commit 6608d0a975
5 changed files with 162 additions and 1 deletions

View File

@@ -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;

View File

@@ -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(

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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<String, Object?> _values;
static Future<RuntimeStorageManager> 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, <String, Object?>{});
}
try {
final raw = jsonDecode(file.readAsStringSync());
if (raw is Map) {
return RuntimeStorageManager._(
file,
Map<String, Object?>.from(raw),
);
}
} catch (_) {
// Corrupt storage should not prevent a game from loading.
}
return RuntimeStorageManager._(file, <String, Object?>{});
}
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<String, Object?> debugJson() => Map<String, Object?>.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));
}
}