Initial flame_lua_runtime package
This commit is contained in:
265
lib/runtime/packages/game_package_manifest.dart
Normal file
265
lib/runtime/packages/game_package_manifest.dart
Normal file
@@ -0,0 +1,265 @@
|
||||
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 {},
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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 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,
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user