Files
flutter_lua_runtime/lib/runtime/packages/game_package_activation_controller.dart
gem 8ddc3be3a7 feat: multi-package loading with base framework support
- Add RuntimeOptions.basePackages for loading framework packages before game package
- Add ScriptEngine.loadPackages() for multi-package module merging
- LuaDardoScriptEngine merges modules from all packages, game overrides framework
- PackageActivationController loads base packages first, then game package
- GamePackageManifest parses optional 'base' field
- Update docs: README, quick-start, lua-package-format, architecture
- Update all test mocks with loadPackages() implementation
2026-06-10 00:04:00 +08:00

263 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../audio/runtime_audio_manager.dart';
import '../game/runtime_options.dart';
import '../models/game_diff.dart';
import '../resources/game_resource_manager.dart';
import '../scripting/runtime_script_services.dart';
import '../scripting/script_engine.dart';
import 'game_package.dart';
import 'game_package_repository.dart';
import 'package_verifier.dart';
import 'stable_package_store.dart';
class PackageActivationController {
const PackageActivationController({
required this.repository,
required this.resources,
required this.scriptEngine,
this.audio,
this.runtimeApiVersion = 1,
this.runtimeOptions = const RuntimeOptions(),
this.store = const StablePackageStore(),
this.assetFallback = const AssetGamePackageRepository(),
this.resourceManagerFactory,
this.audioManagerFactory,
this.scriptEngineFactory,
this.scriptServices = const RuntimeScriptServices(),
});
final GamePackageRepository repository;
final GameResourceManager resources;
final ScriptEngine scriptEngine;
final RuntimeAudioManager? audio;
final int runtimeApiVersion;
final RuntimeOptions runtimeOptions;
final StablePackageStore store;
final GamePackageRepository assetFallback;
final GameResourceManager Function()? resourceManagerFactory;
final RuntimeAudioManager Function()? audioManagerFactory;
final ScriptEngine Function()? scriptEngineFactory;
final RuntimeScriptServices scriptServices;
Future<PackageActivationResult> activate({
required String gameId,
required Map<String, Object?> Function(GamePackage package) contextBuilder,
bool Function()? shouldContinue,
}) async {
final plan = await prepare(
gameId: gameId,
contextBuilder: contextBuilder,
shouldContinue: shouldContinue,
);
await commit(plan, shouldContinue: shouldContinue);
return PackageActivationResult.fromPlan(plan);
}
Future<PackageActivationPlan> prepare({
required String gameId,
required Map<String, Object?> Function(GamePackage package) contextBuilder,
bool Function()? shouldContinue,
}) async {
final verifier = PackageVerifier(runtimeApiVersion: runtimeApiVersion);
final candidates = await _candidatePackages(gameId, shouldContinue);
Object? lastError;
for (final candidate in candidates) {
try {
_ensureContinue(shouldContinue);
final plan = await _prepareCandidate(
candidate: candidate,
verifier: verifier,
contextBuilder: contextBuilder,
shouldContinue: shouldContinue,
);
return plan;
} catch (error) {
if (shouldContinue != null && !shouldContinue()) {
rethrow;
}
lastError = error;
}
}
throw StateError(
'No activatable package for $gameId. Last error: $lastError',
);
}
Future<void> commit(
PackageActivationPlan plan, {
bool Function()? shouldContinue,
}) async {
_ensureContinue(shouldContinue);
await store.markStable(plan.package);
_ensureContinue(shouldContinue);
}
Future<List<GamePackage>> _candidatePackages(
String gameId,
bool Function()? shouldContinue,
) async {
final candidates = <GamePackage>[];
try {
final package = await repository.load(gameId);
_ensureContinue(shouldContinue);
candidates.add(package);
} catch (_) {
// Continue with stable/fallback candidates.
}
_ensureContinue(shouldContinue);
final stable = await store.stablePackage(gameId);
if (stable != null && !_containsPackage(candidates, stable)) {
candidates.add(stable);
}
_ensureContinue(shouldContinue);
final previous = await store.previousStablePackage(gameId);
if (previous != null && !_containsPackage(candidates, previous)) {
candidates.add(previous);
}
_ensureContinue(shouldContinue);
final fallback = await assetFallback.load(gameId);
if (!_containsPackage(candidates, fallback)) {
candidates.add(fallback);
}
_ensureContinue(shouldContinue);
return candidates;
}
Future<PackageActivationPlan> _prepareCandidate({
required GamePackage candidate,
required PackageVerifier verifier,
required Map<String, Object?> Function(GamePackage package) contextBuilder,
required bool Function()? shouldContinue,
}) async {
final preparedResources = resourceManagerFactory?.call() ?? resources;
final preparedAudio = audioManagerFactory?.call() ?? audio;
final preparedScriptEngine = scriptEngineFactory?.call() ?? scriptEngine;
final ownsPreparedResources = preparedResources != resources;
final ownsPreparedAudio = preparedAudio != null && preparedAudio != audio;
try {
await verifier.verify(candidate);
_ensureContinue(shouldContinue);
// 加载 base packages框架包按 runtimeOptions.basePackages 顺序。
final basePackages = <GamePackage>[];
for (final baseId in runtimeOptions.basePackages) {
final baseCandidates = await _candidatePackages(
baseId,
shouldContinue,
);
for (final baseCandidate in baseCandidates) {
try {
await verifier.verify(baseCandidate);
basePackages.add(baseCandidate);
break;
} catch (_) {
// Try next candidate.
}
}
}
await preparedResources.mount(candidate);
_ensureContinue(shouldContinue);
await preparedAudio?.mount(candidate);
_ensureContinue(shouldContinue);
// 合并 base + game 包,传给脚本引擎。
final allPackages = [...basePackages, candidate];
await preparedScriptEngine.loadPackages(
allPackages,
services: scriptServices,
);
_ensureContinue(shouldContinue);
final context = contextBuilder(candidate);
_ensureContinue(shouldContinue);
if (!preparedScriptEngine.smokeTest(context)) {
throw StateError('Lua package smoke_test returned false');
}
_ensureContinue(shouldContinue);
final diff = preparedScriptEngine.init(context);
_ensureContinue(shouldContinue);
return PackageActivationPlan(
package: candidate,
initialDiff: diff,
resources: preparedResources,
scriptEngine: preparedScriptEngine,
audio: preparedAudio,
);
} catch (_) {
if (ownsPreparedResources) {
preparedResources.dispose();
}
if (ownsPreparedAudio) {
preparedAudio.dispose();
}
rethrow;
}
}
void _ensureContinue(bool Function()? shouldContinue) {
if (shouldContinue != null && !shouldContinue()) {
throw StateError('Package activation cancelled');
}
}
bool _containsPackage(List<GamePackage> packages, GamePackage package) {
return packages.any(
(item) =>
item.source == package.source && item.rootPath == package.rootPath,
);
}
}
class PackageActivationPlan {
const PackageActivationPlan({
required this.package,
required this.initialDiff,
required this.resources,
required this.scriptEngine,
this.audio,
});
final GamePackage package;
final GameDiff initialDiff;
final GameResourceManager resources;
final ScriptEngine scriptEngine;
final RuntimeAudioManager? audio;
}
class PackageActivationResult {
const PackageActivationResult({
required this.package,
required this.initialDiff,
required this.resources,
required this.scriptEngine,
this.audio,
});
factory PackageActivationResult.fromPlan(PackageActivationPlan plan) {
return PackageActivationResult(
package: plan.package,
initialDiff: plan.initialDiff,
resources: plan.resources,
scriptEngine: plan.scriptEngine,
audio: plan.audio,
);
}
final GamePackage package;
final GameDiff initialDiff;
final GameResourceManager resources;
final ScriptEngine scriptEngine;
final RuntimeAudioManager? audio;
}