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

@@ -68,6 +68,28 @@ Package dependency root:
packages/flame_lua_runtime/assets/runtime/lua
```
### Multi-package loading
The runtime supports loading multiple packages in sequence, with module merging:
```text
RuntimeOptions.basePackages = ['_framework']
-> load _framework package (modules: app, diff, ids, net, ...)
-> load game package (modules: state, rules, main, ...)
-> merge into flat _moduleScripts map
-> game modules override framework modules on name collision
-> execute game entry script
```
Key classes:
- `RuntimeOptions.basePackages` — ordered list of framework package IDs.
- `PackageActivationController._prepareCandidate()` — loads base packages first, then game package, passes combined list to `ScriptEngine.loadPackages()`.
- `LuaDardoScriptEngine.loadPackages()` — iterates all packages, merges `manifest.modules` into `_moduleScripts`, executes entry from last package.
- `GamePackageManifest.base` — optional metadata field declaring framework dependency.
Module resolution is flat: `runtime.import("xxx")` looks up `_moduleScripts[xxx]`. Game modules and framework modules share the same namespace. Later-loaded packages win on collision.
## Safety model
- Lua module loading is manifest-declared.

View File

@@ -64,6 +64,53 @@ runtime:*.lua
`runtime:` paths must not contain `/`, `..`, or an empty filename.
## Base packages
A game manifest can declare a `base` field to indicate it depends on a framework package:
```json
{
"gameId": "ludo",
"base": "_framework",
"modules": { ... }
}
```
The `base` field is metadata. Actual loading is controlled by `RuntimeOptions.basePackages`:
```dart
LuaGameWidget(
gameId: 'ludo',
runtimeOptions: const RuntimeOptions(
basePackages: ['_framework'],
),
)
```
Loading order:
1. Base packages are loaded first, in `basePackages` order.
2. The game package is loaded last.
3. All modules are merged into a flat map.
4. Later packages override earlier packages on name collision.
5. The entry script always comes from the last (game) package.
This means a game can override any framework module by declaring a module with the same name in its own manifest.
## Multi-package module resolution
When Lua code calls `runtime.import("xxx")`:
1. Look up `xxx` in the merged module map (game modules first, then framework).
2. If not found, throw `FormatException: Lua module is not declared in manifest.modules`.
Framework modules are transparent to game code:
```lua
local app = runtime.import("app") -- resolved from framework
local state = runtime.import("state") -- resolved from game
```
## Entry module
The manifest `entry` module should expose lifecycle/event functions expected by the script engine. Keep game-specific state in Lua modules and return runtime diffs/commands through the approved protocol.

View File

@@ -98,3 +98,77 @@ flutter run --dart-define=LUA_GAME_ID=flight
}
}
```
## Multi-package loading (base + game)
When multiple games share common modules (event router, diff utilities, panel stack, network layer), extract them into a framework package and load it before the game package.
Framework package structure:
```text
assets/games/_framework/
manifest.json
scripts/
app.lua
diff.lua
event_router.lua
ids.lua
net.lua
panel_stack.lua
```
Framework manifest:
```json
{
"gameId": "_framework",
"name": "Lua Game Framework",
"version": "1.0.0",
"runtimeApiVersion": 1,
"entry": "scripts/app.lua",
"assetsBase": "assets",
"modules": {
"app": "scripts/app.lua",
"diff": "scripts/diff.lua",
"event_router": "scripts/event_router.lua",
"ids": "scripts/ids.lua",
"net": "scripts/net.lua",
"panel_stack": "scripts/panel_stack.lua"
}
}
```
Game manifest (no framework modules needed):
```json
{
"gameId": "ludo",
"base": "_framework",
"modules": {
"state": "scripts/state.lua",
"rules": "scripts/rules.lua",
"main": "scripts/main.lua"
}
}
```
Game code imports framework modules transparently:
```lua
local app = runtime.import("app") -- from framework
local state = runtime.import("state") -- from game
```
Embed with base packages:
```dart
LuaGameWidget(
gameId: 'ludo',
runtimeOptions: const RuntimeOptions(
runtimeLuaRoot: 'packages/flame_lua_runtime/assets/runtime/lua',
basePackages: ['_framework'],
),
)
```
Module resolution order: game package modules override framework modules with the same name. The entry script always comes from the last package (game package).