Add bidirectional host bridge
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- Added TexturePacker frame, manual source-region, and nine-slice image rendering fields for image-capable nodes.
|
- Added TexturePacker frame, manual source-region, and nine-slice image rendering fields for image-capable nodes.
|
||||||
|
- Added Runtime host bridge APIs for bidirectional Flutter host calls and Lua notifications.
|
||||||
- Added Runtime Lua networking APIs for async HTTP/HTTPS requests and WS/WSS connections.
|
- Added Runtime Lua networking APIs for async HTTP/HTTPS requests and WS/WSS connections.
|
||||||
- Fixed nine-slice image seams by overlapping destination slices and using inset source sampling during runtime rendering.
|
- Fixed nine-slice image seams by overlapping destination slices and using inset source sampling during runtime rendering.
|
||||||
- Fixed Runtime alpha inheritance so parent fade commands apply to the full child subtree.
|
- Fixed Runtime alpha inheritance so parent fade commands apply to the full child subtree.
|
||||||
|
|||||||
@@ -92,6 +92,22 @@ Network results are delivered back to Lua through `on_event(event)`:
|
|||||||
- `network_ws_error`: WebSocket connection or stream error.
|
- `network_ws_error`: WebSocket connection or stream error.
|
||||||
- `network_ws_close`: WebSocket connection closed.
|
- `network_ws_close`: WebSocket connection closed.
|
||||||
|
|
||||||
|
## Runtime host bridge
|
||||||
|
|
||||||
|
Flutter host apps may register a `RuntimeHostBridge` when creating `LuaGameWidget` or `FlameLuaGame`.
|
||||||
|
|
||||||
|
Lua-to-Flutter calls:
|
||||||
|
|
||||||
|
- `runtime.host_call({ id?, method, data? })`: async request. Result is delivered to Lua as `host_call_result` with `id`, `method`, `ok`, and either `result` or `error`.
|
||||||
|
- `runtime.host_notify({ method, data? })`: fire-and-forget notification to Flutter host code.
|
||||||
|
|
||||||
|
Flutter-to-Lua calls:
|
||||||
|
|
||||||
|
- `FlameLuaGame.notifyLua(method, data?)`: emits a `host_notify` event into Lua.
|
||||||
|
- `FlameLuaGame.callLua(method, data?, timeout?)`: emits a `host_call` event into Lua and waits for Lua to call `runtime.host_respond({ id, result?, error? })`.
|
||||||
|
|
||||||
|
Host bridge payloads must be JSON-like values: null, bool, number, string, list, or string-keyed map. Unsupported Dart objects are converted to strings.
|
||||||
|
|
||||||
## RuntimeCommand
|
## RuntimeCommand
|
||||||
|
|
||||||
Runtime commands request generic side effects owned by Dart/Flame.
|
Runtime commands request generic side effects owned by Dart/Flame.
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
---| 'network_ws_message'
|
---| 'network_ws_message'
|
||||||
---| 'network_ws_error'
|
---| 'network_ws_error'
|
||||||
---| 'network_ws_close'
|
---| 'network_ws_close'
|
||||||
|
---| 'host_notify'
|
||||||
|
---| 'host_call'
|
||||||
|
---| 'host_call_result'
|
||||||
|
|
||||||
---@alias RuntimeScaleMode
|
---@alias RuntimeScaleMode
|
||||||
---| 'fit'
|
---| 'fit'
|
||||||
@@ -598,6 +601,20 @@
|
|||||||
---@field url string ws/wss URL.
|
---@field url string ws/wss URL.
|
||||||
---@field protocols? string[]
|
---@field protocols? string[]
|
||||||
|
|
||||||
|
---@class RuntimeHostCallOptions
|
||||||
|
---@field id? string
|
||||||
|
---@field method string Host method name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostNotifyOptions
|
||||||
|
---@field method string Host notification name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostRespondOptions
|
||||||
|
---@field id string Host-to-Lua call id from host_call event.
|
||||||
|
---@field result? any
|
||||||
|
---@field error? string
|
||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
---@field log fun(...: any)
|
---@field log fun(...: any)
|
||||||
@@ -605,6 +622,9 @@
|
|||||||
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
||||||
---@field ws_send fun(id: string, message: string): boolean
|
---@field ws_send fun(id: string, message: string): boolean
|
||||||
---@field ws_close fun(id: string): boolean
|
---@field ws_close fun(id: string): boolean
|
||||||
|
---@field host_call fun(options: RuntimeHostCallOptions): string Starts an async Lua-to-Flutter host call. Result event type: host_call_result.
|
||||||
|
---@field host_notify fun(options: RuntimeHostNotifyOptions): boolean Sends a fire-and-forget notification to Flutter host code.
|
||||||
|
---@field host_respond fun(options: RuntimeHostRespondOptions): boolean Completes a Flutter-to-Lua host_call event.
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
---| 'network_ws_message'
|
---| 'network_ws_message'
|
||||||
---| 'network_ws_error'
|
---| 'network_ws_error'
|
||||||
---| 'network_ws_close'
|
---| 'network_ws_close'
|
||||||
|
---| 'host_notify'
|
||||||
|
---| 'host_call'
|
||||||
|
---| 'host_call_result'
|
||||||
|
|
||||||
---@alias RuntimeScaleMode
|
---@alias RuntimeScaleMode
|
||||||
---| 'fit'
|
---| 'fit'
|
||||||
@@ -598,6 +601,20 @@
|
|||||||
---@field url string ws/wss URL.
|
---@field url string ws/wss URL.
|
||||||
---@field protocols? string[]
|
---@field protocols? string[]
|
||||||
|
|
||||||
|
---@class RuntimeHostCallOptions
|
||||||
|
---@field id? string
|
||||||
|
---@field method string Host method name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostNotifyOptions
|
||||||
|
---@field method string Host notification name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostRespondOptions
|
||||||
|
---@field id string Host-to-Lua call id from host_call event.
|
||||||
|
---@field result? any
|
||||||
|
---@field error? string
|
||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
---@field log fun(...: any)
|
---@field log fun(...: any)
|
||||||
@@ -605,6 +622,9 @@
|
|||||||
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
||||||
---@field ws_send fun(id: string, message: string): boolean
|
---@field ws_send fun(id: string, message: string): boolean
|
||||||
---@field ws_close fun(id: string): boolean
|
---@field ws_close fun(id: string): boolean
|
||||||
|
---@field host_call fun(options: RuntimeHostCallOptions): string Starts an async Lua-to-Flutter host call. Result event type: host_call_result.
|
||||||
|
---@field host_notify fun(options: RuntimeHostNotifyOptions): boolean Sends a fire-and-forget notification to Flutter host code.
|
||||||
|
---@field host_respond fun(options: RuntimeHostRespondOptions): boolean Completes a Flutter-to-Lua host_call event.
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
---| 'network_ws_message'
|
---| 'network_ws_message'
|
||||||
---| 'network_ws_error'
|
---| 'network_ws_error'
|
||||||
---| 'network_ws_close'
|
---| 'network_ws_close'
|
||||||
|
---| 'host_notify'
|
||||||
|
---| 'host_call'
|
||||||
|
---| 'host_call_result'
|
||||||
|
|
||||||
---@alias RuntimeScaleMode
|
---@alias RuntimeScaleMode
|
||||||
---| 'fit'
|
---| 'fit'
|
||||||
@@ -598,6 +601,20 @@
|
|||||||
---@field url string ws/wss URL.
|
---@field url string ws/wss URL.
|
||||||
---@field protocols? string[]
|
---@field protocols? string[]
|
||||||
|
|
||||||
|
---@class RuntimeHostCallOptions
|
||||||
|
---@field id? string
|
||||||
|
---@field method string Host method name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostNotifyOptions
|
||||||
|
---@field method string Host notification name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostRespondOptions
|
||||||
|
---@field id string Host-to-Lua call id from host_call event.
|
||||||
|
---@field result? any
|
||||||
|
---@field error? string
|
||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
---@field log fun(...: any)
|
---@field log fun(...: any)
|
||||||
@@ -605,6 +622,9 @@
|
|||||||
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
||||||
---@field ws_send fun(id: string, message: string): boolean
|
---@field ws_send fun(id: string, message: string): boolean
|
||||||
---@field ws_close fun(id: string): boolean
|
---@field ws_close fun(id: string): boolean
|
||||||
|
---@field host_call fun(options: RuntimeHostCallOptions): string Starts an async Lua-to-Flutter host call. Result event type: host_call_result.
|
||||||
|
---@field host_notify fun(options: RuntimeHostNotifyOptions): boolean Sends a fire-and-forget notification to Flutter host code.
|
||||||
|
---@field host_respond fun(options: RuntimeHostRespondOptions): boolean Completes a Flutter-to-Lua host_call event.
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
---| 'network_ws_message'
|
---| 'network_ws_message'
|
||||||
---| 'network_ws_error'
|
---| 'network_ws_error'
|
||||||
---| 'network_ws_close'
|
---| 'network_ws_close'
|
||||||
|
---| 'host_notify'
|
||||||
|
---| 'host_call'
|
||||||
|
---| 'host_call_result'
|
||||||
|
|
||||||
---@alias RuntimeScaleMode
|
---@alias RuntimeScaleMode
|
||||||
---| 'fit'
|
---| 'fit'
|
||||||
@@ -598,6 +601,20 @@
|
|||||||
---@field url string ws/wss URL.
|
---@field url string ws/wss URL.
|
||||||
---@field protocols? string[]
|
---@field protocols? string[]
|
||||||
|
|
||||||
|
---@class RuntimeHostCallOptions
|
||||||
|
---@field id? string
|
||||||
|
---@field method string Host method name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostNotifyOptions
|
||||||
|
---@field method string Host notification name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostRespondOptions
|
||||||
|
---@field id string Host-to-Lua call id from host_call event.
|
||||||
|
---@field result? any
|
||||||
|
---@field error? string
|
||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
---@field log fun(...: any)
|
---@field log fun(...: any)
|
||||||
@@ -605,6 +622,9 @@
|
|||||||
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
||||||
---@field ws_send fun(id: string, message: string): boolean
|
---@field ws_send fun(id: string, message: string): boolean
|
||||||
---@field ws_close fun(id: string): boolean
|
---@field ws_close fun(id: string): boolean
|
||||||
|
---@field host_call fun(options: RuntimeHostCallOptions): string Starts an async Lua-to-Flutter host call. Result event type: host_call_result.
|
||||||
|
---@field host_notify fun(options: RuntimeHostNotifyOptions): boolean Sends a fire-and-forget notification to Flutter host code.
|
||||||
|
---@field host_respond fun(options: RuntimeHostRespondOptions): boolean Completes a Flutter-to-Lua host_call event.
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ export 'runtime/game/flame_lua_game.dart' show FlameLuaGame;
|
|||||||
export 'runtime/game/lua_game_widget.dart' show LuaGameWidget;
|
export 'runtime/game/lua_game_widget.dart' show LuaGameWidget;
|
||||||
export 'runtime/game/runtime_locale.dart' show RuntimeLocaleResolver;
|
export 'runtime/game/runtime_locale.dart' show RuntimeLocaleResolver;
|
||||||
export 'runtime/game/runtime_options.dart' show RuntimeOptions;
|
export 'runtime/game/runtime_options.dart' show RuntimeOptions;
|
||||||
|
export 'runtime/host/runtime_host_bridge.dart'
|
||||||
|
show
|
||||||
|
RuntimeHostBridge,
|
||||||
|
RuntimeHostBridgeManager,
|
||||||
|
RuntimeHostCall,
|
||||||
|
RuntimeHostCallHandler,
|
||||||
|
RuntimeHostEventType,
|
||||||
|
RuntimeHostNotification,
|
||||||
|
RuntimeHostNotifyHandler;
|
||||||
export 'runtime/packages/game_package_repository.dart'
|
export 'runtime/packages/game_package_repository.dart'
|
||||||
show
|
show
|
||||||
AssetGamePackageRepository,
|
AssetGamePackageRepository,
|
||||||
|
|||||||
@@ -137,4 +137,5 @@ enum RuntimeDiagnosticType {
|
|||||||
resourceLoadError,
|
resourceLoadError,
|
||||||
commandError,
|
commandError,
|
||||||
networkError,
|
networkError,
|
||||||
|
hostBridgeError,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../commands/command_executor.dart';
|
|||||||
import '../diagnostics/runtime_diagnostics.dart';
|
import '../diagnostics/runtime_diagnostics.dart';
|
||||||
import '../events/runtime_event_dispatcher.dart';
|
import '../events/runtime_event_dispatcher.dart';
|
||||||
import '../lifecycle/runtime_session.dart';
|
import '../lifecycle/runtime_session.dart';
|
||||||
|
import '../host/runtime_host_bridge.dart';
|
||||||
import '../models/game_diff.dart';
|
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';
|
||||||
@@ -41,6 +42,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
this.audioMaxConcurrentLoads = 4,
|
this.audioMaxConcurrentLoads = 4,
|
||||||
this.audioSfxPoolSize = 8,
|
this.audioSfxPoolSize = 8,
|
||||||
this.runtimeOptions = const RuntimeOptions(),
|
this.runtimeOptions = const RuntimeOptions(),
|
||||||
|
this.hostBridge = const RuntimeHostBridge(),
|
||||||
Locale? localeOverride,
|
Locale? localeOverride,
|
||||||
}) : _bootstrapScriptEngine = scriptEngine,
|
}) : _bootstrapScriptEngine = scriptEngine,
|
||||||
_localeOverride = localeOverride,
|
_localeOverride = localeOverride,
|
||||||
@@ -62,6 +64,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
final int audioMaxConcurrentLoads;
|
final int audioMaxConcurrentLoads;
|
||||||
final int audioSfxPoolSize;
|
final int audioSfxPoolSize;
|
||||||
final RuntimeOptions runtimeOptions;
|
final RuntimeOptions runtimeOptions;
|
||||||
|
final RuntimeHostBridge hostBridge;
|
||||||
final Locale? _localeOverride;
|
final Locale? _localeOverride;
|
||||||
|
|
||||||
late final GameResourceManager _resources;
|
late final GameResourceManager _resources;
|
||||||
@@ -69,6 +72,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
late final RenderTreeController _renderTree;
|
late final RenderTreeController _renderTree;
|
||||||
late final PositionComponent _viewportRoot;
|
late final PositionComponent _viewportRoot;
|
||||||
RuntimeNetworkManager? _network;
|
RuntimeNetworkManager? _network;
|
||||||
|
RuntimeHostBridgeManager? _hostBridgeManager;
|
||||||
RuntimeViewportConfig? _viewportConfig;
|
RuntimeViewportConfig? _viewportConfig;
|
||||||
late final CommandExecutor _commands;
|
late final CommandExecutor _commands;
|
||||||
RuntimeSession? _session;
|
RuntimeSession? _session;
|
||||||
@@ -83,6 +87,28 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
|
|
||||||
String diagnosticsDumpText() => diagnostics.dumpText();
|
String diagnosticsDumpText() => diagnostics.dumpText();
|
||||||
|
|
||||||
|
Future<Object?> callLua(
|
||||||
|
String method, {
|
||||||
|
Object? data,
|
||||||
|
Duration timeout = const Duration(seconds: 15),
|
||||||
|
}) {
|
||||||
|
final hostBridgeManager = _hostBridgeManager;
|
||||||
|
if (!_runtimeInitialized || hostBridgeManager == null) {
|
||||||
|
return Future<Object?>.error(
|
||||||
|
StateError('Lua runtime is not initialized'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return hostBridgeManager.callLua(method, data: data, timeout: timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notifyLua(String method, {Object? data}) {
|
||||||
|
final hostBridgeManager = _hostBridgeManager;
|
||||||
|
if (!_runtimeInitialized || hostBridgeManager == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return hostBridgeManager.notifyLua(method, data: data);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object?> resourcesDebugJson() {
|
Map<String, Object?> resourcesDebugJson() {
|
||||||
if (!_runtimeInitialized) {
|
if (!_runtimeInitialized) {
|
||||||
return {'initialized': false};
|
return {'initialized': false};
|
||||||
@@ -110,6 +136,12 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
diagnostics: diagnostics,
|
diagnostics: diagnostics,
|
||||||
);
|
);
|
||||||
_network = network;
|
_network = network;
|
||||||
|
final hostBridgeManager = RuntimeHostBridgeManager(
|
||||||
|
bridge: hostBridge,
|
||||||
|
eventSink: _emitEvent,
|
||||||
|
diagnostics: diagnostics,
|
||||||
|
);
|
||||||
|
_hostBridgeManager = hostBridgeManager;
|
||||||
final activation =
|
final activation =
|
||||||
await PackageActivationController(
|
await PackageActivationController(
|
||||||
repository: _packageRepository,
|
repository: _packageRepository,
|
||||||
@@ -119,7 +151,10 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
resourceManagerFactory: _createResourceManager,
|
resourceManagerFactory: _createResourceManager,
|
||||||
audioManagerFactory: _createAudioManager,
|
audioManagerFactory: _createAudioManager,
|
||||||
scriptEngineFactory: _scriptEngineFactory,
|
scriptEngineFactory: _scriptEngineFactory,
|
||||||
scriptServices: RuntimeScriptServices(network: network),
|
scriptServices: RuntimeScriptServices(
|
||||||
|
network: network,
|
||||||
|
hostBridge: hostBridgeManager,
|
||||||
|
),
|
||||||
store: StablePackageStore(runtimeOptions: runtimeOptions),
|
store: StablePackageStore(runtimeOptions: runtimeOptions),
|
||||||
assetFallback: AssetGamePackageRepository(
|
assetFallback: AssetGamePackageRepository(
|
||||||
runtimeOptions: runtimeOptions,
|
runtimeOptions: runtimeOptions,
|
||||||
@@ -168,6 +203,8 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
_runtimeInitialized = true;
|
_runtimeInitialized = true;
|
||||||
_applyDiff(activation.initialDiff);
|
_applyDiff(activation.initialDiff);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
_hostBridgeManager?.dispose();
|
||||||
|
_hostBridgeManager = null;
|
||||||
_network?.dispose();
|
_network?.dispose();
|
||||||
_network = null;
|
_network = null;
|
||||||
session.dispose();
|
session.dispose();
|
||||||
@@ -347,6 +384,8 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
|||||||
_events?.dispose();
|
_events?.dispose();
|
||||||
if (_runtimeInitialized) {
|
if (_runtimeInitialized) {
|
||||||
_commands.dispose();
|
_commands.dispose();
|
||||||
|
_hostBridgeManager?.dispose();
|
||||||
|
_hostBridgeManager = null;
|
||||||
_network?.dispose();
|
_network?.dispose();
|
||||||
_network = null;
|
_network = null;
|
||||||
_renderTree.clear();
|
_renderTree.clear();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flame/game.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../diagnostics/runtime_diagnostics.dart';
|
import '../diagnostics/runtime_diagnostics.dart';
|
||||||
|
import '../host/runtime_host_bridge.dart';
|
||||||
import '../packages/game_package_repository.dart';
|
import '../packages/game_package_repository.dart';
|
||||||
import '../scripting/lua_dardo_script_engine.dart';
|
import '../scripting/lua_dardo_script_engine.dart';
|
||||||
import 'flame_lua_game.dart';
|
import 'flame_lua_game.dart';
|
||||||
@@ -14,6 +15,7 @@ class LuaGameWidget extends StatelessWidget {
|
|||||||
this.serverUrl,
|
this.serverUrl,
|
||||||
this.localeOverride,
|
this.localeOverride,
|
||||||
this.runtimeOptions = const RuntimeOptions(),
|
this.runtimeOptions = const RuntimeOptions(),
|
||||||
|
this.hostBridge = const RuntimeHostBridge(),
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ class LuaGameWidget extends StatelessWidget {
|
|||||||
final Uri? serverUrl;
|
final Uri? serverUrl;
|
||||||
final Locale? localeOverride;
|
final Locale? localeOverride;
|
||||||
final RuntimeOptions runtimeOptions;
|
final RuntimeOptions runtimeOptions;
|
||||||
|
final RuntimeHostBridge hostBridge;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -42,6 +45,7 @@ class LuaGameWidget extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
gameId: gameId,
|
gameId: gameId,
|
||||||
runtimeOptions: runtimeOptions,
|
runtimeOptions: runtimeOptions,
|
||||||
|
hostBridge: hostBridge,
|
||||||
localeOverride: localeOverride,
|
localeOverride: localeOverride,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
228
lib/runtime/host/runtime_host_bridge.dart
Normal file
228
lib/runtime/host/runtime_host_bridge.dart
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import 'dart:async' as async;
|
||||||
|
|
||||||
|
import '../diagnostics/runtime_diagnostics.dart';
|
||||||
|
import '../models/runtime_event.dart';
|
||||||
|
|
||||||
|
typedef RuntimeHostCallHandler =
|
||||||
|
async.FutureOr<Object?> Function(RuntimeHostCall call);
|
||||||
|
typedef RuntimeHostNotifyHandler =
|
||||||
|
void Function(RuntimeHostNotification notification);
|
||||||
|
|
||||||
|
class RuntimeHostBridge {
|
||||||
|
const RuntimeHostBridge({this.handlers = const {}, this.onNotify});
|
||||||
|
|
||||||
|
final Map<String, RuntimeHostCallHandler> handlers;
|
||||||
|
final RuntimeHostNotifyHandler? onNotify;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RuntimeHostCall {
|
||||||
|
const RuntimeHostCall({required this.id, required this.method, this.data});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String method;
|
||||||
|
final Object? data;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RuntimeHostNotification {
|
||||||
|
const RuntimeHostNotification({required this.method, this.data});
|
||||||
|
|
||||||
|
final String method;
|
||||||
|
final Object? data;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RuntimeHostBridgeManager {
|
||||||
|
RuntimeHostBridgeManager({
|
||||||
|
required RuntimeHostBridge bridge,
|
||||||
|
required void Function(RuntimeEvent event) eventSink,
|
||||||
|
RuntimeDiagnostics? diagnostics,
|
||||||
|
}) : _bridge = bridge,
|
||||||
|
_eventSink = eventSink,
|
||||||
|
_diagnostics = diagnostics;
|
||||||
|
|
||||||
|
final RuntimeHostBridge _bridge;
|
||||||
|
final void Function(RuntimeEvent event) _eventSink;
|
||||||
|
final RuntimeDiagnostics? _diagnostics;
|
||||||
|
final Map<String, async.Completer<Object?>> _pendingLuaCalls = {};
|
||||||
|
var _nextCallId = 0;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
|
Future<void> callHost(RuntimeHostCall call) async {
|
||||||
|
if (_disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final handler = _bridge.handlers[call.method];
|
||||||
|
if (handler == null) {
|
||||||
|
_emitHostCallResult(
|
||||||
|
id: call.id,
|
||||||
|
method: call.method,
|
||||||
|
ok: false,
|
||||||
|
error: 'No host handler registered for ${call.method}',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await handler(call);
|
||||||
|
_emitHostCallResult(
|
||||||
|
id: call.id,
|
||||||
|
method: call.method,
|
||||||
|
ok: true,
|
||||||
|
result: result,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
_diagnostics?.record(
|
||||||
|
type: RuntimeDiagnosticType.hostBridgeError,
|
||||||
|
message: 'Runtime host call failed',
|
||||||
|
error: error,
|
||||||
|
context: {'id': call.id, 'method': call.method},
|
||||||
|
);
|
||||||
|
_emitHostCallResult(
|
||||||
|
id: call.id,
|
||||||
|
method: call.method,
|
||||||
|
ok: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notifyHost(RuntimeHostNotification notification) {
|
||||||
|
if (_disposed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final handler = _bridge.onNotify;
|
||||||
|
if (handler == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
handler(notification);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
_diagnostics?.record(
|
||||||
|
type: RuntimeDiagnosticType.hostBridgeError,
|
||||||
|
message: 'Runtime host notification failed',
|
||||||
|
error: error,
|
||||||
|
context: {'method': notification.method},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object?> callLua(
|
||||||
|
String method, {
|
||||||
|
Object? data,
|
||||||
|
Duration timeout = const Duration(seconds: 15),
|
||||||
|
}) {
|
||||||
|
if (_disposed) {
|
||||||
|
return Future<Object?>.error(StateError('Runtime host bridge disposed'));
|
||||||
|
}
|
||||||
|
final id = 'host:${++_nextCallId}';
|
||||||
|
final completer = async.Completer<Object?>();
|
||||||
|
_pendingLuaCalls[id] = completer;
|
||||||
|
_emit(
|
||||||
|
RuntimeEvent(
|
||||||
|
type: RuntimeHostEventType.call,
|
||||||
|
data: {
|
||||||
|
'id': id,
|
||||||
|
'method': method,
|
||||||
|
if (data != null) 'data': _runtimeValue(data),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return completer.future.timeout(
|
||||||
|
timeout,
|
||||||
|
onTimeout: () {
|
||||||
|
_pendingLuaCalls.remove(id);
|
||||||
|
throw async.TimeoutException(
|
||||||
|
'Lua host call timed out: $method',
|
||||||
|
timeout,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notifyLua(String method, {Object? data}) {
|
||||||
|
if (_disposed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_emit(
|
||||||
|
RuntimeEvent(
|
||||||
|
type: RuntimeHostEventType.notify,
|
||||||
|
data: {'method': method, if (data != null) 'data': _runtimeValue(data)},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool completeLuaCall(String id, {Object? result, String? error}) {
|
||||||
|
final completer = _pendingLuaCalls.remove(id);
|
||||||
|
if (completer == null || completer.isCompleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (error != null) {
|
||||||
|
completer.completeError(StateError(error));
|
||||||
|
} else {
|
||||||
|
completer.complete(_runtimeValue(result));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_disposed = true;
|
||||||
|
for (final completer in _pendingLuaCalls.values) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(StateError('Runtime host bridge disposed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_pendingLuaCalls.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitHostCallResult({
|
||||||
|
required String id,
|
||||||
|
required String method,
|
||||||
|
required bool ok,
|
||||||
|
Object? result,
|
||||||
|
String? error,
|
||||||
|
}) {
|
||||||
|
_emit(
|
||||||
|
RuntimeEvent(
|
||||||
|
type: RuntimeHostEventType.callResult,
|
||||||
|
data: {
|
||||||
|
'id': id,
|
||||||
|
'method': method,
|
||||||
|
'ok': ok,
|
||||||
|
if (ok) 'result': _runtimeValue(result),
|
||||||
|
if (!ok && error != null) 'error': error,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emit(RuntimeEvent event) {
|
||||||
|
if (_disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_eventSink(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _runtimeValue(Object? value) {
|
||||||
|
if (value == null || value is String || value is num || value is bool) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is Iterable) {
|
||||||
|
return value.map(_runtimeValue).toList(growable: false);
|
||||||
|
}
|
||||||
|
if (value is Map) {
|
||||||
|
return {
|
||||||
|
for (final entry in value.entries)
|
||||||
|
entry.key.toString(): _runtimeValue(entry.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract final class RuntimeHostEventType {
|
||||||
|
static const notify = 'host_notify';
|
||||||
|
static const call = 'host_call';
|
||||||
|
static const callResult = 'host_call_result';
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:async' as async;
|
|||||||
import 'package:lua_dardo_plus/lua.dart';
|
import 'package:lua_dardo_plus/lua.dart';
|
||||||
|
|
||||||
import '../diagnostics/runtime_diagnostics.dart';
|
import '../diagnostics/runtime_diagnostics.dart';
|
||||||
|
import '../host/runtime_host_bridge.dart';
|
||||||
import '../models/game_diff.dart';
|
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';
|
||||||
@@ -19,6 +20,7 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
late final Map<String, String> _moduleScripts;
|
late final Map<String, String> _moduleScripts;
|
||||||
RuntimeScriptServices _services = const RuntimeScriptServices();
|
RuntimeScriptServices _services = const RuntimeScriptServices();
|
||||||
int _networkRequestCounter = 0;
|
int _networkRequestCounter = 0;
|
||||||
|
int _hostCallCounter = 0;
|
||||||
final Set<String> _loadingModules = {};
|
final Set<String> _loadingModules = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,6 +30,7 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
}) async {
|
}) async {
|
||||||
_services = services;
|
_services = services;
|
||||||
_networkRequestCounter = 0;
|
_networkRequestCounter = 0;
|
||||||
|
_hostCallCounter = 0;
|
||||||
final script = await package.readText(package.manifest.entry);
|
final script = await package.readText(package.manifest.entry);
|
||||||
_moduleScripts = {};
|
_moduleScripts = {};
|
||||||
for (final entry in package.manifest.modules.entries) {
|
for (final entry in package.manifest.modules.entries) {
|
||||||
@@ -135,6 +138,15 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
_lua.pushDartFunction(_wsClose);
|
_lua.pushDartFunction(_wsClose);
|
||||||
_lua.setField(-2, 'ws_close');
|
_lua.setField(-2, 'ws_close');
|
||||||
|
|
||||||
|
_lua.pushDartFunction(_hostCall);
|
||||||
|
_lua.setField(-2, 'host_call');
|
||||||
|
|
||||||
|
_lua.pushDartFunction(_hostNotify);
|
||||||
|
_lua.setField(-2, 'host_notify');
|
||||||
|
|
||||||
|
_lua.pushDartFunction(_hostRespond);
|
||||||
|
_lua.setField(-2, 'host_respond');
|
||||||
|
|
||||||
_lua.setGlobal('runtime');
|
_lua.setGlobal('runtime');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +216,56 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _hostCall(LuaState lua) {
|
||||||
|
final host = _requireHostBridge();
|
||||||
|
final options = _requiredMapArgument(1, 'runtime.host_call(options)');
|
||||||
|
final method = _requiredString(options, 'method');
|
||||||
|
final id = _optionalString(options, 'id') ?? _nextHostCallId();
|
||||||
|
async.unawaited(
|
||||||
|
host.callHost(
|
||||||
|
RuntimeHostCall(id: id, method: method, data: options['data']),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
lua.pushString(id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _hostNotify(LuaState lua) {
|
||||||
|
final host = _requireHostBridge();
|
||||||
|
final options = _requiredMapArgument(1, 'runtime.host_notify(options)');
|
||||||
|
final method = _requiredString(options, 'method');
|
||||||
|
lua.pushBoolean(
|
||||||
|
host.notifyHost(
|
||||||
|
RuntimeHostNotification(method: method, data: options['data']),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _hostRespond(LuaState lua) {
|
||||||
|
final host = _requireHostBridge();
|
||||||
|
final options = _requiredMapArgument(1, 'runtime.host_respond(options)');
|
||||||
|
final id = _requiredString(options, 'id');
|
||||||
|
final error = _optionalString(options, 'error');
|
||||||
|
lua.pushBoolean(
|
||||||
|
host.completeLuaCall(id, result: options['result'], error: error),
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeHostBridgeManager _requireHostBridge() {
|
||||||
|
final hostBridge = _services.hostBridge;
|
||||||
|
if (hostBridge == null) {
|
||||||
|
throw StateError('Runtime host bridge service is not installed');
|
||||||
|
}
|
||||||
|
return hostBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _nextHostCallId() {
|
||||||
|
_hostCallCounter += 1;
|
||||||
|
return 'lua:$_hostCallCounter';
|
||||||
|
}
|
||||||
|
|
||||||
RuntimeNetworkManager _requireNetwork() {
|
RuntimeNetworkManager _requireNetwork() {
|
||||||
final network = _services.network;
|
final network = _services.network;
|
||||||
if (network == null) {
|
if (network == null) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import '../host/runtime_host_bridge.dart';
|
||||||
import '../network/runtime_network_manager.dart';
|
import '../network/runtime_network_manager.dart';
|
||||||
|
|
||||||
class RuntimeScriptServices {
|
class RuntimeScriptServices {
|
||||||
const RuntimeScriptServices({this.network});
|
const RuntimeScriptServices({this.network, this.hostBridge});
|
||||||
|
|
||||||
final RuntimeNetworkManager? network;
|
final RuntimeNetworkManager? network;
|
||||||
|
final RuntimeHostBridgeManager? hostBridge;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('FlameLuaGame diagnostics debug access', () {
|
group('FlameLuaGame diagnostics debug access', () {
|
||||||
test('exposes diagnostics entries, dump text and debug json', () {
|
test('exposes diagnostics entries, dump text and debug json', () async {
|
||||||
final diagnostics = RuntimeDiagnostics()
|
final diagnostics = RuntimeDiagnostics()
|
||||||
..record(
|
..record(
|
||||||
type: RuntimeDiagnosticType.commandError,
|
type: RuntimeDiagnosticType.commandError,
|
||||||
@@ -29,6 +29,8 @@ void main() {
|
|||||||
expect(game.diagnosticsDumpText(), contains('command failed'));
|
expect(game.diagnosticsDumpText(), contains('command failed'));
|
||||||
expect(game.diagnosticsDebugJson()['count'], 1);
|
expect(game.diagnosticsDebugJson()['count'], 1);
|
||||||
expect(game.resourcesDebugJson(), {'initialized': false});
|
expect(game.resourcesDebugJson(), {'initialized': false});
|
||||||
|
expect(game.notifyLua('host.ready'), isFalse);
|
||||||
|
await expectLater(game.callLua('host.ready'), throwsA(isA<StateError>()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
93
test/runtime/host/runtime_host_bridge_test.dart
Normal file
93
test/runtime/host/runtime_host_bridge_test.dart
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||||
|
import 'package:flame_lua_runtime/runtime/host/runtime_host_bridge.dart';
|
||||||
|
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('RuntimeHostBridgeManager', () {
|
||||||
|
test('calls registered host handler and emits result event', () async {
|
||||||
|
final events = <RuntimeEvent>[];
|
||||||
|
final manager = RuntimeHostBridgeManager(
|
||||||
|
bridge: RuntimeHostBridge(
|
||||||
|
handlers: {
|
||||||
|
'user.profile': (call) => {'name': 'Lua', 'id': call.data},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
eventSink: events.add,
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.callHost(
|
||||||
|
const RuntimeHostCall(id: 'call_1', method: 'user.profile', data: 7),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(events.single.type, RuntimeHostEventType.callResult);
|
||||||
|
expect(events.single.data['ok'], isTrue);
|
||||||
|
expect(events.single.data['result'], {'name': 'Lua', 'id': 7});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'emits failed result and diagnostics when host handler throws',
|
||||||
|
() async {
|
||||||
|
final diagnostics = RuntimeDiagnostics();
|
||||||
|
final events = <RuntimeEvent>[];
|
||||||
|
final manager = RuntimeHostBridgeManager(
|
||||||
|
bridge: RuntimeHostBridge(
|
||||||
|
handlers: {'boom': (_) => throw StateError('boom')},
|
||||||
|
),
|
||||||
|
eventSink: events.add,
|
||||||
|
diagnostics: diagnostics,
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.callHost(
|
||||||
|
const RuntimeHostCall(id: 'call_1', method: 'boom'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(events.single.data['ok'], isFalse);
|
||||||
|
expect(events.single.data['error'], contains('boom'));
|
||||||
|
expect(
|
||||||
|
diagnostics.entries.single.type,
|
||||||
|
RuntimeDiagnosticType.hostBridgeError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('notifies host and emits Lua calls', () async {
|
||||||
|
RuntimeHostNotification? notification;
|
||||||
|
final events = <RuntimeEvent>[];
|
||||||
|
final manager = RuntimeHostBridgeManager(
|
||||||
|
bridge: RuntimeHostBridge(onNotify: (value) => notification = value),
|
||||||
|
eventSink: events.add,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
manager.notifyHost(
|
||||||
|
const RuntimeHostNotification(
|
||||||
|
method: 'analytics',
|
||||||
|
data: {'level': 2},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(notification?.method, 'analytics');
|
||||||
|
expect(manager.notifyLua('pause', data: {'reason': 'host'}), isTrue);
|
||||||
|
|
||||||
|
expect(events.single.type, RuntimeHostEventType.notify);
|
||||||
|
expect(events.single.data['method'], 'pause');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('completes Flutter-to-Lua call through host response', () async {
|
||||||
|
final events = <RuntimeEvent>[];
|
||||||
|
final manager = RuntimeHostBridgeManager(
|
||||||
|
bridge: const RuntimeHostBridge(),
|
||||||
|
eventSink: events.add,
|
||||||
|
);
|
||||||
|
|
||||||
|
final future = manager.callLua('select_avatar', data: {'current': 1});
|
||||||
|
final id = events.single.data['id']! as String;
|
||||||
|
expect(events.single.type, RuntimeHostEventType.call);
|
||||||
|
expect(manager.completeLuaCall(id, result: {'selected': 3}), isTrue);
|
||||||
|
|
||||||
|
expect(await future, {'selected': 3});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||||
|
import 'package:flame_lua_runtime/runtime/host/runtime_host_bridge.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/network/runtime_network_manager.dart';
|
import 'package:flame_lua_runtime/runtime/network/runtime_network_manager.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
|
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
|
||||||
@@ -1021,6 +1022,57 @@ function on_event(event) return {} end
|
|||||||
expect(network.closedWebSockets, ['chat']);
|
expect(network.closedWebSockets, ['chat']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('exposes host bridge runtime API to Lua', () async {
|
||||||
|
final package = await _createPackage(
|
||||||
|
mainScript: '''
|
||||||
|
function smoke_test(ctx) return true end
|
||||||
|
function init(ctx)
|
||||||
|
local id = runtime.host_call({
|
||||||
|
id = "profile",
|
||||||
|
method = "user.profile",
|
||||||
|
data = { userId = 9 },
|
||||||
|
})
|
||||||
|
local notified = runtime.host_notify({
|
||||||
|
method = "analytics",
|
||||||
|
data = { event = "open" },
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
commands = {
|
||||||
|
{ type = "toast", text = id .. ":" .. tostring(notified) },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
function on_event(event)
|
||||||
|
if event.type == "host_call" then
|
||||||
|
runtime.host_respond({ id = event.data.id, result = { handled = event.data.method } })
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
final hostBridge = _RecordingHostBridgeManager();
|
||||||
|
final engine = LuaDardoScriptEngine();
|
||||||
|
|
||||||
|
await engine.loadPackage(
|
||||||
|
package,
|
||||||
|
services: RuntimeScriptServices(hostBridge: hostBridge),
|
||||||
|
);
|
||||||
|
final diff = engine.init({'runtimeApiVersion': 1});
|
||||||
|
|
||||||
|
expect(diff.commands.single.payload['text'], 'profile:true');
|
||||||
|
expect(hostBridge.calls.single.method, 'user.profile');
|
||||||
|
expect(hostBridge.calls.single.data, {'userId': 9});
|
||||||
|
expect(hostBridge.notifications.single.method, 'analytics');
|
||||||
|
expect(hostBridge.notifications.single.data, {'event': 'open'});
|
||||||
|
|
||||||
|
final callFuture = hostBridge.callLua('flutter.request', data: {'id': 2});
|
||||||
|
final event = _RecordingHostBridgeManager.events.singleWhere(
|
||||||
|
(item) => item.type == RuntimeHostEventType.call,
|
||||||
|
);
|
||||||
|
engine.dispatchEvent(event);
|
||||||
|
expect(await callFuture, {'handled': 'flutter.request'});
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects undeclared module imports', () async {
|
test('rejects undeclared module imports', () async {
|
||||||
final package = await _createPackage(
|
final package = await _createPackage(
|
||||||
mainScript: '''
|
mainScript: '''
|
||||||
@@ -1052,6 +1104,26 @@ function on_event(event) return {} end
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RecordingHostBridgeManager extends RuntimeHostBridgeManager {
|
||||||
|
_RecordingHostBridgeManager()
|
||||||
|
: super(bridge: const RuntimeHostBridge(), eventSink: events.add);
|
||||||
|
|
||||||
|
static final events = <RuntimeEvent>[];
|
||||||
|
final calls = <RuntimeHostCall>[];
|
||||||
|
final notifications = <RuntimeHostNotification>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> callHost(RuntimeHostCall call) async {
|
||||||
|
calls.add(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool notifyHost(RuntimeHostNotification notification) {
|
||||||
|
notifications.add(notification);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RecordingNetworkManager extends RuntimeNetworkManager {
|
class _RecordingNetworkManager extends RuntimeNetworkManager {
|
||||||
_RecordingNetworkManager()
|
_RecordingNetworkManager()
|
||||||
: super(
|
: super(
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
---| 'network_ws_message'
|
---| 'network_ws_message'
|
||||||
---| 'network_ws_error'
|
---| 'network_ws_error'
|
||||||
---| 'network_ws_close'
|
---| 'network_ws_close'
|
||||||
|
---| 'host_notify'
|
||||||
|
---| 'host_call'
|
||||||
|
---| 'host_call_result'
|
||||||
|
|
||||||
---@alias RuntimeScaleMode
|
---@alias RuntimeScaleMode
|
||||||
---| 'fit'
|
---| 'fit'
|
||||||
@@ -598,6 +601,20 @@
|
|||||||
---@field url string ws/wss URL.
|
---@field url string ws/wss URL.
|
||||||
---@field protocols? string[]
|
---@field protocols? string[]
|
||||||
|
|
||||||
|
---@class RuntimeHostCallOptions
|
||||||
|
---@field id? string
|
||||||
|
---@field method string Host method name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostNotifyOptions
|
||||||
|
---@field method string Host notification name registered by Flutter.
|
||||||
|
---@field data? any
|
||||||
|
|
||||||
|
---@class RuntimeHostRespondOptions
|
||||||
|
---@field id string Host-to-Lua call id from host_call event.
|
||||||
|
---@field result? any
|
||||||
|
---@field error? string
|
||||||
|
|
||||||
---@class RuntimeImportApi
|
---@class RuntimeImportApi
|
||||||
---@field import fun(moduleName: string): table
|
---@field import fun(moduleName: string): table
|
||||||
---@field log fun(...: any)
|
---@field log fun(...: any)
|
||||||
@@ -605,6 +622,9 @@
|
|||||||
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
---@field ws_connect fun(options: RuntimeWsConnectOptions): string Opens a WebSocket and returns connection id. Event types: network_ws_open/network_ws_message/network_ws_error/network_ws_close.
|
||||||
---@field ws_send fun(id: string, message: string): boolean
|
---@field ws_send fun(id: string, message: string): boolean
|
||||||
---@field ws_close fun(id: string): boolean
|
---@field ws_close fun(id: string): boolean
|
||||||
|
---@field host_call fun(options: RuntimeHostCallOptions): string Starts an async Lua-to-Flutter host call. Result event type: host_call_result.
|
||||||
|
---@field host_notify fun(options: RuntimeHostNotifyOptions): boolean Sends a fire-and-forget notification to Flutter host code.
|
||||||
|
---@field host_respond fun(options: RuntimeHostRespondOptions): boolean Completes a Flutter-to-Lua host_call event.
|
||||||
|
|
||||||
---@type RuntimeImportApi
|
---@type RuntimeImportApi
|
||||||
runtime = runtime
|
runtime = runtime
|
||||||
|
|||||||
Reference in New Issue
Block a user