238 lines
7.0 KiB
Dart
238 lines
7.0 KiB
Dart
import '../audio/runtime_audio_manager.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.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 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);
|
|
await preparedResources.mount(candidate);
|
|
_ensureContinue(shouldContinue);
|
|
await preparedAudio?.mount(candidate);
|
|
_ensureContinue(shouldContinue);
|
|
await preparedScriptEngine.loadPackage(
|
|
candidate,
|
|
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;
|
|
}
|