Initial flame_lua_runtime package
This commit is contained in:
377
lib/runtime/game/flame_lua_game.dart
Normal file
377
lib/runtime/game/flame_lua_game.dart
Normal file
@@ -0,0 +1,377 @@
|
||||
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 '../models/game_diff.dart';
|
||||
import '../models/runtime_event.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/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(),
|
||||
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 Locale? _localeOverride;
|
||||
|
||||
late final GameResourceManager _resources;
|
||||
late final RuntimeAudioManager _audio;
|
||||
late final RenderTreeController _renderTree;
|
||||
late final PositionComponent _viewportRoot;
|
||||
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();
|
||||
|
||||
Map<String, Object?> resourcesDebugJson() {
|
||||
if (!_runtimeInitialized) {
|
||||
return {'initialized': false};
|
||||
}
|
||||
return {
|
||||
'initialized': true,
|
||||
'images': _resources.imagesDebugJson(),
|
||||
'audio': _audio.audioDebugJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Color backgroundColor() => const Color(0xff0f172a);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final session = RuntimeSession(gameId: gameId)..beginLoading();
|
||||
_session = session;
|
||||
|
||||
try {
|
||||
final activation =
|
||||
await PackageActivationController(
|
||||
repository: _packageRepository,
|
||||
resources: _createResourceManager(),
|
||||
scriptEngine: _bootstrapScriptEngine,
|
||||
audio: _createAudioManager(),
|
||||
resourceManagerFactory: _createResourceManager,
|
||||
audioManagerFactory: _createAudioManager,
|
||||
scriptEngineFactory: _scriptEngineFactory,
|
||||
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) {
|
||||
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();
|
||||
_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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user