package openfl.utils; #if !macro import funkin.backend.system.Main; import funkin.options.Options; #end import openfl.utils._internal.Log; import openfl.display.BitmapData; import openfl.display.MovieClip; import openfl.display.Sprite; import openfl.events.Event; import openfl.events.EventDispatcher; import openfl.media.Sound; import openfl.text.Font; #if lime import lime.app.Promise; import lime.media.AudioBuffer; import lime.utils.AssetLibrary as LimeAssetLibrary; import lime.utils.Assets as LimeAssets; #end #if lime_vorbis import lime.media.vorbis.VorbisFile; #end /** The Assets class provides a cross-platform interface to access embedded images, fonts, sounds and other resource files. The contents are populated automatically when an application is compiled using the OpenFL command-line tools, based on the contents of the *.xml project file. For most platforms, the assets are included in the same directory or package as the application, and the paths are handled automatically. For web content, the assets are preloaded before the start of the rest of the application. You can customize the preloader by extending the `NMEPreloader` class, and specifying a custom preloader using in the project file. @see [Working with bitmap assets](https://books.openfl.org/openfl-developers-guide/working-with-bitmaps/working-with-bitmap-assets.html) @see [Working with byte array assets](https://books.openfl.org/openfl-developers-guide/working-with-byte-arrays/working-with-byte-array-assets.html) @see [Working with font assets](https://books.openfl.org/openfl-developers-guide/using-the-textfield-class/working-with-font-assets.html) @see [Working with sound assets](https://books.openfl.org/openfl-developers-guide/working-with-sound/working-with-sound-assets.html) **/ #if !openfl_debug @:fileXml('tags="haxe,release"') @:noDebug #end @:access(openfl.display.BitmapData) @:access(openfl.display.Sprite) @:access(openfl.text.Font) @:access(openfl.utils.AssetLibrary) class Assets { public static var allowCompressedTextures:Bool = false; public static var cache:IAssetCache = new AssetCache(); @:noCompletion private static var dispatcher:EventDispatcher #if !macro = new EventDispatcher() #end; private static var libraryBindings:Map = new Map(); public static function addEventListener(type:String, listener:Dynamic, useCapture:Bool = false, priority:Int = 0, useWeakReference:Bool = false):Void { #if lime if (!LimeAssets.onChange.has(LimeAssets_onChange)) { LimeAssets.onChange.add(LimeAssets_onChange); } #end dispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference); } public static function dispatchEvent(event:Event):Bool { return dispatcher.dispatchEvent(event); } /** Returns whether a specific asset exists @param id The ID or asset path for the asset @param type The asset type to match, or null to match any type @param allowCompressedTextures Whether to check for compressed texture formats (e.g., ASTC) when the asset is a PNG. Defaults to true. @return Whether the requested asset ID and type exists **/ public static function exists(id:String, type:AssetType = null, allowCompressedTextures:Bool = true):Bool { #if lime #if !flash if (allowCompressedTextures) { if (id != null && haxe.io.Path.extension(id) == "png") { if (LimeAssets.exists(haxe.io.Path.withExtension(id, "astc"), BINARY)) { return true; } } else if (id != null && haxe.io.Path.extension(id) == "astc" && type != AssetType.BINARY) { type = AssetType.BINARY; } } #end return LimeAssets.exists(id, cast type); #else return false; #end } /** Gets an instance of an embedded bitmap. ```haxe var bitmap = new Bitmap (Assets.getBitmapData ("image.png")); ``` _Note:_ This method may behave differently, depending on the target platform. On targets that can quickly create new BitmapData instances synchronously, every call to `Assets.getBitmapData()` with the same ID will return a BitmapData instance with its own separate copy of the underlying image data. However, on other targets where loading BitmapData synchronously is unacceptably slow, or where BitmapData may not be loaded synchronously at all (meaning that there is no choice but to load it asynchronously), every call to `Assets.getBitmapData()` with the same ID may return a BitmapData instance that shares the same underlying image data each time. With that in mind, modifying or disposing the contents of the BitmapData returned by `Assets.getBitmapData()` may affect the results of future calls to `Assets.getBitmapData()` on some targets. To access a BitmapData instance that may be modified or disposed without affecting future calls to `Assets.getBitmapData()`, call the BitmapData instance's `clone()` method to manually create a copy. @param id The ID or asset path for the bitmap @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @param allowCompressedTextures (Optional) Wether to allow compressed textures to be used to get this bitmap (Default: true) @return A new BitmapData object @see [Working with bitmap assets](https://books.openfl.org/openfl-developers-guide/working-with-bitmaps/working-with-bitmap-assets.html) **/ public static function getBitmapData(id:String, useCache:Bool = true, allowCompressedTextures:Bool = true):BitmapData { #if (lime && tools && !display) if (useCache && cache.enabled && cache.hasBitmapData(id)) { var bitmapData = cache.getBitmapData(id); if (isValidBitmapData(bitmapData)) { return bitmapData; } } #if !flash if ((allowCompressedTextures || haxe.io.Path.extension(id) == "astc") && openfl.Lib.current.stage.context3D.isASTCSupported()) { final astcTexture:String = haxe.io.Path.withExtension(id, "astc"); if (LimeAssets.exists(astcTexture, BINARY)) { var bitmapData = BitmapData.fromTexture(openfl.Lib.current.stage.context3D.createASTCTexture(LimeAssets.getBytes(astcTexture)), false); if (useCache && cache.enabled) { cache.setBitmapData(id, bitmapData); } return bitmapData; } if (haxe.io.Path.extension(id) == "astc") { return null; } } #end var image = LimeAssets.getImage(id, false); if (image != null) { #if flash var bitmapData = image.src; #else var bitmapData = BitmapData.fromImage(image); if (Assets.allowCompressedTextures && allowCompressedTextures && !Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) BitmapDataUtil.toHardware(bitmapData); bitmapData.__asset = true; #end if (useCache && cache.enabled) { cache.setBitmapData(id, bitmapData); } return bitmapData; } #end return null; } /** Gets an instance of an embedded binary asset ```haxe var bytes = Assets.getBytes ("file.zip"); ``` @param id The ID or asset path for the asset @return A new ByteArray object @see [Working with byte array assets](https://books.openfl.org/openfl-developers-guide/working-with-byte-arrays/working-with-byte-array-assets.html) **/ public static function getBytes(id:String):ByteArray { #if lime return LimeAssets.getBytes(id); #else return null; #end } /** Gets an instance of an embedded font ```haxe var fontName = Assets.getFont ("font.ttf").fontName; ``` @param id The ID or asset path for the font @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return A new Font object @see [Working with font assets](https://books.openfl.org/openfl-developers-guide/using-the-textfield-class/working-with-font-assets.html) **/ public static function getFont(id:String, useCache:Bool = true):Font { #if (lime && tools && !display && !macro) if (useCache && cache.enabled && cache.hasFont(id)) { return cache.getFont(id); } var limeFont = LimeAssets.getFont(id, false); if (limeFont != null) { #if flash var font = limeFont.src; #else var font = new Font(); font.__fromLimeFont(limeFont); #end if (useCache && cache.enabled) { cache.setFont(id, font); } return font; } #end return new Font(); } public static function getLibrary(name:String):#if lime LimeAssetLibrary #else AssetLibrary #end { #if lime return LimeAssets.getLibrary(name); #else return null; #end } /** Gets an instance of an included MovieClip ```haxe var movieClip = Assets.getMovieClip ("library:BouncingBall"); ``` @param id The ID for the MovieClip @return A new MovieClip object **/ public static function getMovieClip(id:String):MovieClip { #if (lime && tools && !display) var libraryName = id.substring(0, id.indexOf(":")); var symbolName = id.substr(id.indexOf(":") + 1); var limeLibrary = getLibrary(libraryName); if (limeLibrary != null) { if ((limeLibrary is AssetLibrary)) { var library:AssetLibrary = cast limeLibrary; if (library.exists(symbolName, cast AssetType.MOVIE_CLIP)) { if (library.isLocal(symbolName, cast AssetType.MOVIE_CLIP)) { return library.getMovieClip(symbolName); } else { Log.error("MovieClip asset \"" + id + "\" exists, but only asynchronously"); return null; } } } Log.error("There is no MovieClip asset with an ID of \"" + id + "\""); } else { Log.error("There is no asset library named \"" + libraryName + "\""); } #end return null; } public static function getMusic(id:String, useCache:Bool = true):Sound { if (Options.streamedMusic) { #if (lime_funkin && lime_native) var path = getPath(id); var buffer = AudioBuffer.fromFile(path, true); if (buffer != null) return Sound.fromAudioBuffer(buffer); #elseif (lime_vorbis && lime > "7.9.0") var path = getPath(id); var vorbisFile = VorbisFile.fromFile(path); var buffer = AudioBuffer.fromVorbisFile(vorbisFile); if (buffer != null) return Sound.fromAudioBuffer(buffer); #end } return getSound(id, useCache); } /** Gets the file path (if available) for an asset ```haxe var path = Assets.getPath ("file.txt"); ``` @param id The ID or asset path for the asset @return The path to the asset, or null if it does not exist **/ public static function getPath(id:String):String { #if lime return LimeAssets.getPath(id); #else return null; #end } /** Gets an instance of an embedded sound ```haxe var sound = Assets.getSound ("sound.wav"); ``` @param id The ID or asset path for the sound @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return A new Sound object @see [Working with sound assets](https://books.openfl.org/openfl-developers-guide/working-with-sound/working-with-sound-assets.html) **/ public static function getSound(id:String, useCache:Bool = true):Sound { #if (lime && tools && !display) if (useCache && cache.enabled && cache.hasSound(id)) { var sound = cache.getSound(id); if (isValidSound(sound)) { return sound; } } var buffer = LimeAssets.getAudioBuffer(id, false); if (buffer != null) { #if flash var sound = buffer.src; #else var sound = Sound.fromAudioBuffer(buffer); #end if (useCache && cache.enabled) { cache.setSound(id, sound); } return sound; } #end return null; } /** Gets an instance of an embedded text asset ```haxe var text = Assets.getText ("text.txt"); ``` @param id The ID or asset path for the asset @return A new String object **/ public static function getText(id:String):String { #if lime return LimeAssets.getText(id); #else return null; #end } public static function hasEventListener(type:String):Bool { return dispatcher.hasEventListener(type); } public static function hasLibrary(name:String):Bool { #if lime return LimeAssets.hasLibrary(name); #else return false; #end } /** Connects a user-defined class to a related asset class. This method call is added to the beginning of user-defined class constructors when the `@:bind` meta-data is used. This allows insertion of related asset resources in compatible super classes, such as `openfl.display.MovieClip`. @param className The registered class name of the asset constructor @param instance The current class instance to be bound (default is null) @return Whether asset binding was successful **/ public static function initBinding(className:String, instance:Dynamic = null):Void { if (libraryBindings.exists(className)) { var library = libraryBindings.get(className); #if !flash if (instance == null) { Sprite.__constructor = function(instance:Sprite) { instance.__bind(library, className); } } else { Sprite.__constructor = null; instance.__bind(library, className); } #else // TODO: Consolidate behavior library.bind(className); #end } else { Log.warn("No asset is registered as \"" + className + "\""); } } /** Returns whether an asset is "local", and therefore can be loaded synchronously @param id The ID or asset path for the asset @param type The asset type to match, or null to match any type @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Whether the asset is local **/ public static function isLocal(id:String, type:AssetType = null, useCache:Bool = true):Bool { #if (lime && tools && !display) if (useCache && cache.enabled) { if (type == AssetType.IMAGE || type == null) { if (cache.hasBitmapData(id)) return true; } if (type == AssetType.FONT || type == null) { if (cache.hasFont(id)) return true; } if (type == AssetType.SOUND || type == AssetType.MUSIC || type == null) { if (cache.hasSound(id)) return true; } } var libraryName = id.substring(0, id.indexOf(":")); var symbolName = id.substr(id.indexOf(":") + 1); var library = getLibrary(libraryName); if (library != null) { return library.isLocal(symbolName, cast type); } #end return false; } @:analyzer(ignore) private static function isValidBitmapData(bitmapData:BitmapData):Bool { #if (lime && tools && !display) #if flash try { bitmapData.width; return true; } catch (e:Dynamic) { return false; } #else return (bitmapData != null && #if !lime_hybrid (bitmapData.image != null || bitmapData.__texture != null) #else bitmapData.__handle != null #end); #end #else return true; #end } @:noCompletion private static function isValidSound(sound:Sound):Bool { #if ((tools && !display) && (cpp || neko || nodejs)) return true; // return (sound.__handle != null && sound.__handle != 0); #else return true; #end } /** Returns a list of all embedded assets (by type) @param type The asset type to match, or null to match any type @return An array of asset ID values **/ public static function list(type:AssetType = null):Array { #if lime return LimeAssets.list(cast type); #else return []; #end } /** Loads an included bitmap asset asynchronously ```haxe Assets.loadBitmapData ("image.png").onComplete (handleImage); ``` @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future @see [Working with bitmap assets](https://books.openfl.org/openfl-developers-guide/working-with-bitmaps/working-with-bitmap-assets.html) **/ public static function loadBitmapData(id:String, useCache:Null = true, allowCompressedTextures:Bool = true):Future { if (useCache == null) useCache = true; #if (lime && tools && !display) var promise = new Promise(); if (useCache && cache.enabled && cache.hasBitmapData(id)) { var bitmapData = cache.getBitmapData(id); if (isValidBitmapData(bitmapData)) { promise.complete(bitmapData); return promise.future; } } #if !flash if ((allowCompressedTextures || haxe.io.Path.extension(id) == "astc") && openfl.Lib.current.stage.context3D.isASTCSupported()) { final astcTexture:String = haxe.io.Path.withExtension(id, "astc"); if (LimeAssets.exists(astcTexture, BINARY)) { LimeAssets.loadBytes(astcTexture).onComplete(function(bytes) { if (bytes != null) { var bitmapData = BitmapData.fromTexture(openfl.Lib.current.stage.context3D.createASTCTexture(bytes), false); if (Assets.allowCompressedTextures && allowCompressedTextures && !Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) BitmapDataUtil.toHardware(bitmapData); bitmapData.__asset = true; if (useCache && cache.enabled) { cache.setBitmapData(id, bitmapData); } promise.complete(bitmapData); } else { promise.error("[Assets] Could not load Image \"" + id + "\""); } }).onError(promise.error).onProgress(promise.progress); return promise.future; } if (haxe.io.Path.extension(id) == "astc") { promise.error("[Assets] Could not load Image \"" + id + "\""); return promise.future; } } #end LimeAssets.loadImage(id, false).onComplete(function(image) { if (image != null) { #if flash var bitmapData = image.src; #else var bitmapData = BitmapData.fromImage(image); if (Assets.allowCompressedTextures && allowCompressedTextures && !Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) BitmapDataUtil.toHardware(bitmapData); bitmapData.__asset = true; #end if (useCache && cache.enabled) { cache.setBitmapData(id, bitmapData); } promise.complete(bitmapData); } else { promise.error("[Assets] Could not load Image \"" + id + "\""); } }).onError(promise.error).onProgress(promise.progress); return promise.future; #else return Future.withValue(getBitmapData(id, useCache)); #end } /** Loads an included byte asset asynchronously ```haxe Assets.loadBytes ("file.zip").onComplete (handleBytes); ``` @param id The ID or asset path for the asset @return Returns a Future @see [Working with byte array assets](https://books.openfl.org/openfl-developers-guide/working-with-byte-arrays/working-with-byte-array-assets.html) **/ public static function loadBytes(id:String):Future { #if lime var promise = new Promise(); var future = LimeAssets.loadBytes(id); future.onComplete(function(bytes) promise.complete(bytes)); future.onProgress(function(progress, total) promise.progress(progress, total)); future.onError(function(msg) promise.error(msg)); return promise.future; #else return Future.withValue(getBytes(id)); #end } /** Loads an included font asset asynchronously ```haxe Assets.loadFont ("font.ttf").onComplete (handleFont); ``` @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future @see [Working with font assets](https://books.openfl.org/openfl-developers-guide/using-the-textfield-class/working-with-font-assets.html) **/ public static function loadFont(id:String, useCache:Null = true):Future { if (useCache == null) useCache = true; #if (lime && tools && !display && !macro) var promise = new Promise(); if (useCache && cache.enabled && cache.hasFont(id)) { promise.complete(cache.getFont(id)); return promise.future; } LimeAssets.loadFont(id) .onComplete(function(limeFont) { #if flash var font = limeFont.src; #else var font = new Font(); font.__fromLimeFont(limeFont); #end if (useCache && cache.enabled) { cache.setFont(id, font); } promise.complete(font); }) .onError(promise.error) .onProgress(promise.progress); return promise.future; #else return Future.withValue(getFont(id, useCache)); #end } /** Load an included AssetLibrary @param name The name of the AssetLibrary to load @return Returns a Future **/ public static function loadLibrary(name:String):#if java Future #else Future #end { #if lime return LimeAssets.loadLibrary(name).then(function(library) { var _library:AssetLibrary = null; if (library != null) { if ((library is AssetLibrary)) { _library = cast library; } else { // TODO: after Lime 8.2.0 is released, use conditional // compilation to call LimeAssets.removeLibrary(name, false) // since that is a new public API @:privateAccess LimeAssets.libraries.remove(name); _library = new AssetLibrary(); _library.__proxy = library; // ERIC: Figure out what bug the change here was made to fix // https://github.com/FunkinCrew/openfl/commit/7af97e4baff7371eba1f79959909ffca7971902e // https://github.com/FunkinCrew/lime/commit/f195121ebec688b417e38ab115185c8d93c349d3 LimeAssets.registerLibrary(name, _library); } } return Future.withValue(_library); }); #else return cast Future.withError("Cannot load library"); #end } /** Loads an included music asset asynchronously ```haxe Assets.loadMusic ("music.ogg").onComplete (handleMusic); ``` @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future **/ public static function loadMusic(id:String, useCache:Null = true):Future { if (useCache == null) useCache = true; #if lime #if !html5 var promise = new Promise(); LimeAssets.loadAudioBuffer(id, useCache) .onComplete(function(buffer) { if (buffer != null) { #if flash var sound = buffer.src; #else var sound = Sound.fromAudioBuffer(buffer); #end if (useCache && cache.enabled) { cache.setSound(id, sound); } promise.complete(sound); } else { promise.error("[Assets] Could not load Sound \"" + id + "\""); } }) .onError(promise.error) .onProgress(promise.progress); return promise.future; #else var future = new Future(function() return getMusic(id, useCache)); return future; #end #else return Future.withValue(getMusic(id, useCache)); #end } /** Loads an included MovieClip asset asynchronously ```haxe Assets.loadMovieClip ("library:BouncingBall").onComplete (handleMovieClip); ``` @param id The ID for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future **/ public static function loadMovieClip(id:String):Future { #if (lime && tools && !display) var promise = new Promise(); var libraryName = id.substring(0, id.indexOf(":")); var symbolName = id.substr(id.indexOf(":") + 1); var limeLibrary = getLibrary(libraryName); if (limeLibrary != null) { if ((limeLibrary is AssetLibrary)) { var library:AssetLibrary = cast limeLibrary; if (library.exists(symbolName, cast AssetType.MOVIE_CLIP)) { promise.completeWith(library.loadMovieClip(symbolName)); return promise.future; } } promise.error("[Assets] There is no MovieClip asset with an ID of \"" + id + "\""); } else { promise.error("[Assets] There is no asset library named \"" + libraryName + "\""); } return promise.future; #else return Future.withValue(getMovieClip(id)); #end } /** Loads an included sound asset asynchronously ```haxe Assets.loadSound ("sound.wav").onComplete (handleSound); ``` @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future @see [Working with sound assets](https://books.openfl.org/openfl-developers-guide/working-with-sound/working-with-sound-assets.html) **/ public static function loadSound(id:String, useCache:Null = true):Future { if (useCache == null) useCache = true; #if lime var promise = new Promise(); LimeAssets.loadAudioBuffer(id, useCache) .onComplete(function(buffer) { if (buffer != null) { #if flash var sound = buffer.src; #else var sound = Sound.fromAudioBuffer(buffer); #end if (useCache && cache.enabled) { cache.setSound(id, sound); } promise.complete(sound); } else { promise.error("[Assets] Could not load Sound \"" + id + "\""); } }) .onError(promise.error) .onProgress(promise.progress); return promise.future; #else return Future.withValue(getSound(id, useCache)); #end } /** Loads an included text asset asynchronously ```haxe Assets.loadText ("text.txt").onComplete (handleString); ``` @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future **/ public static function loadText(id:String):Future { #if lime var future = LimeAssets.loadText(id); return future; #else return Future.withValue(getText(id)); #end } /** Registers an AssetLibrary binding for use with @:bind or Assets.bind @param className The class name to use for the binding @param method The AssetLibrary responsible for the binding **/ public static function registerBinding(className:String, library:AssetLibrary):Void { libraryBindings.set(className, library); } /** Registers a new AssetLibrary with the Assets class @param name The name (prefix) to use for the library @param library An AssetLibrary instance to register **/ public static function registerLibrary(name:String, library:AssetLibrary):Void { #if lime LimeAssets.registerLibrary(name, library); #end } public static function removeEventListener(type:String, listener:Dynamic, capture:Bool = false):Void { dispatcher.removeEventListener(type, listener, capture); } @:noCompletion private static function resolveClass(name:String):Class { return Type.resolveClass(name); } @:noCompletion private static function resolveEnum(name:String):Enum { var value = Type.resolveEnum(name); #if flash if (value == null) { return cast Type.resolveClass(name); } #end return value; } public static function unloadLibrary(name:String):Void { #if lime LimeAssets.unloadLibrary(name); #end } /** Unregisters an AssetLibrary binding for use with @:bind or Assets.bind @param className The class name to use for the binding @param method The AssetLibrary responsible for the binding **/ public static function unregisterBinding(className:String, library:AssetLibrary):Void { if (libraryBindings.exists(className) && libraryBindings.get(className) == library) { libraryBindings.remove(className); } } // Event Handlers @:noCompletion private static function LimeAssets_onChange():Void { dispatchEvent(new Event(Event.CHANGE)); } }