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 activate({ required String gameId, required Map 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 prepare({ required String gameId, required Map 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 commit( PackageActivationPlan plan, { bool Function()? shouldContinue, }) async { _ensureContinue(shouldContinue); await store.markStable(plan.package); _ensureContinue(shouldContinue); } Future> _candidatePackages( String gameId, bool Function()? shouldContinue, ) async { final candidates = []; 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 _prepareCandidate({ required GamePackage candidate, required PackageVerifier verifier, required Map 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 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; }