Initial flame_lua_runtime package
This commit is contained in:
231
lib/runtime/packages/game_package_activation_controller.dart
Normal file
231
lib/runtime/packages/game_package_activation_controller.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import '../audio/runtime_audio_manager.dart';
|
||||
import '../models/game_diff.dart';
|
||||
import '../resources/game_resource_manager.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,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
_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;
|
||||
}
|
||||
Reference in New Issue
Block a user