feat: add Lua runtime storage API
This commit is contained in:
@@ -21,3 +21,4 @@ export 'runtime/packages/game_package_repository.dart'
|
|||||||
export 'runtime/scripting/lua_dardo_script_engine.dart'
|
export 'runtime/scripting/lua_dardo_script_engine.dart'
|
||||||
show LuaDardoScriptEngine;
|
show LuaDardoScriptEngine;
|
||||||
export 'runtime/scripting/script_engine.dart' show ScriptEngine;
|
export 'runtime/scripting/script_engine.dart' show ScriptEngine;
|
||||||
|
export 'runtime/storage/runtime_storage_manager.dart' show RuntimeStorageManager;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import '../display/runtime_viewport.dart';
|
|||||||
import '../resources/game_resource_manager.dart';
|
import '../resources/game_resource_manager.dart';
|
||||||
import '../scripting/runtime_script_services.dart';
|
import '../scripting/runtime_script_services.dart';
|
||||||
import '../scripting/script_engine.dart';
|
import '../scripting/script_engine.dart';
|
||||||
|
import '../storage/runtime_storage_manager.dart';
|
||||||
import 'runtime_locale.dart';
|
import 'runtime_locale.dart';
|
||||||
import 'runtime_options.dart';
|
import 'runtime_options.dart';
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
late final PositionComponent _viewportRoot;
|
late final PositionComponent _viewportRoot;
|
||||||
RuntimeNetworkManager? _network;
|
RuntimeNetworkManager? _network;
|
||||||
RuntimeHostBridgeManager? _hostBridgeManager;
|
RuntimeHostBridgeManager? _hostBridgeManager;
|
||||||
|
RuntimeStorageManager? _storage;
|
||||||
RuntimeViewportConfig? _viewportConfig;
|
RuntimeViewportConfig? _viewportConfig;
|
||||||
late final CommandExecutor _commands;
|
late final CommandExecutor _commands;
|
||||||
RuntimeSession? _session;
|
RuntimeSession? _session;
|
||||||
@@ -117,6 +119,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
'initialized': true,
|
'initialized': true,
|
||||||
'images': _resources.imagesDebugJson(),
|
'images': _resources.imagesDebugJson(),
|
||||||
'audio': _audio.audioDebugJson(),
|
'audio': _audio.audioDebugJson(),
|
||||||
|
'storage': _storage?.debugJson() ?? const {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +145,8 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
diagnostics: diagnostics,
|
diagnostics: diagnostics,
|
||||||
);
|
);
|
||||||
_hostBridgeManager = hostBridgeManager;
|
_hostBridgeManager = hostBridgeManager;
|
||||||
|
final storage = await RuntimeStorageManager.create(gameId: gameId);
|
||||||
|
_storage = storage;
|
||||||
final activation =
|
final activation =
|
||||||
await PackageActivationController(
|
await PackageActivationController(
|
||||||
repository: _packageRepository,
|
repository: _packageRepository,
|
||||||
@@ -155,6 +160,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
scriptServices: RuntimeScriptServices(
|
scriptServices: RuntimeScriptServices(
|
||||||
network: network,
|
network: network,
|
||||||
hostBridge: hostBridgeManager,
|
hostBridge: hostBridgeManager,
|
||||||
|
storage: storage,
|
||||||
),
|
),
|
||||||
store: StablePackageStore(runtimeOptions: runtimeOptions),
|
store: StablePackageStore(runtimeOptions: runtimeOptions),
|
||||||
assetFallback: AssetGamePackageRepository(
|
assetFallback: AssetGamePackageRepository(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../models/game_diff.dart';
|
|||||||
import '../models/runtime_event.dart';
|
import '../models/runtime_event.dart';
|
||||||
import '../network/runtime_network_manager.dart';
|
import '../network/runtime_network_manager.dart';
|
||||||
import '../packages/game_package.dart';
|
import '../packages/game_package.dart';
|
||||||
|
import '../storage/runtime_storage_manager.dart';
|
||||||
import 'runtime_script_services.dart';
|
import 'runtime_script_services.dart';
|
||||||
import 'script_engine.dart';
|
import 'script_engine.dart';
|
||||||
|
|
||||||
@@ -166,6 +167,18 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
_lua.pushDartFunction(_hostRespond);
|
_lua.pushDartFunction(_hostRespond);
|
||||||
_lua.setField(-2, 'host_respond');
|
_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');
|
_lua.setGlobal('runtime');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +285,43 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
return 1;
|
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() {
|
RuntimeHostBridgeManager _requireHostBridge() {
|
||||||
final hostBridge = _services.hostBridge;
|
final hostBridge = _services.hostBridge;
|
||||||
if (hostBridge == null) {
|
if (hostBridge == null) {
|
||||||
@@ -293,6 +343,14 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
return network;
|
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) {
|
String _nextNetworkRequestId(String prefix) {
|
||||||
_networkRequestCounter += 1;
|
_networkRequestCounter += 1;
|
||||||
return '$prefix:$_networkRequestCounter';
|
return '$prefix:$_networkRequestCounter';
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import '../host/runtime_host_bridge.dart';
|
import '../host/runtime_host_bridge.dart';
|
||||||
import '../network/runtime_network_manager.dart';
|
import '../network/runtime_network_manager.dart';
|
||||||
|
import '../storage/runtime_storage_manager.dart';
|
||||||
|
|
||||||
class RuntimeScriptServices {
|
class RuntimeScriptServices {
|
||||||
const RuntimeScriptServices({this.network, this.hostBridge});
|
const RuntimeScriptServices({this.network, this.hostBridge, this.storage});
|
||||||
|
|
||||||
final RuntimeNetworkManager? network;
|
final RuntimeNetworkManager? network;
|
||||||
final RuntimeHostBridgeManager? hostBridge;
|
final RuntimeHostBridgeManager? hostBridge;
|
||||||
|
final RuntimeStorageManager? storage;
|
||||||
}
|
}
|
||||||
|
|||||||
94
lib/runtime/storage/runtime_storage_manager.dart
Normal file
94
lib/runtime/storage/runtime_storage_manager.dart
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user