From e94a7e82b1d733985eb11f637f1d45f532a3f635 Mon Sep 17 00:00:00 2001 From: Dmitry <76906744+s1r1m1r1@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:01:25 +0100 Subject: [PATCH] Refactor asset path resolution and loading in TexturePackerAtlas to correctly handle prefixes and different asset types. [#3844] --- .../lib/src/texture_packer_atlas.dart | 63 +++++++-- .../test/atlas_path_resolution_test.dart | 128 ++++++++++++++++++ 2 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 packages/flame_texturepacker/test/atlas_path_resolution_test.dart diff --git a/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart b/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart index 80179fde838..fac8b6e4f03 100644 --- a/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart +++ b/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart @@ -188,7 +188,7 @@ Future _fromAssets( ); } on Exception catch (e, stack) { Error.throwWithStackTrace( - Exception('Error loading $assetsPrefix$path from assets: $e'), + Exception('Error loading $path (prefix: $assetsPrefix) from assets: $e'), stack, ); } @@ -240,12 +240,33 @@ Future _parse( final regions = []; var hasIndexes = false; - final fileContent = fromStorage - ? await XFile(path).readAsString() - : await (assets ?? Flame.assets).readFile( - '${assetsPrefix!}/$path', - package: package, - ); + final String fileContent; + if (fromStorage) { + fileContent = await XFile(path).readAsString(); + } else { + final prefix = (assetsPrefix ?? '').trim(); + var cleanPath = path.trim(); + + if (cleanPath.startsWith('/')) { + cleanPath = cleanPath.substring(1); + } + + String fullPath; + if (prefix.isEmpty) { + fullPath = cleanPath; + } else { + if (prefix.endsWith('/')) { + fullPath = '$prefix$cleanPath'; + } else { + fullPath = '$prefix/$cleanPath'; + } + } + + fileContent = await (assets ?? Flame.assets).readFile( + fullPath, + package: package, + ); + } final lines = LineSplitter.split( fileContent, @@ -261,6 +282,8 @@ Future _parse( fromStorage, images, package, + assetsPrefix: assetsPrefix, + assets: assets, ); pages.add(page); @@ -329,8 +352,10 @@ Future _parsePage( String path, bool fromStorage, Images images, - String? package, -) async { + String? package, { + String? assetsPrefix, + AssetsCache? assets, +}) async { final page = Page(); page.textureFile = lineQueue.removeFirst(); @@ -345,7 +370,25 @@ Future _parsePage( images.add(texturePath, image); page.texture = images.fromCache(texturePath); } else { - page.texture = await images.load(texturePath, package: package); + final prefix = (assetsPrefix ?? '').trim(); + var cleanPath = texturePath.trim(); + + if (cleanPath.startsWith('/')) { + cleanPath = cleanPath.substring(1); + } + + String fullTexturePath; + if (prefix.isEmpty) { + fullTexturePath = cleanPath; + } else { + if (prefix.endsWith('/')) { + fullTexturePath = '$prefix$cleanPath'; + } else { + fullTexturePath = '$prefix/$cleanPath'; + } + } + + page.texture = await images.load(fullTexturePath, package: package); } _parsePageProperties(lineQueue, page); diff --git a/packages/flame_texturepacker/test/atlas_path_resolution_test.dart b/packages/flame_texturepacker/test/atlas_path_resolution_test.dart new file mode 100644 index 00000000000..6ca85fce83c --- /dev/null +++ b/packages/flame_texturepacker/test/atlas_path_resolution_test.dart @@ -0,0 +1,128 @@ +import 'dart:ui' as ui; +import 'package:flame/cache.dart'; +import 'package:flame_texturepacker/flame_texturepacker.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class _MockAssetBundle extends Mock implements AssetBundle {} + +class _MockImages extends Mock implements Images {} + +class FakeImage extends Mock implements ui.Image {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('TexturePackerAtlas Path Resolution', () { + late _MockAssetBundle bundle; + late _MockImages images; + late String atlasContent; + + setUpAll(() { + registerFallbackValue(const Symbol('package')); + }); + + setUp(() { + bundle = _MockAssetBundle(); + images = _MockImages(); + atlasContent = ''' +test.png +size: 64, 64 +filter: Nearest, Nearest +repeat: none +sprite1 + bounds: 0, 0, 32, 32 +'''; + + // Mock loading the atlas file + when( + () => bundle.loadString(any(), cache: any(named: 'cache')), + ).thenAnswer((_) async => atlasContent); + + // Mock loading an image + when( + () => images.load(any(), package: any(named: 'package')), + ).thenAnswer((_) async => FakeImage()); + }); + + test('should resolve paths correctly with leading slashes', () async { + final assets = AssetsCache(bundle: bundle); + + await TexturePackerAtlas.load( + '/path/to/atlas_name.atlas', + assets: assets, + images: images, + ); + + // Verify it tried to load 'images/path/to/atlas.atlas' + // The leading slash in /path/to/atlas.atlas should be removed. + verify( + () => bundle.loadString( + 'assets/images/path/to/atlas_name.atlas', + cache: any(named: 'cache'), + ), + ).called(1); + }); + + test('should handle assetsPrefix WITH trailing slash', () async { + final assets = AssetsCache(bundle: bundle); + + await TexturePackerAtlas.load( + 'atlas_name.atlas', + assetsPrefix: 'custom/', + assets: assets, + images: images, + ); + + verify( + () => bundle.loadString( + 'assets/custom/atlas_name.atlas', + cache: any(named: 'cache'), + ), + ).called(1); + }); + + test('should handle assetsPrefix WITHOUT trailing slash', () async { + final assets = AssetsCache(bundle: bundle); + + await TexturePackerAtlas.load( + 'atlas_name.atlas', + assetsPrefix: 'custom', + assets: assets, + images: images, + ); + + verify( + () => bundle.loadString( + 'assets/custom/atlas_name.atlas', + cache: any(named: 'cache'), + ), + ).called(1); + }); + + test('should pass package parameter to AssetsCache and Images', () async { + final assets = AssetsCache(bundle: bundle); + + await TexturePackerAtlas.load( + 'atlas_name.atlas', + assets: assets, + images: images, + package: 'my_package', + ); + + // Verify bundle call includes the package-prefixed path + verify( + () => bundle.loadString( + 'packages/my_package/assets/images/atlas_name.atlas', + cache: any(named: 'cache'), + ), + ).called(1); + + // Verify images.load call also includes the package + verify( + () => images.load('images/test.png', package: 'my_package'), + ).called(1); + }); + }); +}