import 'dart:io'; import 'package:flutter/services.dart'; import '../game/runtime_options.dart'; import 'game_package_manifest.dart'; class GamePackage { const GamePackage.asset({ required this.rootPath, required this.manifest, this.runtimeLuaRoot = RuntimeOptions.defaultRuntimeLuaRoot, }) : source = GamePackageSource.asset; static const runtimeLuaPrefix = 'runtime:'; const GamePackage.file({ required this.rootPath, required this.manifest, this.runtimeLuaRoot = RuntimeOptions.defaultRuntimeLuaRoot, }) : source = GamePackageSource.file; final String rootPath; final GamePackageSource source; final GamePackageManifest manifest; final String runtimeLuaRoot; String get entryPath => _join(rootPath, manifest.entry); bool get isAsset => source == GamePackageSource.asset; Future readText(String relativeOrAbsolutePath) async { final runtimePath = _resolveRuntimeLuaPath(relativeOrAbsolutePath); if (runtimePath != null) { return rootBundle.loadString(runtimePath); } final path = _resolvePackagePath(relativeOrAbsolutePath); if (isAsset) { return rootBundle.loadString(path); } return File(path).readAsString(); } Future readBytes(String relativeOrAbsolutePath) async { final path = _resolvePackagePath(relativeOrAbsolutePath); if (isAsset) { return rootBundle.load(path); } final bytes = await File(path).readAsBytes(); return ByteData.sublistView(bytes); } String resolveResourcePath(String keyOrPath) { final resource = manifest.resources[keyOrPath]; if (resource != null) { return _join(rootPath, resource.path); } if (keyOrPath.startsWith(rootPath)) { return keyOrPath; } if (keyOrPath.contains('/')) { return _join(rootPath, keyOrPath); } return _join(_join(rootPath, manifest.assetsBase), keyOrPath); } String _resolvePackagePath(String relativeOrAbsolutePath) { if (relativeOrAbsolutePath.startsWith(rootPath)) { return relativeOrAbsolutePath; } return _join(rootPath, relativeOrAbsolutePath); } String? _resolveRuntimeLuaPath(String path) { if (!path.startsWith(runtimeLuaPrefix)) { return null; } final name = path.substring(runtimeLuaPrefix.length); if (name.isEmpty || name.contains('/') || name.contains('..')) { throw FormatException('Invalid runtime Lua module path: $path'); } return _join(runtimeLuaRoot, name); } String _join(String left, String right) { final normalizedLeft = left.endsWith('/') ? left.substring(0, left.length - 1) : left; final normalizedRight = right.startsWith('/') ? right.substring(1) : right; return '$normalizedLeft/$normalizedRight'; } } enum GamePackageSource { asset, file }