437 lines
13 KiB
Dart
437 lines
13 KiB
Dart
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 '../storage/runtime_storage_manager.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;
|
|
RuntimeStorageManager? _storage;
|
|
RuntimeViewportConfig? _viewportConfig;
|
|
late final CommandExecutor _commands;
|
|
RuntimeSession? _session;
|
|
RuntimeEventDispatcher? _events;
|
|
String? _draggingListViewId;
|
|
bool _runtimeInitialized = false;
|
|
String? loadError;
|
|
|
|
List<RuntimeDiagnosticEntry> get diagnosticEntries => diagnostics.entries;
|
|
|
|
Map<String, Object?> diagnosticsDebugJson() => diagnostics.toDebugJson();
|
|
|
|
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() {
|
|
if (!_runtimeInitialized) {
|
|
return {'initialized': false};
|
|
}
|
|
return {
|
|
'initialized': true,
|
|
'images': _resources.imagesDebugJson(),
|
|
'audio': _audio.audioDebugJson(),
|
|
'storage': _storage?.debugJson() ?? const {},
|
|
};
|
|
}
|
|
|
|
@override
|
|
Color backgroundColor() => const Color(0xff0f172a);
|
|
|
|
@override
|
|
Future<void> 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 storage = await RuntimeStorageManager.create(gameId: gameId);
|
|
_storage = storage;
|
|
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,
|
|
storage: storage,
|
|
),
|
|
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<String, Object?> _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');
|
|
}
|
|
}
|
|
}
|