Skip to content

Instantly share code, notes, and snippets.

@Maligan
Last active July 13, 2017 11:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Maligan/994691ec8ecd27d7a991309f044b9c64 to your computer and use it in GitHub Desktop.
Save Maligan/994691ec8ecd27d7a991309f044b9c64 to your computer and use it in GitHub Desktop.
AssetManager
package
{
import flash.display.Bitmap;
import flash.display.Loader;
import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.HTTPStatusEvent;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.system.ImageDecodingPolicy;
import flash.system.LoaderContext;
import flash.utils.ByteArray;
import flash.utils.Dictionary;
import starling.text.BitmapFont;
import starling.text.TextField;
import starling.textures.AtfData;
import starling.textures.Texture;
import starling.textures.TextureAtlas;
import starling.textures.TextureOptions;
/**
* Usage:
*
* var assets:AssetManager = new AssetManager();
* assets.handle("path/to/file1");
* ...
* assets.handle("path/to/fileN");
*
* And show trace log :-)
*
* This sample support next type of assets:
* - Texture (png, jpg, gif, atf)
* - JSON
* - XML
* - BitmapFont
* - TextureAtlas
*/
public class AssetManager
{
// http://help.adobe.com/en_US/as3/dev/WS5b3ccc516d4fbf351e63e3d118a9b90204-7d66.html
// http://en.wikipedia.org/wiki/List_of_file_signatures
public static const IMAGE_FORMAT_SIGNATURES:Array =
[
"\u0089PNG\r\n\u001A\n", // PNG
"\u00FF\u00D8\u00FF", // JPG (not full there are 3 forms)
"\u0047\u0049\u0046\u0038\u0037\u0061", // GIF87a
"\u0047\u0049\u0046\u0038\u0039\u0061" // GIF89a
];
public static const DOWNLOAD_TRIES:int = 3;
protected var _handlers:Vector.<Handler>;
protected var _registry:Dictionary;
protected var _shared:Object;
private var _verbose:Boolean = true;
private var _checkPolicyFile:Boolean = false;
public function AssetManager()
{
_registry = new Dictionary();
_handlers = new Vector.<Handler>();
_shared = new Object();
addHandler(loadURL, URLRequest);
addHandler(decodeStringToURLRequest, String);
addHandler(decodeByteArrayToTexture, ByteArray);
addHandler(decodeByteArrayToXML, ByteArray);
addHandler(decodeByteArrayToJSON, ByteArray);
addHandler(assembleTextureAtlas, Object);
addHandler(assembleBitmapFont, Object);
}
//
// Basic API
//
public function addHandler(callback:Function, type:Class = null, priority:int = 0):void
{
// Param 'type' define which type of asset handler can process
// this info used as rough filter in handle() method.
// Type added only for remove excess types check within each handler.
_handlers.push(new Handler(callback, type || Object, priority));
_handlers.sort(byPriority);
}
private function byPriority(h1:Handler, h2:Handler):int
{
return h1.priority - h2.priority
}
public function handle(asset:Object, key:String = null, options:Object = null):int
{
for each (var handler:Handler in _handlers)
{
if (asset is handler.type)
{
var status:int = null;
if (handler.callback.length == 1) status = handler.callback(asset);
else if (handler.callback.length == 2) status = handler.callback(asset, key);
else status = handler.callback(asset, key, options);
// Prevent next processing (each handler can stop next processing via return status = -1)
if (status === -1) break;
}
}
return 0;
}
/** This is generic method can be used with wrappers: getTexture, getObject, getSound, getBytes etc. */
public final function getAsset(type:Class, key:String):Object
{
return _registry[type] ? _registry[type][key] : null;
}
public final function addAsset(type:Class, key:String, asset:Object):void
{
_registry[type] ||= new Dictionary();
_registry[type][key] = asset;
log("Added " + type.toString().substr(7).substr(0, -1) + "::" + key);
}
public function removeAsset(type:Class, key:String):void
{
if (type in _registry) delete _registry[type][key]
}
//
// Conveyor Handerls
//
protected function loadURL(value:URLRequest, key:String, options:Object):void
{
download(value, complete, DOWNLOAD_TRIES);
function complete(result:Object):void
{
if (result is ByteArray)
handle(result, key || getName(value.url), options);
else if (result is Error)
log(Error(result).message);
}
}
protected function decodeStringToURLRequest(value:String, key:String, options:Object):void
{
handle(new URLRequest(value), key, options);
}
protected function decodeByteArrayToTexture(value:ByteArray, key:String, options:Object):void
{
if (isBitmapData(value))
{
var loaderContext:LoaderContext = new LoaderContext(_checkPolicyFile);
loaderContext.imageDecodingPolicy = ImageDecodingPolicy.ON_LOAD;
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, complete);
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, complete);
loader.loadBytes(value, loaderContext);
function complete(e:Event):void
{
loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, complete);
loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, complete);
if (e.type == Event.COMPLETE)
{
var bitmap:Bitmap = Bitmap(loader.content);
var texture:Texture = Texture.fromData(bitmap, options as TextureOptions);
addAsset(Texture, key, texture);
handle(texture, key, options);
}
}
}
else if (AtfData.isAtfData(value))
{
var texture:Texture = Texture.fromData(value, options as TextureOptions);
addAsset(Texture, key, texture);
handle(texture, key);
}
}
protected function decodeByteArrayToXML(value:ByteArray, key:String):void
{
if (checkFirstMeanigfulLetter(value, "<"))
{
var xml:XML = null;
try { xml = new XML(value); }
catch (e:Error) { /* NOP */ }
if (xml)
{
addAsset(XML, key, xml);
handle(xml, key);
}
}
}
protected function decodeByteArrayToJSON(value:ByteArray, key:String):void
{
if (checkFirstMeanigfulLetter(value, "{") || checkFirstMeanigfulLetter(value, "["))
{
var object:Object = null;
try { object = JSON.parse(value.toString()); }
catch (e:Error) { /* NOP */ }
if (object)
{
addAsset(Object, key, object);
handle(object, key);
}
}
}
protected function assembleTextureAtlas(value:Object, key:String):void
{
var waiting:Object = (_shared["assembleTextureAtlas"] ||= {});
if (value is XML && XML(value).localName() == "TextureAtlas")
{
var textureKey:String = getName(XML(value).@imagePath);
var texture:Texture = Texture(getAsset(Texture, textureKey));
if (texture == null) waiting[textureKey] = [key, value]; // TODO: it is possible multiple links to one texture
else importAtlas(key, texture, XML(value));
}
else if (value is Texture)
{
if (waiting[key] != null)
{
var atlasKey:String = waiting[key][0];
var atlasXML:XML = waiting[key][1];
importAtlas(atlasKey, Texture(value), atlasXML);
delete waiting[key];
}
}
}
protected function assembleBitmapFont(value:Object, key:String):void
{
var waiting:Object = (_shared["assembleBitmapFont"] ||= {});
if (value is XML && XML(value).localName() == "font")
{
var textureKey:String = getName(XML(value).pages.page.@file);
var texture:Texture = Texture(getAsset(Texture, textureKey));
if (texture == null) waiting[textureKey] = [key, value];
else registerBitmapFont(texture, XML(value));
}
else if (value is Texture)
{
if (waiting[key] != null)
{
var atlasXML:XML = waiting[key][1];
registerBitmapFont(Texture(value), atlasXML);
delete waiting[key];
}
}
}
//
// Utility methods
//
private function download(url:URLRequest, callback:Function, numTries:int):void
{
var urlLoader:URLLoader = new URLLoader();
var urlStatus:int = 0;
urlLoader.addEventListener(Event.COMPLETE, urlLoaderComplete);
urlLoader.addEventListener(IOErrorEvent.IO_ERROR, urlLoaderComplete);
urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoaderComplete);
urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, urlLoaderStatusEvent);
urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
urlLoader.load(url);
function urlLoaderStatusEvent(e:HTTPStatusEvent):void
{
urlStatus = e.status;
}
function urlLoaderComplete(e:Event):void
{
urlLoader.removeEventListener(Event.COMPLETE, urlLoaderComplete);
urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, urlLoaderComplete);
urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoaderComplete);
urlLoader.removeEventListener(HTTPStatusEvent.HTTP_STATUS, urlLoaderStatusEvent);
if (e.type == Event.COMPLETE)
callback(urlLoader.data);
else if (e.type == IOErrorEvent.IO_ERROR && numTries>0 && (urlStatus==0 || urlStatus==408 || urlStatus==503))
download(url, callback, numTries-1);
else
callback(new Error(ErrorEvent(e).text + ". RC: " + urlStatus));
}
}
protected function isBitmapData(bytes:ByteArray):Boolean
{
for each (var signature:String in IMAGE_FORMAT_SIGNATURES)
if (checkSignature(bytes, signature))
return true;
return false;
}
private function importAtlas(key:String, texture:Texture, xml:XML):void
{
var atlas:TextureAtlas = new TextureAtlas(texture, xml);
addAsset(TextureAtlas, key, atlas);
for each (var textureName:String in atlas.getNames())
addAsset(Texture, textureName, atlas.getTexture(textureName));
}
private function registerBitmapFont(texture:Texture, xml:XML):void
{
var font:BitmapFont = new BitmapFont(texture, xml);
TextField.registerCompositor(font, font.name);
log("Added BitmapFont::" + font.name);
}
/** Extracts the base name of a file path or URL, i.e. the file name without extension. */
protected final function getName(url:String):String
{
var NAME_REGEX:RegExp = /([^\?\/\\]+?)(?:\.([\w\-]+))?(?:\?.*)?$/;
var matches:Array = NAME_REGEX.exec(url);
if (matches && matches.length > 0) return matches[1];
else return null;
}
/** Check whenever byte array starts with signature. */
protected final function checkSignature(bytes:ByteArray, signature:String):Boolean
{
if (bytes.bytesAvailable < signature.length) return false;
for (var i:int = 0; i < signature.length; i++)
if (signature.charCodeAt(i) != bytes[i]) return false;
return true;
}
protected final function checkFirstMeanigfulLetter(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;
}
public function log(message:String):void
{
if (_verbose) trace("[AssetManager]", message);
}
}
}
class Handler
{
public var callback:Function;
public var type:Class;
public var priority:int;
public function Handler(callback:Function, type:Class, priority:int)
{
this.callback = callback;
this.type = type;
this.priority = priority;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment