package lime.utils; import haxe.io.Path; import lime.app.Event; import lime.app.Future; import lime.app.Promise; import lime.media.AudioBuffer; import lime.graphics.Image; import lime.net.HTTPRequest; import lime.text.Font; import lime.utils.AssetType; #if flash import flash.display.BitmapData; import flash.media.Sound; #end #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end @:access(lime.text.Font) @:access(lime.utils.Assets) class AssetLibrary { public var onChange = new EventVoid>(); @:noCompletion private var assetsLoaded:Int; @:noCompletion private var assetsTotal:Int; @:noCompletion private var bytesLoaded:Int; @:noCompletion private var bytesLoadedCache:Map; @:noCompletion private var bytesTotal:Int; @:noCompletion private var cachedAudioBuffers = new Map(); @:noCompletion private var cachedBytes = new Map(); @:noCompletion private var cachedFonts = new Map(); @:noCompletion private var cachedImages = new Map(); @:noCompletion private var cachedText = new Map(); @:noCompletion private var classTypes = new Map>(); @:noCompletion private var loaded:Bool; @:noCompletion private var pathGroups = new Map>(); @:noCompletion private var paths = new Map(); @:noCompletion private var preload = new Map(); @:noCompletion private var promise:Promise; @:noCompletion private var sizes = new Map(); @:noCompletion private var types = new Map(); public function new() { bytesLoaded = 0; bytesTotal = 0; } public function exists(id:String, type:String):Bool { var requestedType = type != null ? cast(type, AssetType) : null; var assetType = types.get(id); if (assetType != null) { if (assetType == requestedType || ((requestedType == SOUND || requestedType == MUSIC) && (assetType == MUSIC || assetType == SOUND))) { return true; } #if flash if (requestedType == BINARY && (assetType == BINARY || assetType == TEXT || assetType == IMAGE)) { return true; } else if (requestedType == TEXT && assetType == BINARY) { return true; } else if (requestedType == null || paths.exists(id)) { return true; } #else if (requestedType == BINARY || requestedType == null || (assetType == BINARY && requestedType == TEXT)) { return true; } #end } return false; } public function checkSpecificAsset(id:String, type:String):Bool { return switch (type) { case BINARY: cachedBytes.exists(id) || classTypes.exists(id) || paths.exists(id); case FONT: cachedFonts.exists(id) || classTypes.exists(id) || paths.exists(id); case IMAGE: cachedImages.exists(id) || classTypes.exists(id) || paths.exists(id); case MUSIC, SOUND: cachedAudioBuffers.exists(id) || classTypes.exists(id) || paths.exists(id); case TEXT: cachedText.exists(id) || cachedBytes.exists(id) || paths.exists(id); case TEMPLATE: false; default: throw "Unknown asset type: " + type; } } public static function fromBytes(bytes:Bytes, rootPath:String = null):AssetLibrary { return fromManifest(AssetManifest.fromBytes(bytes, rootPath)); } public static function fromFile(path:String, rootPath:String = null):AssetLibrary { return fromManifest(AssetManifest.fromFile(path, rootPath)); } public static function fromBundle(bundle:AssetBundle):AssetLibrary { if (bundle.data.exists("library.json")) { var manifest = AssetManifest.fromBytes(bundle.data.get("library.json")); if (manifest != null) { var library:AssetLibrary = null; if (manifest.libraryType == null) { library = new AssetLibrary(); } else { var libraryClass = Type.resolveClass(manifest.libraryType); if (libraryClass != null) { library = Type.createInstance(libraryClass, manifest.libraryArgs); } else { Log.warn("Could not find library type: " + manifest.libraryType); return null; } } library.__fromBundle(bundle, manifest); return library; } } else { var library = new AssetLibrary(); library.__fromBundle(bundle); return library; } return null; } public static function fromManifest(manifest:AssetManifest):AssetLibrary { if (manifest == null) return null; var library:AssetLibrary = null; if (manifest.libraryType == null) { library = new AssetLibrary(); } else { var libraryClass = Type.resolveClass(manifest.libraryType); if (libraryClass != null) { library = Type.createInstance(libraryClass, manifest.libraryArgs); } else { Log.warn("Could not find library type: " + manifest.libraryType); return null; } } library.__fromManifest(manifest); return library; } public function getAsset(id:String, type:String):Dynamic { return switch (type) { case BINARY: getBytes(id); case FONT: getFont(id); case IMAGE: getImage(id); case MUSIC, SOUND: getAudioBuffer(id); case TEXT: getText(id); case TEMPLATE: throw "Not sure how to get template: " + id; default: throw "Unknown asset type: " + type; } } public function getAudioBuffer(id:String):AudioBuffer { if (cachedAudioBuffers.exists(id)) { return cachedAudioBuffers.get(id); } else if (classTypes.exists(id)) { #if flash var buffer = new AudioBuffer(); buffer.src = cast(Type.createInstance(classTypes.get(id), []), Sound); return buffer; #else return AudioBuffer.fromBytes(cast(Type.createInstance(classTypes.get(id), []), Bytes)); #end } else { return AudioBuffer.fromFile(getPath(id)); } } public function getBytes(id:String):Bytes { if (cachedBytes.exists(id)) { return cachedBytes.get(id); } else if (cachedText.exists(id)) { var bytes = Bytes.ofString(cachedText.get(id)); cachedBytes.set(id, bytes); return bytes; } else if (classTypes.exists(id)) { #if flash var data = Type.createInstance(classTypes.get(id), []); switch (types.get(id)) { case TEXT, BINARY: return Bytes.ofData(cast(Type.createInstance(classTypes.get(id), []), flash.utils.ByteArray)); case IMAGE: var bitmapData = cast(Type.createInstance(classTypes.get(id), []), BitmapData); return Bytes.ofData(bitmapData.getPixels(bitmapData.rect)); default: return null; } #else return cast(Type.createInstance(classTypes.get(id), []), Bytes); #end } else { return Bytes.fromFile(getPath(id)); } } public function getFont(id:String):Font { if (cachedFonts.exists(id)) { return cachedFonts.get(id); } else if (classTypes.exists(id)) { #if flash var src = Type.createInstance(classTypes.get(id), []); var font = new Font(src.fontName); font.src = src; return font; #else return cast(Type.createInstance(classTypes.get(id), []), Font); #end } else { return Font.fromFile(getPath(id)); } } public function getImage(id:String):Image { if (cachedImages.exists(id)) { return cachedImages.get(id); } else if (classTypes.exists(id)) { #if flash return Image.fromBitmapData(cast(Type.createInstance(classTypes.get(id), []), BitmapData)); #else return cast(Type.createInstance(classTypes.get(id), []), Image); #end } else { return Image.fromFile(getPath(id)); } } public function getPath(id:String):String { if (paths.exists(id)) { return paths.get(id); } else if (pathGroups.exists(id)) { return pathGroups.get(id)[0]; } else { return null; } } public function getText(id:String):String { if (cachedText.exists(id)) { return cachedText.get(id); } else { var bytes = getBytes(id); if (bytes == null) { return null; } else { return bytes.getString(0, bytes.length); } } } public function isLocal(id:String, type:String):Bool { #if sys return true; #else if (classTypes.exists(id)) { return true; } return switch (cast(type, AssetType)) { case null: cachedBytes.exists(id) || cachedText.exists(id) || cachedImages.exists(id) || cachedAudioBuffers.exists(id) || cachedFonts.exists(id); case IMAGE: cachedImages.exists(id); case MUSIC, SOUND: cachedAudioBuffers.exists(id); case FONT: cachedFonts.exists(id); default: cachedBytes.exists(id) || cachedText.exists(id); } #end } public function list(type:String):Array { var requestedType = type != null ? cast(type, AssetType) : null; var items = []; for (id in types.keys()) { if (requestedType == null || exists(id, type)) { items.push(id); } } return items; } public function loadAsset(id:String, type:String):Future { return switch (type) { case BINARY: loadBytes(id); case FONT: loadFont(id); case IMAGE: loadImage(id); case MUSIC, SOUND: loadAudioBuffer(id); case TEXT: loadText(id); case TEMPLATE: throw "Not sure how to load template: " + id; default: throw "Unknown asset type: " + type; } } public function load():Future { if (loaded) { return Future.withValue(this); } if (promise == null) { promise = new Promise(); bytesLoadedCache = new Map(); assetsLoaded = 0; assetsTotal = 1; for (id in preload.keys()) { if (!preload.get(id)) continue; Log.verbose("Preloading asset: " + id + " [" + types.get(id) + "]"); switch (types.get(id)) { case BINARY: assetsTotal++; var future = loadBytes(id); future.onProgress(load_onProgress.bind(id)); future.onError(load_onError.bind(id)); future.onComplete(loadBytes_onComplete.bind(id)); case FONT: assetsTotal++; var future = loadFont(id); future.onProgress(load_onProgress.bind(id)); future.onError(load_onError.bind(id)); future.onComplete(loadFont_onComplete.bind(id)); case IMAGE: assetsTotal++; var future = loadImage(id); future.onProgress(load_onProgress.bind(id)); future.onError(load_onError.bind(id)); future.onComplete(loadImage_onComplete.bind(id)); case MUSIC, SOUND: assetsTotal++; var future = loadAudioBuffer(id); future.onProgress(load_onProgress.bind(id)); future.onError(loadAudioBuffer_onError.bind(id)); future.onComplete(loadAudioBuffer_onComplete.bind(id)); case TEXT: assetsTotal++; var future = loadText(id); future.onProgress(load_onProgress.bind(id)); future.onError(load_onError.bind(id)); future.onComplete(loadText_onComplete.bind(id)); default: } } __assetLoaded(null); } return promise.future; } public function loadAudioBuffer(id:String):Future { if (cachedAudioBuffers.exists(id)) { return Future.withValue(cachedAudioBuffers.get(id)); } else if (classTypes.exists(id)) { return Future.withValue(AudioBuffer.fromBytes(cast(Type.createInstance(classTypes.get(id), []), Bytes))); } else { if (pathGroups.exists(id)) { return AudioBuffer.loadFromFiles(pathGroups.get(id)); } else { return AudioBuffer.loadFromFile(paths.get(id)); } } } public function loadBytes(id:String):Future { if (cachedBytes.exists(id)) { return Future.withValue(cachedBytes.get(id)); } else if (classTypes.exists(id)) { #if flash return Future.withValue(Bytes.ofData(Type.createInstance(classTypes.get(id), []))); #else return Future.withValue(Type.createInstance(classTypes.get(id), [])); #end } else { return Bytes.loadFromFile(getPath(id)); } } public function loadFont(id:String):Future { if (cachedFonts.exists(id)) { return Future.withValue(cachedFonts.get(id)); } else if (classTypes.exists(id)) { var font:Font = Type.createInstance(classTypes.get(id), []); #if (js && html5) return font.__loadFromName(font.name); #else return Future.withValue(font); #end } else { #if (js && html5) return Font.loadFromName(getPath(id)); #else return Font.loadFromFile(getPath(id)); #end } } public static function loadFromBytes(bytes:Bytes, rootPath:String = null):Future { return AssetManifest.loadFromBytes(bytes, rootPath).then(function(manifest) { return loadFromManifest(manifest); }); } public static function loadFromFile(path:String, rootPath:String = null):Future { return AssetManifest.loadFromFile(path, rootPath).then(function(manifest) { return loadFromManifest(manifest); }); } public static function loadFromManifest(manifest:AssetManifest):Future { var library = fromManifest(manifest); if (library != null) { return library.load(); } else { return cast Future.withError("Could not load asset manifest"); } } public function loadImage(id:String):Future { if (cachedImages.exists(id)) { return Future.withValue(cachedImages.get(id)); } else if (classTypes.exists(id)) { return Future.withValue(Type.createInstance(classTypes.get(id), [])); } else if (cachedBytes.exists(id)) { return Image.loadFromBytes(cachedBytes.get(id)).then(function(image) { cachedBytes.remove(id); cachedImages.set(id, image); return Future.withValue(image); }); } else { return Image.loadFromFile(getPath(id)); } } public function loadText(id:String):Future { if (cachedText.exists(id)) { return Future.withValue(cachedText.get(id)); } else if (cachedBytes.exists(id) || classTypes.exists(id)) { var bytes = getBytes(id); if (bytes == null) { return cast Future.withValue(null); } else { var text = bytes.getString(0, bytes.length); cachedText.set(id, text); return Future.withValue(text); } } else { var request = new HTTPRequest(); return request.load(getPath(id)); } } public function unload():Void { #if haxe4 cachedBytes.clear(); cachedFonts.clear(); cachedImages.clear(); cachedAudioBuffers.clear(); cachedText.clear(); #else cachedBytes = new Map(); cachedFonts = new Map(); cachedImages = new Map(); cachedText = new Map(); classTypes = new Map>(); #end } @:noCompletion private function __assetLoaded(id:String):Void { assetsLoaded++; if (id != null) { Log.verbose("Loaded asset: " + id + " [" + types.get(id) + "] (" + (assetsLoaded - 1) + "/" + (assetsTotal - 1) + ")"); } if (id != null) { var size = sizes.exists(id) ? sizes.get(id) : 0; if (!bytesLoadedCache.exists(id)) { bytesLoaded += size; } else { var cache = bytesLoadedCache.get(id); if (cache < size) { bytesLoaded += (size - cache); } } bytesLoadedCache.set(id, size); } if (assetsLoaded < assetsTotal) { promise.progress(bytesLoaded, bytesTotal); } else { loaded = true; promise.progress(bytesTotal, bytesTotal); promise.complete(this); } } @:noCompletion private function __cacheBreak(path:String):String { return Assets.__cacheBreak(path); } @:noCompletion private function __fromBundle(bundle:AssetBundle, manifest:AssetManifest = null):Void { if (manifest != null) { var id, data, type:AssetType; for (asset in manifest.assets) { id = Reflect.hasField(asset, "id") ? asset.id : asset.path; data = bundle.data.get(asset.path); if (Reflect.hasField(asset, "type")) { type = asset.type; switch (type) { #if !web case IMAGE: cachedImages.set(id, Image.fromBytes(data)); case MUSIC, SOUND: cachedAudioBuffers.set(id, AudioBuffer.fromBytes(data)); case FONT: cachedFonts.set(id, Font.fromBytes(data)); #end case TEXT: cachedText.set(id, data != null ? Std.string(data) : null); default: cachedBytes.set(id, data); } types.set(id, asset.type); } else { cachedBytes.set(id, data); types.set(id, BINARY); } } } else { for (path in bundle.paths) { cachedBytes.set(path, bundle.data.get(path)); types.set(path, BINARY); } } } @:noCompletion private function __fromManifest(manifest:AssetManifest):Void { var hasSize = (manifest.version >= 2); var size, id, pathGroup:Array, classRef; var basePath = manifest.rootPath; if (basePath == null) basePath = ""; if (basePath != "") basePath += "/"; for (asset in manifest.assets) { size = hasSize && Reflect.hasField(asset, "size") ? asset.size : 100; id = Reflect.hasField(asset, "id") ? asset.id : asset.path; if (Reflect.hasField(asset, "path")) { paths.set(id, __cacheBreak(__resolvePath(basePath + Reflect.field(asset, "path")))); } if (Reflect.hasField(asset, "pathGroup")) { pathGroup = Reflect.field(asset, "pathGroup"); for (i in 0...pathGroup.length) { pathGroup[i] = __cacheBreak(__resolvePath(basePath + pathGroup[i])); } pathGroups.set(id, pathGroup); } sizes.set(id, size); types.set(id, asset.type); if (Reflect.hasField(asset, "preload")) { preload.set(id, Reflect.field(asset, "preload")); } if (Reflect.hasField(asset, "className")) { classRef = Type.resolveClass(Reflect.field(asset, "className")); #if (js && html5 && modular) if (classRef == null) { classRef = untyped $hx_exports[asset.className]; } #end classTypes.set(id, classRef); } } bytesTotal = 0; for (asset in manifest.assets) { id = Reflect.hasField(asset, "id") ? asset.id : asset.path; if (preload.exists(id) && preload.get(id) && sizes.exists(id)) { bytesTotal += sizes.get(id); } } } @:noCompletion private function __resolvePath(path:String):String { path = StringTools.replace(path, "\\", "/"); var colonIdx:Int = path.indexOf(":"); if (StringTools.startsWith(path, "http") && colonIdx > 0) { var lastSlashIdx:Int = colonIdx + 3; var httpSection:String = path.substr(0, lastSlashIdx); path = httpSection + StringTools.replace(path.substr(lastSlashIdx), "//", "/"); } else { path = StringTools.replace(path, "//", "/"); } #if android if (StringTools.startsWith(path, "./")) { path = path.substr(2); } #end if (path.indexOf("./") > -1) { var split = path.split("/"); var newPath = []; for (i in 0...split.length) { if (split[i] == "..") { if (i == 0 || newPath[i - 1] == "..") { newPath.push(".."); } else { newPath.pop(); } } else if (split[i] == ".") { if (i == 0) { newPath.push("."); } } else { newPath.push(split[i]); } } path = newPath.join("/"); } return path; } // Event Handlers @:noCompletion private function loadAudioBuffer_onComplete(id:String, audioBuffer:AudioBuffer):Void { cachedAudioBuffers.set(id, audioBuffer); if (pathGroups.exists(id)) { var pathGroup = pathGroups.get(id); for (otherID in pathGroups.keys()) { if (otherID == id) continue; for (path in pathGroup) { if (pathGroups.get(otherID).indexOf(path) > -1) { cachedAudioBuffers.set(otherID, audioBuffer); break; } } } } __assetLoaded(id); } @:noCompletion private function loadAudioBuffer_onError(id:String, message:Dynamic):Void { #if (js && html5) if (message != null && message != "") { Log.warn("Could not load \"" + id + "\": " + Std.string(message)); } else { Log.warn("Could not load \"" + id + "\""); } loadAudioBuffer_onComplete(id, new AudioBuffer()); #else load_onError(id, message); #end } @:noCompletion private function loadBytes_onComplete(id:String, bytes:Bytes):Void { cachedBytes.set(id, bytes); __assetLoaded(id); } @:noCompletion private function loadFont_onComplete(id:String, font:Font):Void { cachedFonts.set(id, font); __assetLoaded(id); } @:noCompletion private function loadImage_onComplete(id:String, image:Image):Void { cachedImages.set(id, image); __assetLoaded(id); } @:noCompletion private function loadText_onComplete(id:String, text:String):Void { cachedText.set(id, text); __assetLoaded(id); } @:noCompletion private function load_onError(id:String, message:Dynamic):Void { if (message != null && message != "") { promise.error("Error loading asset \"" + id + "\": " + Std.string(message)); } else { promise.error("Error loading asset \"" + id + "\""); } } @:noCompletion private function load_onProgress(id:String, bytesLoaded:Int, bytesTotal:Int):Void { if (bytesLoaded > 0) { var size = sizes.get(id); var percent; if (bytesTotal > 0) { // Use a ratio in case the real bytesTotal is different than our precomputed total percent = (bytesLoaded / bytesTotal); if (percent > 1) percent = 1; bytesLoaded = Math.floor(percent * size); } else if (bytesLoaded > size) { bytesLoaded = size; } if (bytesLoadedCache.exists(id)) { var cache = bytesLoadedCache.get(id); if (bytesLoaded != cache) { this.bytesLoaded += (bytesLoaded - cache); } } else { this.bytesLoaded += bytesLoaded; } bytesLoadedCache.set(id, bytesLoaded); promise.progress(this.bytesLoaded, this.bytesTotal); } } }