1
0
Fork 0
mirror of https://github.com/TerryCavanagh/VVVVVV.git synced 2024-09-27 16:57:25 +02:00
VVVVVV/mobile_version/src/starling/utils/AssetManager.as

1303 lines
54 KiB
ActionScript
Raw Normal View History

// =================================================================================================
//
// Starling Framework
// Copyright Gamua GmbH. All Rights Reserved.
//
// This program is free software. You can redistribute and/or modify it
// in accordance with the terms of the accompanying license agreement.
//
// =================================================================================================
package starling.utils
{
import flash.display.Bitmap;
import flash.display.Loader;
import flash.display.LoaderInfo;
import flash.events.HTTPStatusEvent;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.SecurityErrorEvent;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.media.SoundTransform;
import flash.net.FileReference;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.system.ImageDecodingPolicy;
import flash.system.LoaderContext;
import flash.system.System;
import flash.utils.ByteArray;
import flash.utils.Dictionary;
import flash.utils.describeType;
import flash.utils.getQualifiedClassName;
import flash.utils.setTimeout;
import starling.core.Starling;
import starling.events.Event;
import starling.events.EventDispatcher;
import starling.text.BitmapFont;
import starling.text.TextField;
import starling.textures.AtfData;
import starling.textures.Texture;
import starling.textures.TextureAtlas;
import starling.textures.TextureOptions;
/** Dispatched when all textures have been restored after a context loss. */
[Event(name="texturesRestored", type="starling.events.Event")]
/** Dispatched when an URLLoader fails with an IO_ERROR while processing the queue.
* The 'data' property of the Event contains the URL-String that could not be loaded. */
[Event(name="ioError", type="starling.events.Event")]
/** Dispatched when an URLLoader fails with a SECURITY_ERROR while processing the queue.
* The 'data' property of the Event contains the URL-String that could not be loaded. */
[Event(name="securityError", type="starling.events.Event")]
/** Dispatched when an XML or JSON file couldn't be parsed.
* The 'data' property of the Event contains the name of the asset that could not be parsed. */
[Event(name="parseError", type="starling.events.Event")]
/** The AssetManager handles loading and accessing a variety of asset types. You can
* add assets directly (via the 'add...' methods) or asynchronously via a queue. This allows
* you to deal with assets in a unified way, no matter if they are loaded from a file,
* directory, URL, or from an embedded object.
*
* <p>The class can deal with the following media types:
* <ul>
* <li>Textures, either from Bitmaps or ATF data</li>
* <li>Texture atlases</li>
* <li>Bitmap Fonts</li>
* <li>Sounds</li>
* <li>XML data</li>
* <li>JSON data</li>
* <li>ByteArrays</li>
* </ul>
* </p>
*
* <p>For more information on how to add assets from different sources, read the documentation
* of the "enqueue()" method.</p>
*
* <strong>Context Loss</strong>
*
* <p>When the stage3D context is lost (and you have enabled 'Starling.handleLostContext'),
* the AssetManager will automatically restore all loaded textures. To save memory, it will
* get them from their original sources. Since this is done asynchronously, your images might
* not reappear all at once, but during a timeframe of several seconds. If you want, you can
* pause your game during that time; the AssetManager dispatches an "Event.TEXTURES_RESTORED"
* event when all textures have been restored.</p>
*
* <strong>Error handling</strong>
*
* <p>Loading of some assets may fail while the queue is being processed. In that case, the
* AssetManager will dispatch events of type "IO_ERROR", "SECURITY_ERROR" or "PARSE_ERROR".
* You can listen to those events and handle the errors manually (e.g., you could enqueue
* them once again and retry, or provide placeholder textures). Queue processing will
* continue even when those events are dispatched.</p>
*
* <strong>Using variable texture formats</strong>
*
* <p>When you enqueue a texture, its properties for "format", "scale", "mipMapping", and
* "repeat" will reflect the settings of the AssetManager at the time they were enqueued.
* This means that you can enqueue a bunch of textures, then change the settings and enqueue
* some more. Like this:</p>
*
* <listing>
* var appDir:File = File.applicationDirectory;
* var assets:AssetManager = new AssetManager();
*
* assets.textureFormat = Context3DTextureFormat.BGRA;
* assets.enqueue(appDir.resolvePath("textures/32bit"));
*
* assets.textureFormat = Context3DTextureFormat.BGRA_PACKED;
* assets.enqueue(appDir.resolvePath("textures/16bit"));
*
* assets.loadQueue(...);</listing>
*/
public class AssetManager extends EventDispatcher
{
// This HTTPStatusEvent is only available in AIR
private static const HTTP_RESPONSE_STATUS:String = "httpResponseStatus";
private var _starling:Starling;
private var _numLostTextures:int;
private var _numRestoredTextures:int;
private var _numLoadingQueues:int;
private var _defaultTextureOptions:TextureOptions;
private var _checkPolicyFile:Boolean;
private var _keepAtlasXmls:Boolean;
private var _keepFontXmls:Boolean;
private var _numConnections:int;
private var _verbose:Boolean;
private var _queue:Array;
private var _textures:Dictionary;
private var _atlases:Dictionary;
private var _sounds:Dictionary;
private var _xmls:Dictionary;
private var _objects:Dictionary;
private var _byteArrays:Dictionary;
/** helper objects */
private static var sNames:Vector.<String> = new <String>[];
/** Regex for name / extension extraction from URL. */
private static const NAME_REGEX:RegExp = /([^\?\/\\]+?)(?:\.([\w\-]+))?(?:\?.*)?$/;
/** Create a new AssetManager. The 'scaleFactor' and 'useMipmaps' parameters define
* how enqueued bitmaps will be converted to textures. */
public function AssetManager(scaleFactor:Number=1, useMipmaps:Boolean=false)
{
_defaultTextureOptions = new TextureOptions(scaleFactor, useMipmaps);
_textures = new Dictionary();
_atlases = new Dictionary();
_sounds = new Dictionary();
_xmls = new Dictionary();
_objects = new Dictionary();
_byteArrays = new Dictionary();
_numConnections = 3;
_verbose = true;
_queue = [];
}
/** Disposes all contained textures, XMLs and ByteArrays.
*
* <p>Beware that all references to the assets will remain intact, even though the assets
* are no longer valid. Call 'purge' if you want to remove all resources and reuse
* the AssetManager later.</p>
*/
public function dispose():void
{
for each (var texture:Texture in _textures)
texture.dispose();
for each (var atlas:TextureAtlas in _atlases)
atlas.dispose();
for each (var xml:XML in _xmls)
System.disposeXML(xml);
for each (var byteArray:ByteArray in _byteArrays)
byteArray.clear();
}
// retrieving
/** Returns a texture with a certain name. The method first looks through the directly
* added textures; if no texture with that name is found, it scans through all
* texture atlases. */
public function getTexture(name:String):Texture
{
if (name in _textures) return _textures[name];
else
{
for each (var atlas:TextureAtlas in _atlases)
{
var texture:Texture = atlas.getTexture(name);
if (texture) return texture;
}
return null;
}
}
/** Returns all textures that start with a certain string, sorted alphabetically
* (especially useful for "MovieClip"). */
public function getTextures(prefix:String="", out:Vector.<Texture>=null):Vector.<Texture>
{
if (out == null) out = new <Texture>[];
for each (var name:String in getTextureNames(prefix, sNames))
out[out.length] = getTexture(name); // avoid 'push'
sNames.length = 0;
return out;
}
/** Returns all texture names that start with a certain string, sorted alphabetically. */
public function getTextureNames(prefix:String="", out:Vector.<String>=null):Vector.<String>
{
out = getDictionaryKeys(_textures, prefix, out);
for each (var atlas:TextureAtlas in _atlases)
atlas.getNames(prefix, out);
out.sort(Array.CASEINSENSITIVE);
return out;
}
/** Returns a texture atlas with a certain name, or null if it's not found. */
public function getTextureAtlas(name:String):TextureAtlas
{
return _atlases[name] as TextureAtlas;
}
/** Returns all texture atlas names that start with a certain string, sorted alphabetically.
* If you pass an <code>out</code>-vector, the names will be added to that vector. */
public function getTextureAtlasNames(prefix:String="", out:Vector.<String>=null):Vector.<String>
{
return getDictionaryKeys(_atlases, prefix, out);
}
/** Returns a sound with a certain name, or null if it's not found. */
public function getSound(name:String):Sound
{
return _sounds[name];
}
/** Returns all sound names that start with a certain string, sorted alphabetically.
* If you pass an <code>out</code>-vector, the names will be added to that vector. */
public function getSoundNames(prefix:String="", out:Vector.<String>=null):Vector.<String>
{
return getDictionaryKeys(_sounds, prefix, out);
}
/** Generates a new SoundChannel object to play back the sound. This method returns a
* SoundChannel object, which you can access to stop the sound and to control volume. */
public function playSound(name:String, startTime:Number=0, loops:int=0,
transform:SoundTransform=null):SoundChannel
{
if (name in _sounds)
return getSound(name).play(startTime, loops, transform);
else
return null;
}
/** Returns an XML with a certain name, or null if it's not found. */
public function getXml(name:String):XML
{
return _xmls[name];
}
/** Returns all XML names that start with a certain string, sorted alphabetically.
* If you pass an <code>out</code>-vector, the names will be added to that vector. */
public function getXmlNames(prefix:String="", out:Vector.<String>=null):Vector.<String>
{
return getDictionaryKeys(_xmls, prefix, out);
}
/** Returns an object with a certain name, or null if it's not found. Enqueued JSON
* data is parsed and can be accessed with this method. */
public function getObject(name:String):Object
{
return _objects[name];
}
/** Returns all object names that start with a certain string, sorted alphabetically.
* If you pass an <code>out</code>-vector, the names will be added to that vector. */
public function getObjectNames(prefix:String="", out:Vector.<String>=null):Vector.<String>
{
return getDictionaryKeys(_objects, prefix, out);
}
/** Returns a byte array with a certain name, or null if it's not found. */
public function getByteArray(name:String):ByteArray
{
return _byteArrays[name];
}
/** Returns all byte array names that start with a certain string, sorted alphabetically.
* If you pass an <code>out</code>-vector, the names will be added to that vector. */
public function getByteArrayNames(prefix:String="", out:Vector.<String>=null):Vector.<String>
{
return getDictionaryKeys(_byteArrays, prefix, out);
}
// direct adding
/** Register a texture under a certain name. It will be available right away.
* If the name was already taken, the existing texture will be disposed and replaced
* by the new one. */
public function addTexture(name:String, texture:Texture):void
{
log("Adding texture '" + name + "'");
if (name in _textures)
{
log("Warning: name was already in use; the previous texture will be replaced.");
_textures[name].dispose();
}
_textures[name] = texture;
}
/** Register a texture atlas under a certain name. It will be available right away.
* If the name was already taken, the existing atlas will be disposed and replaced
* by the new one. */
public function addTextureAtlas(name:String, atlas:TextureAtlas):void
{
log("Adding texture atlas '" + name + "'");
if (name in _atlases)
{
log("Warning: name was already in use; the previous atlas will be replaced.");
_atlases[name].dispose();
}
_atlases[name] = atlas;
}
/** Register a sound under a certain name. It will be available right away.
* If the name was already taken, the existing sound will be replaced by the new one. */
public function addSound(name:String, sound:Sound):void
{
log("Adding sound '" + name + "'");
if (name in _sounds)
log("Warning: name was already in use; the previous sound will be replaced.");
_sounds[name] = sound;
}
/** Register an XML object under a certain name. It will be available right away.
* If the name was already taken, the existing XML will be disposed and replaced
* by the new one. */
public function addXml(name:String, xml:XML):void
{
log("Adding XML '" + name + "'");
if (name in _xmls)
{
log("Warning: name was already in use; the previous XML will be replaced.");
System.disposeXML(_xmls[name]);
}
_xmls[name] = xml;
}
/** Register an arbitrary object under a certain name. It will be available right away.
* If the name was already taken, the existing object will be replaced by the new one. */
public function addObject(name:String, object:Object):void
{
log("Adding object '" + name + "'");
if (name in _objects)
log("Warning: name was already in use; the previous object will be replaced.");
_objects[name] = object;
}
/** Register a byte array under a certain name. It will be available right away.
* If the name was already taken, the existing byte array will be cleared and replaced
* by the new one. */
public function addByteArray(name:String, byteArray:ByteArray):void
{
log("Adding byte array '" + name + "'");
if (name in _byteArrays)
{
log("Warning: name was already in use; the previous byte array will be replaced.");
_byteArrays[name].clear();
}
_byteArrays[name] = byteArray;
}
// removing
/** Removes a certain texture, optionally disposing it. */
public function removeTexture(name:String, dispose:Boolean=true):void
{
log("Removing texture '" + name + "'");
if (dispose && name in _textures)
_textures[name].dispose();
delete _textures[name];
}
/** Removes a certain texture atlas, optionally disposing it. */
public function removeTextureAtlas(name:String, dispose:Boolean=true):void
{
log("Removing texture atlas '" + name + "'");
if (dispose && name in _atlases)
_atlases[name].dispose();
delete _atlases[name];
}
/** Removes a certain sound. */
public function removeSound(name:String):void
{
log("Removing sound '"+ name + "'");
delete _sounds[name];
}
/** Removes a certain Xml object, optionally disposing it. */
public function removeXml(name:String, dispose:Boolean=true):void
{
log("Removing xml '"+ name + "'");
if (dispose && name in _xmls)
System.disposeXML(_xmls[name]);
delete _xmls[name];
}
/** Removes a certain object. */
public function removeObject(name:String):void
{
log("Removing object '"+ name + "'");
delete _objects[name];
}
/** Removes a certain byte array, optionally disposing its memory right away. */
public function removeByteArray(name:String, dispose:Boolean=true):void
{
log("Removing byte array '"+ name + "'");
if (dispose && name in _byteArrays)
_byteArrays[name].clear();
delete _byteArrays[name];
}
/** Empties the queue and aborts any pending load operations. */
public function purgeQueue():void
{
_queue.length = 0;
dispatchEventWith(Event.CANCEL);
}
/** Removes assets of all types (disposing them along the way), empties the queue and
* aborts any pending load operations. */
public function purge():void
{
log("Purging all assets, emptying queue");
purgeQueue();
dispose();
_textures = new Dictionary();
_atlases = new Dictionary();
_sounds = new Dictionary();
_xmls = new Dictionary();
_objects = new Dictionary();
_byteArrays = new Dictionary();
}
// queued adding
/** Enqueues one or more raw assets; they will only be available after successfully
* executing the "loadQueue" method. This method accepts a variety of different objects:
*
* <ul>
* <li>Strings or URLRequests containing an URL to a local or remote resource. Supported
* types: <code>png, jpg, gif, atf, mp3, xml, fnt, json, binary</code>.</li>
* <li>Instances of the File class (AIR only) pointing to a directory or a file.
* Directories will be scanned recursively for all supported types.</li>
* <li>Classes that contain <code>static</code> embedded assets.</li>
* <li>If the file extension is not recognized, the data is analyzed to see if
* contains XML or JSON data. If it's neither, it is stored as ByteArray.</li>
* </ul>
*
* <p>Suitable object names are extracted automatically: A file named "image.png" will be
* accessible under the name "image". When enqueuing embedded assets via a class,
* the variable name of the embedded object will be used as its name. An exception
* are texture atlases: they will have the same name as the actual texture they are
* referencing.</p>
*
* <p>XMLs that contain texture atlases or bitmap fonts are processed directly: fonts are
* registered at the TextField class, atlas textures can be acquired with the
* "getTexture()" method. All other XMLs are available via "getXml()".</p>
*
* <p>If you pass in JSON data, it will be parsed into an object and will be available via
* "getObject()".</p>
*/
public function enqueue(...rawAssets):void
{
for each (var rawAsset:Object in rawAssets)
{
if (rawAsset is Array)
{
enqueue.apply(this, rawAsset);
}
else if (rawAsset is Class)
{
var typeXml:XML = describeType(rawAsset);
var childNode:XML;
if (_verbose)
log("Looking for static embedded assets in '" +
(typeXml.@name).split("::").pop() + "'");
for each (childNode in typeXml.constant.(@type == "Class"))
enqueueWithName(rawAsset[childNode.@name], childNode.@name);
for each (childNode in typeXml.variable.(@type == "Class"))
enqueueWithName(rawAsset[childNode.@name], childNode.@name);
}
else if (getQualifiedClassName(rawAsset) == "flash.filesystem::File")
{
if (!rawAsset["exists"])
{
log("File or directory not found: '" + rawAsset["url"] + "'");
}
else if (!rawAsset["isHidden"])
{
if (rawAsset["isDirectory"])
enqueue.apply(this, rawAsset["getDirectoryListing"]());
else
enqueueWithName(rawAsset);
}
}
else if (rawAsset is String || rawAsset is URLRequest)
{
enqueueWithName(rawAsset);
}
else
{
log("Ignoring unsupported asset type: " + getQualifiedClassName(rawAsset));
}
}
}
/** Enqueues a single asset with a custom name that can be used to access it later.
* If the asset is a texture, you can also add custom texture options.
*
* @param asset The asset that will be enqueued; accepts the same objects as the
* 'enqueue' method.
* @param name The name under which the asset will be found later. If you pass null or
* omit the parameter, it's attempted to generate a name automatically.
* @param options Custom options that will be used if 'asset' points to texture data.
* @return the name with which the asset was registered.
*/
public function enqueueWithName(asset:Object, name:String=null,
options:TextureOptions=null):String
{
var filename:String = null;
if (getQualifiedClassName(asset) == "flash.filesystem::File")
{
filename = asset["name"];
asset = decodeURI(asset["url"]);
}
if (name == null) name = getName(asset);
if (options == null) options = _defaultTextureOptions.clone();
else options = options.clone();
log("Enqueuing '" + (filename || name) + "'");
_queue.push({
name: name,
asset: asset,
options: options
});
return name;
}
/** Loads all enqueued assets asynchronously. The 'onProgress' function will be called
* with a 'ratio' between '0.0' and '1.0', with '1.0' meaning that it's complete.
*
* <p>When you call this method, the manager will save a reference to "Starling.current";
* all textures that are loaded will be accessible only from within this instance. Thus,
* if you are working with more than one Starling instance, be sure to call
* "makeCurrent()" on the appropriate instance before processing the queue.</p>
*
* @param onProgress <code>function(ratio:Number):void;</code>
*/
public function loadQueue(onProgress:Function):void
{
if (onProgress == null)
throw new ArgumentError("Argument 'onProgress' must not be null");
if (_queue.length == 0)
{
onProgress(1.0);
return;
}
_starling = Starling.current;
if (_starling == null || _starling.context == null)
throw new Error("The Starling instance needs to be ready before assets can be loaded.");
const PROGRESS_PART_ASSETS:Number = 0.9;
const PROGRESS_PART_XMLS:Number = 1.0 - PROGRESS_PART_ASSETS;
var i:int;
var canceled:Boolean = false;
var xmls:Vector.<XML> = new <XML>[];
var assetInfos:Array = _queue.concat();
var assetCount:int = _queue.length;
var assetProgress:Array = [];
var assetIndex:int = 0;
for (i=0; i<assetCount; ++i)
assetProgress[i] = 0.0;
for (i=0; i<_numConnections; ++i)
loadNextQueueElement();
_queue.length = 0;
_numLoadingQueues++;
addEventListener(Event.CANCEL, cancel);
function loadNextQueueElement():void
{
if (assetIndex < assetInfos.length)
{
// increment asset index *before* using it, since
// 'loadQueueElement' could by synchronous in subclasses.
var index:int = assetIndex++;
loadQueueElement(index, assetInfos[index]);
}
}
function loadQueueElement(index:int, assetInfo:Object):void
{
if (canceled) return;
var onElementProgress:Function = function(progress:Number):void
{
updateAssetProgress(index, progress * 0.8); // keep 20 % for completion
};
var onElementLoaded:Function = function():void
{
updateAssetProgress(index, 1.0);
assetCount--;
if (assetCount > 0) loadNextQueueElement();
else processXmls();
};
processRawAsset(assetInfo.name, assetInfo.asset, assetInfo.options,
xmls, onElementProgress, onElementLoaded);
}
function updateAssetProgress(index:int, progress:Number):void
{
assetProgress[index] = progress;
var sum:Number = 0.0;
var len:int = assetProgress.length;
for (i=0; i<len; ++i)
sum += assetProgress[i];
onProgress(sum / len * PROGRESS_PART_ASSETS);
}
function processXmls():void
{
// xmls are processed separately at the end, because the textures they reference
// have to be available for other XMLs. Texture atlases are processed first:
// that way, their textures can be referenced, too.
xmls.sort(function(a:XML, b:XML):int {
return a.localName() == "TextureAtlas" ? -1 : 1;
});
setTimeout(processXml, 1, 0);
}
function processXml(index:int):void
{
if (canceled) return;
else if (index == xmls.length)
{
finish();
return;
}
var name:String;
var texture:Texture;
var xml:XML = xmls[index];
var rootNode:String = xml.localName();
var xmlProgress:Number = (index + 1) / (xmls.length + 1);
if (rootNode == "TextureAtlas")
{
name = getName(xml.@imagePath.toString());
texture = getTexture(name);
if (texture)
{
addTextureAtlas(name, new TextureAtlas(texture, xml));
removeTexture(name, false);
if (_keepAtlasXmls) addXml(name, xml);
else System.disposeXML(xml);
}
else log("Cannot create atlas: texture '" + name + "' is missing.");
}
else if (rootNode == "font")
{
name = getName(xml.pages.page.@file.toString());
texture = getTexture(name);
if (texture)
{
log("Adding bitmap font '" + name + "'");
TextField.registerCompositor(new BitmapFont(texture, xml), name);
removeTexture(name, false);
if (_keepFontXmls) addXml(name, xml);
else System.disposeXML(xml);
}
else log("Cannot create bitmap font: texture '" + name + "' is missing.");
}
else
throw new Error("XML contents not recognized: " + rootNode);
onProgress(PROGRESS_PART_ASSETS + PROGRESS_PART_XMLS * xmlProgress);
setTimeout(processXml, 1, index + 1);
}
function cancel():void
{
removeEventListener(Event.CANCEL, cancel);
_numLoadingQueues--;
canceled = true;
}
function finish():void
{
// We dance around the final "onProgress" call with some "setTimeout" calls here
// to make sure the progress bar gets the chance to be rendered. Otherwise, all
// would happen in one frame.
setTimeout(function():void
{
if (!canceled)
{
cancel();
onProgress(1.0);
}
}, 1);
}
}
private function processRawAsset(name:String, rawAsset:Object, options:TextureOptions,
xmls:Vector.<XML>,
onProgress:Function, onComplete:Function):void
{
var canceled:Boolean = false;
addEventListener(Event.CANCEL, cancel);
loadRawAsset(rawAsset, progress, process);
function process(asset:Object):void
{
var texture:Texture;
var bytes:ByteArray;
var object:Object = null;
var xml:XML = null;
// the 'current' instance might have changed by now
// if we're running in a set-up with multiple instances.
_starling.makeCurrent();
if (canceled)
{
// do nothing
}
else if (asset == null)
{
onComplete();
}
else if (asset is Sound)
{
addSound(name, asset as Sound);
onComplete();
}
else if (asset is XML)
{
xml = asset as XML;
if (xml.localName() == "TextureAtlas" || xml.localName() == "font")
xmls.push(xml);
else
addXml(name, xml);
onComplete();
}
else if (_starling.context.driverInfo == "Disposed")
{
log("Context lost while processing assets, retrying ...");
setTimeout(process, 1, asset);
return; // to keep CANCEL event listener intact
}
else if (asset is Bitmap)
{
texture = Texture.fromData(asset, options);
texture.root.onRestore = function():void
{
_numLostTextures++;
loadRawAsset(rawAsset, null, function(asset:Object):void
{
try
{
if (asset == null) throw new Error("Reload failed");
texture.root.uploadBitmap(asset as Bitmap);
asset.bitmapData.dispose();
}
catch (e:Error)
{
log("Texture restoration failed for '" + name + "': " + e.message);
}
_numRestoredTextures++;
Starling.current.stage.setRequiresRedraw();
if (_numLostTextures == _numRestoredTextures)
dispatchEventWith(Event.TEXTURES_RESTORED);
});
};
asset.bitmapData.dispose();
addTexture(name, texture);
onComplete();
}
else if (asset is ByteArray)
{
bytes = asset as ByteArray;
if (AtfData.isAtfData(bytes))
{
options.onReady = prependCallback(options.onReady, function():void
{
addTexture(name, texture);
onComplete();
});
texture = Texture.fromData(bytes, options);
texture.root.onRestore = function():void
{
_numLostTextures++;
loadRawAsset(rawAsset, null, function(asset:Object):void
{
try
{
if (asset == null) throw new Error("Reload failed");
texture.root.uploadAtfData(asset as ByteArray, 0, false);
asset.clear();
}
catch (e:Error)
{
log("Texture restoration failed for '" + name + "': " + e.message);
}
_numRestoredTextures++;
Starling.current.stage.setRequiresRedraw();
if (_numLostTextures == _numRestoredTextures)
dispatchEventWith(Event.TEXTURES_RESTORED);
});
};
bytes.clear();
}
else if (byteArrayStartsWith(bytes, "{") || byteArrayStartsWith(bytes, "["))
{
try { object = JSON.parse(bytes.readUTFBytes(bytes.length)); }
catch (e:Error)
{
log("Could not parse JSON: " + e.message);
dispatchEventWith(Event.PARSE_ERROR, false, name);
}
if (object) addObject(name, object);
bytes.clear();
onComplete();
}
else if (byteArrayStartsWith(bytes, "<"))
{
try { xml = new XML(bytes); }
catch (e:Error)
{
log("Could not parse XML: " + e.message);
dispatchEventWith(Event.PARSE_ERROR, false, name);
}
process(xml);
bytes.clear();
}
else
{
addByteArray(name, bytes);
onComplete();
}
}
else
{
addObject(name, asset);
onComplete();
}
// avoid that objects stay in memory (through 'onRestore' functions)
asset = null;
bytes = null;
removeEventListener(Event.CANCEL, cancel);
}
function progress(ratio:Number):void
{
if (!canceled) onProgress(ratio);
}
function cancel():void
{
canceled = true;
}
}
/** This method is called internally for each element of the queue when it is loaded.
* 'rawAsset' is typically either a class (pointing to an embedded asset) or a string
* (containing the path to a file). For texture data, it will also be called after a
* context loss.
*
* <p>The method has to transform this object into one of the types that the AssetManager
* can work with, e.g. a Bitmap, a Sound, XML data, or a ByteArray. This object needs to
* be passed to the 'onComplete' callback.</p>
*
* <p>The calling method will then process this data accordingly (e.g. a Bitmap will be
* transformed into a texture). Unknown types will be available via 'getObject()'.</p>
*
* <p>When overriding this method, you can call 'onProgress' with a number between 0 and 1
* to update the total queue loading progress.</p>
*/
protected function loadRawAsset(rawAsset:Object, onProgress:Function, onComplete:Function):void
{
var extension:String = null;
var loaderInfo:LoaderInfo = null;
var urlLoader:URLLoader = null;
var urlRequest:URLRequest = null;
var url:String = null;
if (rawAsset is Class)
{
setTimeout(complete, 1, new rawAsset());
}
else if (rawAsset is String || rawAsset is URLRequest)
{
urlRequest = rawAsset as URLRequest || new URLRequest(rawAsset as String);
url = urlRequest.url;
extension = getExtensionFromUrl(url);
urlLoader = new URLLoader();
urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onIoError);
urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError);
urlLoader.addEventListener(HTTP_RESPONSE_STATUS, onHttpResponseStatus);
urlLoader.addEventListener(ProgressEvent.PROGRESS, onLoadProgress);
urlLoader.addEventListener(Event.COMPLETE, onUrlLoaderComplete);
urlLoader.load(urlRequest);
}
function onIoError(event:IOErrorEvent):void
{
log("IO error: " + event.text);
dispatchEventWith(Event.IO_ERROR, false, url);
complete(null);
}
function onSecurityError(event:SecurityErrorEvent):void
{
log("security error: " + event.text);
dispatchEventWith(Event.SECURITY_ERROR, false, url);
complete(null);
}
function onHttpResponseStatus(event:HTTPStatusEvent):void
{
if (extension == null)
{
var headers:Array = event["responseHeaders"];
var contentType:String = getHttpHeader(headers, "Content-Type");
if (contentType && /(audio|image)\//.exec(contentType))
extension = contentType.split("/").pop();
}
}
function onLoadProgress(event:ProgressEvent):void
{
if (onProgress != null && event.bytesTotal > 0)
onProgress(event.bytesLoaded / event.bytesTotal);
}
function onUrlLoaderComplete(event:Object):void
{
var bytes:ByteArray = transformData(urlLoader.data as ByteArray, url);
var sound:Sound;
if (bytes == null)
{
complete(null);
return;
}
if (extension)
extension = extension.toLowerCase();
switch (extension)
{
case "mpeg":
case "mp3":
sound = new Sound();
sound.loadCompressedDataFromByteArray(bytes, bytes.length);
bytes.clear();
complete(sound);
break;
case "jpg":
case "jpeg":
case "png":
case "gif":
var loaderContext:LoaderContext = new LoaderContext(_checkPolicyFile);
var loader:Loader = new Loader();
loaderContext.imageDecodingPolicy = ImageDecodingPolicy.ON_LOAD;
loaderInfo = loader.contentLoaderInfo;
loaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onIoError);
loaderInfo.addEventListener(Event.COMPLETE, onLoaderComplete);
loader.loadBytes(bytes, loaderContext);
break;
default: // any XML / JSON / binary data
complete(bytes);
break;
}
}
function onLoaderComplete(event:Object):void
{
urlLoader.data.clear();
complete(event.target.content);
}
function complete(asset:Object):void
{
// clean up event listeners
if (urlLoader)
{
urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, onIoError);
urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError);
urlLoader.removeEventListener(HTTP_RESPONSE_STATUS, onHttpResponseStatus);
urlLoader.removeEventListener(ProgressEvent.PROGRESS, onLoadProgress);
urlLoader.removeEventListener(Event.COMPLETE, onUrlLoaderComplete);
}
if (loaderInfo)
{
loaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, onIoError);
loaderInfo.removeEventListener(Event.COMPLETE, onLoaderComplete);
}
// On mobile, it is not allowed / endorsed to make stage3D calls while the app
// is in the background. Thus, we pause queue processing if that's the case.
if (SystemUtil.isDesktop)
onComplete(asset);
else
SystemUtil.executeWhenApplicationIsActive(onComplete, asset);
}
}
// helpers
/** This method is called by 'enqueue' to determine the name under which an asset will be
* accessible; override it if you need a custom naming scheme. Note that this method won't
* be called for embedded assets.
*
* @param rawAsset either a String, an URLRequest or a FileReference.
*/
protected function getName(rawAsset:Object):String
{
var name:String;
if (rawAsset is String) name = rawAsset as String;
else if (rawAsset is URLRequest) name = (rawAsset as URLRequest).url;
else if (rawAsset is FileReference) name = (rawAsset as FileReference).name;
if (name)
{
name = name.replace(/%20/g, " "); // URLs use '%20' for spaces
name = getBasenameFromUrl(name);
if (name) return name;
else throw new ArgumentError("Could not extract name from String '" + rawAsset + "'");
}
else
{
name = getQualifiedClassName(rawAsset);
throw new ArgumentError("Cannot extract names for objects of type '" + name + "'");
}
}
/** This method is called when raw byte data has been loaded from an URL or a file.
* Override it to process the downloaded data in some way (e.g. decompression) or
* to cache it on disk.
*
* <p>It's okay to call one (or more) of the 'add...' methods from here. If the binary
* data contains multiple objects, this allows you to process all of them at once.
* Return 'null' to abort processing of the current item.</p> */
protected function transformData(data:ByteArray, url:String):ByteArray
{
return data;
}
/** This method is called during loading of assets when 'verbose' is activated. Per
* default, it traces 'message' to the console. */
protected function log(message:String):void
{
if (_verbose) trace("[AssetManager]", message);
}
private function byteArrayStartsWith(bytes:ByteArray, char:String):Boolean
{
var start:int = 0;
var length:int = bytes.length;
var wanted:int = char.charCodeAt(0);
// recognize BOMs
if (length >= 4 &&
(bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xfe && bytes[3] == 0xff) ||
(bytes[0] == 0xff && bytes[1] == 0xfe && bytes[2] == 0x00 && bytes[3] == 0x00))
{
start = 4; // UTF-32
}
else if (length >= 3 && bytes[0] == 0xef && bytes[1] == 0xbb && bytes[2] == 0xbf)
{
start = 3; // UTF-8
}
else if (length >= 2 &&
(bytes[0] == 0xfe && bytes[1] == 0xff) || (bytes[0] == 0xff && bytes[1] == 0xfe))
{
start = 2; // UTF-16
}
// find first meaningful letter
for (var i:int=start; i<length; ++i)
{
var byte:int = bytes[i];
if (byte == 0 || byte == 10 || byte == 13 || byte == 32) continue; // null, \n, \r, space
else return byte == wanted;
}
return false;
}
private function getDictionaryKeys(dictionary:Dictionary, prefix:String="",
out:Vector.<String>=null):Vector.<String>
{
if (out == null) out = new <String>[];
for (var name:String in dictionary)
if (name.indexOf(prefix) == 0)
out[out.length] = name; // avoid 'push'
out.sort(Array.CASEINSENSITIVE);
return out;
}
private function getHttpHeader(headers:Array, headerName:String):String
{
if (headers)
{
for each (var header:Object in headers)
if (header.name == headerName) return header.value;
}
return null;
}
/** Extracts the base name of a file path or URL, i.e. the file name without extension. */
protected function getBasenameFromUrl(url:String):String
{
var matches:Array = NAME_REGEX.exec(url);
if (matches && matches.length > 0) return matches[1];
else return null;
}
/** Extracts the file extension from an URL. */
protected function getExtensionFromUrl(url:String):String
{
var matches:Array = NAME_REGEX.exec(url);
if (matches && matches.length > 1) return matches[2];
else return null;
}
private function prependCallback(oldCallback:Function, newCallback:Function):Function
{
// TODO: it might make sense to add this (together with "appendCallback")
// as a public utility method ("FunctionUtil"?)
if (oldCallback == null) return newCallback;
else if (newCallback == null) return oldCallback;
else return function():void
{
newCallback();
oldCallback();
};
}
// properties
/** The queue contains one 'Object' for each enqueued asset. Each object has 'asset'
* and 'name' properties, pointing to the raw asset and its name, respectively. */
protected function get queue():Array { return _queue; }
/** Returns the number of raw assets that have been enqueued, but not yet loaded. */
public function get numQueuedAssets():int { return _queue.length; }
/** When activated, the class will trace information about added/enqueued assets.
* @default true */
public function get verbose():Boolean { return _verbose; }
public function set verbose(value:Boolean):void { _verbose = value; }
/** Indicates if a queue is currently being loaded. */
public function get isLoading():Boolean { return _numLoadingQueues > 0; }
/** For bitmap textures, this flag indicates if mip maps should be generated when they
* are loaded; for ATF textures, it indicates if mip maps are valid and should be
* used. @default false */
public function get useMipMaps():Boolean { return _defaultTextureOptions.mipMapping; }
public function set useMipMaps(value:Boolean):void { _defaultTextureOptions.mipMapping = value; }
/** Textures that are created from Bitmaps or ATF files will have the scale factor
* assigned here. @default 1 */
public function get scaleFactor():Number { return _defaultTextureOptions.scale; }
public function set scaleFactor(value:Number):void { _defaultTextureOptions.scale = value; }
/** Textures that are created from Bitmaps will be uploaded to the GPU with the
* <code>Context3DTextureFormat</code> assigned to this property. @default "bgra" */
public function get textureFormat():String { return _defaultTextureOptions.format; }
public function set textureFormat(value:String):void { _defaultTextureOptions.format = value; }
/** Indicates if the underlying Stage3D textures should be created as the power-of-two based
* <code>Texture</code> class instead of the more memory efficient <code>RectangleTexture</code>.
* @default false */
public function get forcePotTextures():Boolean { return _defaultTextureOptions.forcePotTexture; }
public function set forcePotTextures(value:Boolean):void { _defaultTextureOptions.forcePotTexture = value; }
/** Specifies whether a check should be made for the existence of a URL policy file before
* loading an object from a remote server. More information about this topic can be found
* in the 'flash.system.LoaderContext' documentation. @default false */
public function get checkPolicyFile():Boolean { return _checkPolicyFile; }
public function set checkPolicyFile(value:Boolean):void { _checkPolicyFile = value; }
/** Indicates if atlas XML data should be stored for access via the 'getXml' method.
* If true, you can access an XML under the same name as the atlas.
* If false, XMLs will be disposed when the atlas was created. @default false. */
public function get keepAtlasXmls():Boolean { return _keepAtlasXmls; }
public function set keepAtlasXmls(value:Boolean):void { _keepAtlasXmls = value; }
/** Indicates if bitmap font XML data should be stored for access via the 'getXml' method.
* If true, you can access an XML under the same name as the bitmap font.
* If false, XMLs will be disposed when the font was created. @default false. */
public function get keepFontXmls():Boolean { return _keepFontXmls; }
public function set keepFontXmls(value:Boolean):void { _keepFontXmls = value; }
/** The maximum number of parallel connections that are spawned when loading the queue.
* More connections can reduce loading times, but require more memory. @default 3. */
public function get numConnections():int { return _numConnections; }
public function set numConnections(value:int):void { _numConnections = value; }
}
}