Initial flame_lua_runtime package

This commit is contained in:
gem
2026-06-07 22:53:58 +08:00
commit 733b2fb798
262 changed files with 28439 additions and 0 deletions

View 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');
}
}
}