Files
flutter_lua_runtime/lib/runtime/resources/game_resource_manager.dart
2026-06-07 22:53:58 +08:00

280 lines
7.8 KiB
Dart

import 'dart:ui' as ui;
import 'package:flame_spine/flame_spine.dart';
import '../diagnostics/runtime_diagnostics.dart';
import '../lifecycle/runtime_async_gate.dart';
import '../packages/game_package.dart';
import '../packages/game_package_manifest.dart';
import 'resource_load_limiter.dart';
// These part files only group GameResourceManager private helpers. The public
// facade stays in GameResourceManager so callers do not depend on extensions.
part 'game_resource_loading.dart';
part 'game_resource_debug.dart';
part 'game_resource_cache.dart';
class GameResourceManager {
GameResourceManager({
RuntimeDiagnostics? diagnostics,
int? maxCacheBytes,
int? maxCacheEntries,
int maxConcurrentLoads = 4,
}) : _diagnostics = diagnostics,
_maxCacheBytes = maxCacheBytes,
_maxCacheEntries = maxCacheEntries,
_loadLimiter = ResourceLoadLimiter(maxConcurrentLoads);
final RuntimeDiagnostics? _diagnostics;
final int? _maxCacheBytes;
final int? _maxCacheEntries;
final ResourceLoadLimiter _loadLimiter;
final RuntimeAsyncGate _asyncGate = RuntimeAsyncGate(initiallyClosed: true);
GamePackage? _package;
final Map<String, _ImageResourceRecord> _images = {};
int _cacheBytes = 0;
int _accessCounter = 0;
int get generation => _asyncGate.generation;
bool get hasPackage => _package != null;
GamePackage get package {
final value = _package;
if (value == null) {
throw StateError('GameResourceManager has no active package');
}
return value;
}
Future<void> mount(GamePackage package) async {
_releaseCachedImages();
_asyncGate.activate();
_package = package;
await preloadDeclaredImages(package.manifest);
await preloadDeclaredSpines(package.manifest);
}
void dispose() {
_asyncGate.close();
_releaseCachedImages();
_package = null;
}
String resolve(String keyOrPath) {
return package.resolveResourcePath(keyOrPath);
}
GameResourceState imageState(String keyOrPath) {
final path = _tryResolve(keyOrPath);
if (path == null) {
return GameResourceState.failed;
}
return _images[path]?.state ?? GameResourceState.idle;
}
Object? imageError(String keyOrPath) {
final path = _tryResolve(keyOrPath);
if (path == null) {
return StateError('GameResourceManager has no active package');
}
return _images[path]?.lastError;
}
Map<String, Object?> imagesDebugJson() {
final activePackage = _package;
final declaredPaths = <String>{};
final resources = <Map<String, Object?>>[];
if (activePackage != null) {
for (final entry in activePackage.manifest.resources.entries) {
final resource = entry.value;
if (resource.type != GameResourceType.image) {
continue;
}
final path = activePackage.resolveResourcePath(entry.key);
declaredPaths.add(path);
resources.add(
_imageRecordDebugJson(
key: entry.key,
path: path,
preload: resource.preload,
declared: true,
),
);
}
}
for (final path in _images.keys) {
if (declaredPaths.contains(path)) {
continue;
}
resources.add(
_imageRecordDebugJson(
key: null,
path: path,
preload: null,
declared: false,
),
);
}
return {
'generation': generation,
'hasPackage': activePackage != null,
'count': resources.length,
'activeLoads': _loadLimiter.activeCount,
'pendingLoads': _loadLimiter.pendingCount,
'resources': resources,
};
}
bool evictImage(String keyOrPath) {
final path = _tryResolve(keyOrPath);
if (path == null) {
return false;
}
return _removeImageRecord(path);
}
Future<ui.Image?> retryImage(String keyOrPath) {
evictImage(keyOrPath);
return loadImage(keyOrPath);
}
Future<ui.Image?> loadImage(String? keyOrPath, {bool retain = false}) {
return _loadImage(keyOrPath, failOnError: false, retain: retain);
}
Future<SpineComponent?> createSpineComponent(String? keyOrPath) {
return _createSpineComponent(keyOrPath);
}
bool retainImage(String keyOrPath, {int? generation}) {
final path = _tryResolve(keyOrPath);
if (path == null) {
return false;
}
final record = _images[path];
if (record == null ||
record.state != GameResourceState.ready ||
(generation != null && record.generation != generation)) {
return false;
}
record.refCount++;
_touch(record);
return true;
}
bool releaseImage(String keyOrPath, {int? generation}) {
final path = _tryResolve(keyOrPath);
if (path == null) {
return false;
}
final record = _images[path];
if (record == null ||
(generation != null && record.generation != generation)) {
return false;
}
if (record.refCount > 0) {
record.refCount--;
}
_touch(record);
_enforceImageBudget();
return true;
}
Future<void> preloadGroup(String group, {bool failOnError = false}) async {
final activePackage = _package;
if (activePackage == null) {
throw StateError('GameResourceManager has no active package');
}
final futures = <Future<void>>[];
for (final entry in activePackage.manifest.resources.entries) {
final resource = entry.value;
if (resource.type == GameResourceType.image && resource.group == group) {
futures.add(
_loadImage(entry.key, failOnError: failOnError).then((_) {}),
);
}
if (resource.type == GameResourceType.spine && resource.group == group) {
futures.add(_preloadSpine(entry.key, failOnError: failOnError));
}
}
await Future.wait(futures);
}
int evictGroup(String group) {
final activePackage = _package;
if (activePackage == null) {
return 0;
}
var count = 0;
for (final entry in activePackage.manifest.resources.entries) {
final resource = entry.value;
if (resource.type != GameResourceType.image || resource.group != group) {
continue;
}
final path = activePackage.resolveResourcePath(entry.key);
if (_removeImageRecord(path)) {
count++;
}
}
return count;
}
Future<void> preloadDeclaredImages(GamePackageManifest manifest) async {
final futures = <Future<void>>[];
for (final entry in manifest.resources.entries) {
final resource = entry.value;
if (resource.type != GameResourceType.image ||
resource.preload == GameResourcePreload.lazy) {
continue;
}
final failOnError = resource.preload == GameResourcePreload.required;
futures.add(_loadImage(entry.key, failOnError: failOnError).then((_) {}));
}
await Future.wait(futures);
}
Future<void> preloadDeclaredSpines(GamePackageManifest manifest) async {
final futures = <Future<void>>[];
for (final entry in manifest.resources.entries) {
final resource = entry.value;
if (resource.type != GameResourceType.spine ||
resource.preload == GameResourcePreload.lazy) {
continue;
}
final failOnError = resource.preload == GameResourcePreload.required;
futures.add(_preloadSpine(entry.key, failOnError: failOnError));
}
await Future.wait(futures);
}
}
enum GameResourceState { idle, loading, ready, failed, disposed }
class ResourceLoadException implements Exception {
const ResourceLoadException(this.message);
final String message;
@override
String toString() => 'ResourceLoadException: $message';
}
class _ImageResourceRecord {
_ImageResourceRecord({required this.generation});
final int generation;
GameResourceState state = GameResourceState.idle;
Future<ui.Image?>? inflight;
ui.Image? image;
Object? lastError;
int estimatedBytes = 0;
int refCount = 0;
int lastUsed = 0;
}