- 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
263 lines
7.8 KiB
Dart
263 lines
7.8 KiB
Dart
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;
|
||
}
|