Files
flutter_lua_runtime/lib/runtime/packages/game_package_manifest.dart
gem 8ddc3be3a7 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
2026-06-10 00:04:00 +08:00

280 lines
8.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import '../display/runtime_viewport.dart';
class GamePackageManifest {
const GamePackageManifest({
required this.gameId,
required this.name,
required this.version,
required this.runtimeApiVersion,
required this.entry,
required this.assetsBase,
this.defaultLocale = 'en',
this.supportedLocales = const ['en'],
this.display = const GameDisplayConfig(),
this.resources = const {},
this.modules = const {},
this.base,
});
final String gameId;
final String name;
final String version;
final int runtimeApiVersion;
final String entry;
final String assetsBase;
final String defaultLocale;
final List<String> supportedLocales;
final GameDisplayConfig display;
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?>);
}
static GamePackageManifest fromMap(Map<String, Object?> map) {
final resourcesValue = map['resources'];
final resources = <String, GameResource>{};
if (resourcesValue is Map) {
for (final entry in resourcesValue.entries) {
if (entry.key is! String || entry.value is! Map) {
throw const FormatException('manifest.resources must be a map');
}
resources[entry.key as String] = GameResource.fromMap(
Map<String, Object?>.from(entry.value as Map),
);
}
}
final modulesValue = map['modules'];
final modules = <String, String>{};
if (modulesValue is Map) {
for (final entry in modulesValue.entries) {
if (entry.key is! String || entry.value is! String) {
throw const FormatException('manifest.modules must be a string map');
}
modules[entry.key as String] = entry.value as String;
}
}
final base = map['base'] as String?;
final defaultLocale = (map['defaultLocale'] as String?) ?? 'en';
final supportedLocales = _stringList(
map,
'supportedLocales',
fallback: [defaultLocale],
);
if (!supportedLocales.contains(defaultLocale)) {
throw const FormatException(
'manifest.supportedLocales must include defaultLocale',
);
}
final displayValue = map['display'];
final display = displayValue == null
? const GameDisplayConfig()
: GameDisplayConfig.fromMap(
Map<String, Object?>.from(displayValue as Map),
);
return GamePackageManifest(
gameId: _string(map, 'gameId'),
name: _string(map, 'name'),
version: _string(map, 'version'),
runtimeApiVersion: _int(map, 'runtimeApiVersion'),
entry: _string(map, 'entry'),
assetsBase: (map['assetsBase'] as String?) ?? 'assets',
defaultLocale: defaultLocale,
supportedLocales: supportedLocales,
display: display,
resources: resources,
modules: modules,
base: base,
);
}
static String _string(Map<String, Object?> map, String key) {
final value = map[key];
if (value is String && value.isNotEmpty) {
return value;
}
throw FormatException('manifest.$key must be a non-empty string');
}
static List<String> _stringList(
Map<String, Object?> map,
String key, {
required List<String> fallback,
}) {
final value = map[key];
if (value == null) {
return fallback;
}
if (value is! List || value.isEmpty) {
throw FormatException('manifest.$key must be a non-empty string list');
}
final result = <String>[];
for (final item in value) {
if (item is! String || item.isEmpty) {
throw FormatException('manifest.$key must be a non-empty string list');
}
result.add(item);
}
return result;
}
static int _int(Map<String, Object?> map, String key) {
final value = map[key];
if (value is num) {
return value.toInt();
}
throw FormatException('manifest.$key must be an integer');
}
}
class GameDisplayConfig {
const GameDisplayConfig({
this.designWidth = 720,
this.designHeight = 720,
this.scaleMode = RuntimeScaleMode.fit,
});
final double designWidth;
final double designHeight;
final String scaleMode;
RuntimeViewportConfig toViewportConfig() {
return RuntimeViewportConfig(
designWidth: designWidth,
designHeight: designHeight,
scaleMode: scaleMode,
);
}
static GameDisplayConfig fromMap(Map<String, Object?> map) {
final designWidth = _number(map, 'designWidth', fallback: 720);
final designHeight = _number(map, 'designHeight', fallback: 720);
final scaleMode = (map['scaleMode'] as String?) ?? RuntimeScaleMode.fit;
if (designWidth <= 0 || designHeight <= 0) {
throw const FormatException('manifest.display design size must be > 0');
}
if (!RuntimeScaleMode.isSupported(scaleMode)) {
throw const FormatException('manifest.display.scaleMode is unsupported');
}
return GameDisplayConfig(
designWidth: designWidth,
designHeight: designHeight,
scaleMode: scaleMode,
);
}
static double _number(
Map<String, Object?> map,
String key, {
required double fallback,
}) {
final value = map[key];
if (value == null) {
return fallback;
}
if (value is num) {
return value.toDouble();
}
throw FormatException('manifest.display.$key must be a number');
}
}
class GameResource {
const GameResource({
required this.type,
required this.path,
this.preload = GameResourcePreload.required,
this.group,
this.atlas,
this.skeleton,
});
final String type;
final String path;
final String preload;
final String? group;
final String? atlas;
final String? skeleton;
static GameResource fromMap(Map<String, Object?> map) {
final type = map['type'];
final path = map['path'];
final atlas = map['atlas'];
final skeleton = map['skeleton'];
if (type is! String || type.isEmpty) {
throw const FormatException('resource.type must be a non-empty string');
}
if (!GameResourceType.isSupported(type)) {
throw const FormatException('resource.type is unsupported');
}
if (type == GameResourceType.spine) {
if (atlas is! String || atlas.isEmpty) {
throw const FormatException(
'spine resource.atlas must be a non-empty string',
);
}
if (skeleton is! String || skeleton.isEmpty) {
throw const FormatException(
'spine resource.skeleton must be a non-empty string',
);
}
} else {
if (path is! String || path.isEmpty) {
throw const FormatException('resource.path must be a non-empty string');
}
if (atlas != null && (atlas is! String || atlas.isEmpty)) {
throw const FormatException(
'image resource.atlas must be a non-empty string',
);
}
}
final preload = map['preload'] as String? ?? GameResourcePreload.required;
if (!GameResourcePreload.isSupported(preload)) {
throw const FormatException('resource.preload is unsupported');
}
final group = map['group'];
if (group != null && (group is! String || group.isEmpty)) {
throw const FormatException('resource.group must be a non-empty string');
}
return GameResource(
type: type,
path: path as String? ?? '',
preload: preload,
group: group as String?,
atlas: atlas as String?,
skeleton: skeleton as String?,
);
}
}
abstract final class GameResourceType {
static const image = 'image';
static const audio = 'audio';
static const spine = 'spine';
static bool isSupported(String value) {
return value == image || value == audio || value == spine;
}
}
abstract final class GameResourcePreload {
static const required = 'required';
static const lazy = 'lazy';
static const optional = 'optional';
static bool isSupported(String value) {
return value == required || value == lazy || value == optional;
}
}