Initial flame_lua_runtime package

This commit is contained in:
gem
2026-06-07 22:53:58 +08:00
commit 733b2fb798
262 changed files with 28439 additions and 0 deletions

View 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;
}