feat: multi-package loading with base framework support

- 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
This commit is contained in:
gem
2026-06-10 00:04:00 +08:00
parent 0d4fbd030c
commit 8ddc3be3a7
13 changed files with 255 additions and 7 deletions

View File

@@ -148,6 +148,7 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
resources: _createResourceManager(),
scriptEngine: _bootstrapScriptEngine,
audio: _createAudioManager(),
runtimeOptions: runtimeOptions,
resourceManagerFactory: _createResourceManager,
audioManagerFactory: _createAudioManager,
scriptEngineFactory: _scriptEngineFactory,

View File

@@ -1,7 +1,14 @@
class RuntimeOptions {
const RuntimeOptions({this.runtimeLuaRoot = defaultRuntimeLuaRoot});
const RuntimeOptions({
this.runtimeLuaRoot = defaultRuntimeLuaRoot,
this.basePackages = const [],
});
static const defaultRuntimeLuaRoot = 'assets/runtime/lua';
final String runtimeLuaRoot;
// 框架包 gameId 列表,按顺序先于游戏包加载。
// 后加载的同名模块覆盖先加载的。
final List<String> basePackages;
}

View File

@@ -1,4 +1,5 @@
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';
@@ -15,6 +16,7 @@ class PackageActivationController {
required this.scriptEngine,
this.audio,
this.runtimeApiVersion = 1,
this.runtimeOptions = const RuntimeOptions(),
this.store = const StablePackageStore(),
this.assetFallback = const AssetGamePackageRepository(),
this.resourceManagerFactory,
@@ -28,6 +30,7 @@ class PackageActivationController {
final ScriptEngine scriptEngine;
final RuntimeAudioManager? audio;
final int runtimeApiVersion;
final RuntimeOptions runtimeOptions;
final StablePackageStore store;
final GamePackageRepository assetFallback;
final GameResourceManager Function()? resourceManagerFactory;
@@ -143,12 +146,34 @@ class PackageActivationController {
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);
await preparedScriptEngine.loadPackage(
candidate,
// 合并 base + game 包,传给脚本引擎。
final allPackages = [...basePackages, candidate];
await preparedScriptEngine.loadPackages(
allPackages,
services: scriptServices,
);
_ensureContinue(shouldContinue);

View File

@@ -15,6 +15,7 @@ class GamePackageManifest {
this.display = const GameDisplayConfig(),
this.resources = const {},
this.modules = const {},
this.base,
});
final String gameId;
@@ -29,6 +30,9 @@ class GamePackageManifest {
final Map<String, GameResource> resources;
final Map<String, String> modules;
/// 依赖的框架包 gameId。加载时会先加载框架包再加载游戏包。
final String? base;
static GamePackageManifest fromJsonString(String source) {
return fromMap(jsonDecode(source) as Map<String, Object?>);
}
@@ -58,6 +62,8 @@ class GamePackageManifest {
}
}
final base = map['base'] as String?;
final defaultLocale = (map['defaultLocale'] as String?) ?? 'en';
final supportedLocales = _stringList(
map,
@@ -89,6 +95,7 @@ class GamePackageManifest {
display: display,
resources: resources,
modules: modules,
base: base,
);
}

View File

@@ -27,15 +27,34 @@ class LuaDardoScriptEngine implements ScriptEngine {
Future<void> loadPackage(
GamePackage package, {
RuntimeScriptServices services = const RuntimeScriptServices(),
}) {
return loadPackages([package], services: services);
}
@override
Future<void> loadPackages(
List<GamePackage> packages, {
RuntimeScriptServices services = const RuntimeScriptServices(),
}) async {
if (packages.isEmpty) {
throw const FormatException('loadPackages requires at least one package');
}
_services = services;
_networkRequestCounter = 0;
_hostCallCounter = 0;
final script = await package.readText(package.manifest.entry);
_moduleScripts = {};
for (final entry in package.manifest.modules.entries) {
_moduleScripts[entry.key] = await package.readText(entry.value);
// 按顺序加载所有包的模块,后加载的同名模块覆盖先加载的。
for (final package in packages) {
for (final entry in package.manifest.modules.entries) {
_moduleScripts[entry.key] = await package.readText(entry.value);
}
}
// 入口脚本使用最后一个包(游戏包)。
final entryPackage = packages.last;
final script = await entryPackage.readText(entryPackage.manifest.entry);
_loadingModules.clear();
_lua = LuaState.newState();

View File

@@ -4,11 +4,19 @@ import '../packages/game_package.dart';
import 'runtime_script_services.dart';
abstract interface class ScriptEngine {
// 加载单个包(向后兼容,内部调 loadPackages([package]))。
Future<void> loadPackage(
GamePackage package, {
RuntimeScriptServices services = const RuntimeScriptServices(),
});
// 加载多个包,按顺序合并模块,后加载的同名模块覆盖先加载的。
// 入口脚本使用最后一个包。
Future<void> loadPackages(
List<GamePackage> packages, {
RuntimeScriptServices services = const RuntimeScriptServices(),
});
bool smokeTest(Map<String, Object?> context);
GameDiff init(Map<String, Object?> context);