import 'dart:ui' show PlatformDispatcher; import 'package:flame/components.dart'; import 'package:flame/events.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import '../audio/runtime_audio_manager.dart'; import '../commands/command_executor.dart'; import '../diagnostics/runtime_diagnostics.dart'; import '../events/runtime_event_dispatcher.dart'; import '../lifecycle/runtime_session.dart'; import '../host/runtime_host_bridge.dart'; import '../models/game_diff.dart'; import '../models/runtime_event.dart'; import '../network/runtime_network_manager.dart'; import '../packages/game_package.dart'; import '../packages/game_package_activation_controller.dart'; import '../packages/game_package_repository.dart'; import '../packages/stable_package_store.dart'; import '../protocol/runtime_protocol.dart'; import '../rendering/render_tree_controller.dart'; import '../display/runtime_viewport.dart'; import '../resources/game_resource_manager.dart'; import '../scripting/runtime_script_services.dart'; import '../scripting/script_engine.dart'; import 'runtime_locale.dart'; import 'runtime_options.dart'; class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector { FlameLuaGame({ required ScriptEngine scriptEngine, ScriptEngine Function()? scriptEngineFactory, required GamePackageRepository packageRepository, required this.gameId, RuntimeDiagnostics? diagnostics, this.imageCacheMaxBytes, this.imageCacheMaxEntries, this.imageMaxConcurrentLoads = 4, this.audioCacheMaxBytes, this.audioCacheMaxEntries, this.audioMaxConcurrentLoads = 4, this.audioSfxPoolSize = 8, this.runtimeOptions = const RuntimeOptions(), this.hostBridge = const RuntimeHostBridge(), Locale? localeOverride, }) : _bootstrapScriptEngine = scriptEngine, _localeOverride = localeOverride, _scriptEngineFactory = scriptEngineFactory, _packageRepository = packageRepository, diagnostics = diagnostics ?? RuntimeDiagnostics(); final ScriptEngine _bootstrapScriptEngine; final ScriptEngine Function()? _scriptEngineFactory; late ScriptEngine _scriptEngine; final GamePackageRepository _packageRepository; final String gameId; final RuntimeDiagnostics diagnostics; final int? imageCacheMaxBytes; final int? imageCacheMaxEntries; final int imageMaxConcurrentLoads; final int? audioCacheMaxBytes; final int? audioCacheMaxEntries; final int audioMaxConcurrentLoads; final int audioSfxPoolSize; final RuntimeOptions runtimeOptions; final RuntimeHostBridge hostBridge; final Locale? _localeOverride; late final GameResourceManager _resources; late final RuntimeAudioManager _audio; late final RenderTreeController _renderTree; late final PositionComponent _viewportRoot; RuntimeNetworkManager? _network; RuntimeHostBridgeManager? _hostBridgeManager; RuntimeViewportConfig? _viewportConfig; late final CommandExecutor _commands; RuntimeSession? _session; RuntimeEventDispatcher? _events; String? _draggingListViewId; bool _runtimeInitialized = false; String? loadError; List get diagnosticEntries => diagnostics.entries; Map diagnosticsDebugJson() => diagnostics.toDebugJson(); String diagnosticsDumpText() => diagnostics.dumpText(); Future callLua( String method, { Object? data, Duration timeout = const Duration(seconds: 15), }) { final hostBridgeManager = _hostBridgeManager; if (!_runtimeInitialized || hostBridgeManager == null) { return Future.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 resourcesDebugJson() { if (!_runtimeInitialized) { return {'initialized': false}; } return { 'initialized': true, 'images': _resources.imagesDebugJson(), 'audio': _audio.audioDebugJson(), }; } @override Color backgroundColor() => const Color(0xff0f172a); @override Future onLoad() async { await super.onLoad(); final session = RuntimeSession(gameId: gameId)..beginLoading(); _session = session; try { final network = RuntimeNetworkManager( eventSink: _emitEvent, diagnostics: diagnostics, ); _network = network; final hostBridgeManager = RuntimeHostBridgeManager( bridge: hostBridge, eventSink: _emitEvent, diagnostics: diagnostics, ); _hostBridgeManager = hostBridgeManager; final activation = await PackageActivationController( repository: _packageRepository, resources: _createResourceManager(), scriptEngine: _bootstrapScriptEngine, audio: _createAudioManager(), runtimeOptions: runtimeOptions, resourceManagerFactory: _createResourceManager, audioManagerFactory: _createAudioManager, scriptEngineFactory: _scriptEngineFactory, scriptServices: RuntimeScriptServices( network: network, hostBridge: hostBridgeManager, ), store: StablePackageStore(runtimeOptions: runtimeOptions), assetFallback: AssetGamePackageRepository( runtimeOptions: runtimeOptions, ), ).activate( gameId: gameId, contextBuilder: _buildContext, shouldContinue: () => session.acceptsWork, ); if (!session.acceptsWork) { activation.resources.dispose(); activation.audio?.dispose(); return; } session.activate(); _resources = activation.resources; _audio = activation.audio ?? _createAudioManager(); _scriptEngine = activation.scriptEngine; _viewportConfig = activation.package.manifest.display.toViewportConfig(); _viewportRoot = PositionComponent(); add(_viewportRoot); _applyViewportTransform(); _renderTree = RenderTreeController( root: _viewportRoot, resources: _resources, eventSink: _emitEvent, ); _commands = CommandExecutor( renderTree: _renderTree, eventSink: _emitEvent, audio: _audio, resources: _resources, overlaySize: _viewportConfig?.designSize, ); _renderTree.onScopeRemoved = _commands.cancelScope; _events = RuntimeEventDispatcher( session: session, scriptEngine: _scriptEngine, isScopeAlive: _renderTree.contains, isNodeEpochAlive: _renderTree.isNodeEpochAlive, applyDiff: _applyDiff, diagnostics: diagnostics, onError: (error) => debugPrint('Lua event failed: $error'), ); _runtimeInitialized = true; _applyDiff(activation.initialDiff); } catch (error) { _hostBridgeManager?.dispose(); _hostBridgeManager = null; _network?.dispose(); _network = null; session.dispose(); loadError = error.toString(); diagnostics.record( type: RuntimeDiagnosticType.packageActivationError, message: 'Lua game package activation failed', error: error, context: {'gameId': gameId}, ); debugPrint('Lua game load failed: $error'); } } GameResourceManager _createResourceManager() { return GameResourceManager( diagnostics: diagnostics, maxCacheBytes: imageCacheMaxBytes, maxCacheEntries: imageCacheMaxEntries, maxConcurrentLoads: imageMaxConcurrentLoads, ); } RuntimeAudioManager _createAudioManager() { return RuntimeAudioManager( diagnostics: diagnostics, maxSfxPoolSize: audioSfxPoolSize, maxCacheBytes: audioCacheMaxBytes, maxCacheEntries: audioCacheMaxEntries, maxConcurrentLoads: audioMaxConcurrentLoads, ); } Map _buildContext(GamePackage package) { final display = package.manifest.display; final viewport = RuntimeViewport.compute( screenSize: size, config: display.toViewportConfig(), ); final locale = RuntimeLocaleResolver.resolve( requested: _localeOverride ?? PlatformDispatcher.instance.locale, defaultLocale: package.manifest.defaultLocale, supportedLocales: package.manifest.supportedLocales, ); return { 'screen': {'width': size.x, 'height': size.y}, 'design': {'width': display.designWidth, 'height': display.designHeight}, 'viewport': viewport.toMap(), 'seed': DateTime.now().millisecondsSinceEpoch, 'runtimeApiVersion': 1, 'gameId': package.manifest.gameId, 'gameVersion': package.manifest.version, 'locale': locale.toMap(), }; } void _emitEvent(RuntimeEvent event) { final session = _session; if (session == null || !session.isActive) { return; } _events?.enqueue(event.withLifecycle(sessionId: session.id)); } @override void onScroll(PointerScrollInfo info) { if (!_runtimeInitialized) { return; } _renderTree.scrollListViewAt( info.eventPosition.widget, deltaX: info.scrollDelta.global.x, deltaY: info.scrollDelta.global.y, source: 'wheel', ); } @override void onPanStart(DragStartInfo info) { if (!_runtimeInitialized) { _draggingListViewId = null; return; } _draggingListViewId = _renderTree.listViewAt(info.eventPosition.widget); final id = _draggingListViewId; if (id != null) { _renderTree.stopListViewVelocity(id); info.handled = true; } } @override void onPanUpdate(DragUpdateInfo info) { final id = _draggingListViewId; if (!_runtimeInitialized || id == null) { return; } final consumed = _renderTree.scrollListView( id, deltaX: -info.delta.global.x, deltaY: -info.delta.global.y, source: 'drag', ); if (consumed) { info.handled = true; } } @override void onPanEnd(DragEndInfo info) { final id = _draggingListViewId; if (id != null) { _renderTree.setListViewVelocity( id, Vector2(-info.velocity.x, -info.velocity.y), ); info.handled = true; } _draggingListViewId = null; } @override void onPanCancel() { _draggingListViewId = null; } @override void update(double dt) { super.update(dt); if (_runtimeInitialized) { _renderTree.updateListViewInertia(dt); } } @override void onGameResize(Vector2 size) { super.onGameResize(size); if (_runtimeInitialized) { _applyViewportTransform(); _emitResizeEvent(); } } void _emitResizeEvent() { final config = _viewportConfig; if (config == null) { return; } final viewport = RuntimeViewport.compute(screenSize: size, config: config); _emitEvent( RuntimeEvent( type: RuntimeEventType.resize, data: { 'screen': {'width': size.x, 'height': size.y}, 'viewport': viewport.toMap(), }, ), ); } void _applyViewportTransform() { final config = _viewportConfig; if (config == null) { return; } RuntimeViewport.apply( _viewportRoot, RuntimeViewport.compute(screenSize: size, config: config), ); } @override void onRemove() { _draggingListViewId = null; _session?.beginDisposing(); _events?.dispose(); if (_runtimeInitialized) { _commands.dispose(); _hostBridgeManager?.dispose(); _hostBridgeManager = null; _network?.dispose(); _network = null; _renderTree.clear(); _audio.dispose(); _resources.dispose(); } _session?.dispose(); super.onRemove(); } void _applyDiff(GameDiff diff) { final session = _session; if (session == null || !session.isActive) { return; } try { _renderTree ..apply(diff.render) ..apply(diff.ui); } catch (error) { diagnostics.record( type: RuntimeDiagnosticType.diffApplyError, message: 'Runtime diff apply failed', error: error, ); debugPrint('Runtime diff apply failed: $error'); return; } try { _commands.executeAll(diff.commands); } catch (error) { diagnostics.record( type: RuntimeDiagnosticType.commandError, message: 'Runtime command execution failed', error: error, ); debugPrint('Runtime command execution failed: $error'); } } }