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 _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 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 imagesDebugJson() { final activePackage = _package; final declaredPaths = {}; final resources = >[]; 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 retryImage(String keyOrPath) { evictImage(keyOrPath); return loadImage(keyOrPath); } Future loadImage(String? keyOrPath, {bool retain = false}) { return _loadImage(keyOrPath, failOnError: false, retain: retain); } Future 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 preloadGroup(String group, {bool failOnError = false}) async { final activePackage = _package; if (activePackage == null) { throw StateError('GameResourceManager has no active package'); } final futures = >[]; 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 preloadDeclaredImages(GamePackageManifest manifest) async { final futures = >[]; 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 preloadDeclaredSpines(GamePackageManifest manifest) async { final futures = >[]; 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? inflight; ui.Image? image; Object? lastError; int estimatedBytes = 0; int refCount = 0; int lastUsed = 0; }