/** * @author Richard Davey * @copyright 2018 Photon Storm Ltd. * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} */ var Class = require('../utils/Class'); var CONST = require('./const'); var CustomSet = require('../structs/Set'); var EventEmitter = require('eventemitter3'); var FileTypesManager = require('./FileTypesManager'); var GetFastValue = require('../utils/object/GetFastValue'); var ParseXMLBitmapFont = require('../gameobjects/bitmaptext/ParseXMLBitmapFont'); var PluginManager = require('../boot/PluginManager'); var XHRSettings = require('./XHRSettings'); /** * @typedef {object} LinkFileObject * * @property {string} type - [description] * @property {Phaser.Loader.File} fileA - [description] * @property {Phaser.Loader.File} fileB - [description] */ /** * @typedef {object} LoaderFileObject * * @property {string} key - [description] * @property {string} type - [description] * @property {string} [url] - [description] * @property {string[]} [urls] - [description] * @property {string} [textureURL] - [description] * @property {string} [atlasURL] - [description] * @property {string} [xmlURL] - [description] * @property {string[]} [textureURLs] - [description] * @property {string[]} [atlasURLs] - [description] * @property {object} [config] - [description] * @property {object} [json] - [description] * @property {XHRSettingsObject} [xhrSettings] - [description] * @property {XHRSettingsObject} [textureXhrSettings] - [description] * @property {XHRSettingsObject} [atlasXhrSettings] - [description] * @property {XHRSettingsObject} [xmlXhrSettings] - [description] * @property {XHRSettingsObject} [audioXhrSettings] - [description] * @property {XHRSettingsObject} [jsonXhrSettings] - [description] */ /** * @classdesc * [description] * * @class LoaderPlugin * @extends Phaser.Events.EventEmitter * @memberOf Phaser.Loader * @constructor * @since 3.0.0 * * @param {Phaser.Scene} scene - [description] */ var LoaderPlugin = new Class({ Extends: EventEmitter, initialize: function LoaderPlugin (scene) { EventEmitter.call(this); /** * [description] * * @name Phaser.Loader.LoaderPlugin#scene * @type {Phaser.Scene} * @since 3.0.0 */ this.scene = scene; /** * [description] * * @name Phaser.Loader.LoaderPlugin#systems * @type {Phaser.Scenes.Systems} * @since 3.0.0 */ this.systems = scene.sys; /** * [description] * * @name Phaser.Loader.LoaderPlugin#_multilist * @type {object} * @private * @default {} * @since 3.0.0 */ this._multilist = {}; // Inject the available filetypes into the Loader FileTypesManager.install(this); var gameConfig = this.systems.game.config; var sceneConfig = this.systems.settings.loader; /** * [description] * * @name Phaser.Loader.LoaderPlugin#path * @type {string} * @default '' * @since 3.0.0 */ this.path = ''; /** * [description] * * @name Phaser.Loader.LoaderPlugin#baseURL * @type {string} * @default '' * @since 3.0.0 */ this.baseURL = ''; this.setBaseURL(GetFastValue(sceneConfig, 'baseURL', gameConfig.loaderBaseURL)); this.setPath(GetFastValue(sceneConfig, 'path', gameConfig.loaderPath)); /** * [description] * * @name Phaser.Loader.LoaderPlugin#enableParallel * @type {boolean} * @since 3.0.0 */ this.enableParallel = GetFastValue(sceneConfig, 'enableParallel', gameConfig.loaderEnableParallel); /** * [description] * * @name Phaser.Loader.LoaderPlugin#maxParallelDownloads * @type {integer} * @since 3.0.0 */ this.maxParallelDownloads = GetFastValue(sceneConfig, 'maxParallelDownloads', gameConfig.loaderMaxParallelDownloads); /** * xhr specific global settings (can be overridden on a per-file basis) * * @name Phaser.Loader.LoaderPlugin#xhr * @type {XHRSettingsObject} * @since 3.0.0 */ this.xhr = XHRSettings( GetFastValue(sceneConfig, 'responseType', gameConfig.loaderResponseType), GetFastValue(sceneConfig, 'async', gameConfig.loaderAsync), GetFastValue(sceneConfig, 'user', gameConfig.loaderUser), GetFastValue(sceneConfig, 'password', gameConfig.loaderPassword), GetFastValue(sceneConfig, 'timeout', gameConfig.loaderTimeout) ); /** * [description] * * @name Phaser.Loader.LoaderPlugin#crossOrigin * @type {string} * @since 3.0.0 */ this.crossOrigin = GetFastValue(sceneConfig, 'crossOrigin', gameConfig.loaderCrossOrigin); /** * [description] * * @name Phaser.Loader.LoaderPlugin#totalToLoad * @type {number} * @default 0 * @since 3.0.0 */ this.totalToLoad = 0; /** * [description] * * @name Phaser.Loader.LoaderPlugin#progress * @type {number} * @default 0 * @since 3.0.0 */ this.progress = 0; /** * [description] * * @name Phaser.Loader.LoaderPlugin#list * @type {Phaser.Structs.Set.} * @since 3.0.0 */ this.list = new CustomSet(); /** * [description] * * @name Phaser.Loader.LoaderPlugin#inflight * @type {Phaser.Structs.Set.} * @since 3.0.0 */ this.inflight = new CustomSet(); /** * [description] * * @name Phaser.Loader.LoaderPlugin#failed * @type {Phaser.Structs.Set.} * @since 3.0.0 */ this.failed = new CustomSet(); /** * [description] * * @name Phaser.Loader.LoaderPlugin#queue * @type {Phaser.Structs.Set.} * @since 3.0.0 */ this.queue = new CustomSet(); /** * [description] * * @name Phaser.Loader.LoaderPlugin#storage * @type {Phaser.Structs.Set.<(Phaser.Loader.File|LinkFileObject)>} * @since 3.0.0 */ this.storage = new CustomSet(); /** * [description] * * @name Phaser.Loader.LoaderPlugin#state * @type {integer} * @since 3.0.0 */ this.state = CONST.LOADER_IDLE; scene.sys.events.on('start', this.boot, this); }, /** * This method is called automatically by the Scene when it is starting up. * It is responsible for creating local systems, properties and listening for Scene events. * Do not invoke it directly. * * @method Phaser.Loader.LoaderPlugin#boot * @private * @since 3.0.0 */ boot: function () { var eventEmitter = this.systems.events; eventEmitter.once('shutdown', this.shutdown, this); eventEmitter.once('destroy', this.destroy, this); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#setBaseURL * @since 3.0.0 * * @param {string} url - [description] * * @return {Phaser.Loader.LoaderPlugin} This Loader object. */ setBaseURL: function (url) { if (url !== '' && url.substr(-1) !== '/') { url = url.concat('/'); } this.baseURL = url; return this; }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#setPath * @since 3.0.0 * * @param {string} path - [description] * * @return {Phaser.Loader.LoaderPlugin} This Loader object. */ setPath: function (path) { if (path !== '' && path.substr(-1) !== '/') { path = path.concat('/'); } this.path = path; return this; }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#setCORS * @since 3.0.0 * * @param {string} crossOrigin - [description] * * @return {Phaser.Loader.LoaderPlugin} This Loader object. */ setCORS: function (crossOrigin) { this.crossOrigin = crossOrigin; return this; }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#addFile * @since 3.0.0 * * @param {Phaser.Loader.File} file - [description] * * @return {Phaser.Loader.File} [description] */ addFile: function (file) { if (!this.isReady()) { return -1; } file.path = this.path; this.list.set(file); return file; }, /** * Is the Loader actively loading (or processing loaded files) * * @method Phaser.Loader.LoaderPlugin#isLoading * @since 3.0.0 * * @return {boolean} [description] */ isLoading: function () { return (this.state === CONST.LOADER_LOADING || this.state === CONST.LOADER_PROCESSING); }, /** * Is the Loader ready to start a new load? * * @method Phaser.Loader.LoaderPlugin#isReady * @since 3.0.0 * * @return {boolean} [description] */ isReady: function () { return (this.state === CONST.LOADER_IDLE || this.state === CONST.LOADER_COMPLETE); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#start * @since 3.0.0 */ start: function () { if (!this.isReady()) { return; } this.progress = 0; this.totalToLoad = this.list.size; this.emit('start', this); if (this.list.size === 0) { this.finishedLoading(); } else { this.state = CONST.LOADER_LOADING; this.failed.clear(); this.inflight.clear(); this.queue.clear(); this.queue.debug = true; this.updateProgress(); this.processLoadQueue(); } }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#updateProgress * @since 3.0.0 */ updateProgress: function () { this.progress = 1 - ((this.list.size + this.inflight.size) / this.totalToLoad); this.emit('progress', this.progress); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#processLoadQueue * @since 3.0.0 */ processLoadQueue: function () { this.list.each(function (file) { if (file.state === CONST.FILE_POPULATED || (file.state === CONST.FILE_PENDING && this.inflight.size < this.maxParallelDownloads)) { this.inflight.set(file); this.list.delete(file); this.loadFile(file); } if (this.inflight.size === this.maxParallelDownloads) { // Tells the Set iterator to abort return false; } }, this); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#loadFile * @since 3.0.0 * * @param {Phaser.Loader.File} file - [description] */ loadFile: function (file) { // If the file doesn't have its own crossOrigin set, // we'll use the Loaders (which is undefined by default) if (!file.crossOrigin) { file.crossOrigin = this.crossOrigin; } file.load(this); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#nextFile * @since 3.0.0 * * @param {Phaser.Loader.File} previousFile - [description] * @param {boolean} success - [description] */ nextFile: function (previousFile, success) { // Move the file that just loaded from the inflight list to the queue or failed Set if (success) { this.emit('load', previousFile); this.queue.set(previousFile); } else { this.emit('loaderror', previousFile); this.failed.set(previousFile); } this.inflight.delete(previousFile); this.updateProgress(); if (this.list.size > 0) { this.processLoadQueue(); } else if (this.inflight.size === 0) { this.finishedLoading(); } }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#finishedLoading * @since 3.0.0 */ finishedLoading: function () { if (this.state === CONST.LOADER_PROCESSING) { return; } this.progress = 1; this.state = CONST.LOADER_PROCESSING; this.storage.clear(); if (this.queue.size === 0) { // Everything failed, so nothing to process this.processComplete(); } else { this.queue.each(function (file) { file.onProcess(this.processUpdate.bind(this)); }, this); } }, /** * Called automatically by the File when it has finished processing. * * @method Phaser.Loader.LoaderPlugin#processUpdate * @since 3.0.0 * * @param {Phaser.Loader.File} file - [description] */ processUpdate: function (file) { // This file has failed to load, so move it to the failed Set if (file.state === CONST.FILE_ERRORED) { this.failed.set(file); if (file.linkFile) { this.queue.delete(file.linkFile); } return this.removeFromQueue(file); } // If we got here, then the file loaded // Special handling for multi-part files if (file.linkFile) { if (file.state === CONST.FILE_COMPLETE && file.linkFile.state === CONST.FILE_COMPLETE) { // Partner has loaded, so add them both to Storage this.storage.set({ type: file.linkType, fileA: file, fileB: file.linkFile }); this.queue.delete(file.linkFile); this.removeFromQueue(file); } } else { this.storage.set(file); this.removeFromQueue(file); } }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#removeFromQueue * @since 3.0.0 * * @param {Phaser.Loader.File} file - [description] */ removeFromQueue: function (file) { this.queue.delete(file); if (this.queue.size === 0 && this.state === CONST.LOADER_PROCESSING) { // We've processed all the files we loaded this.processComplete(); } }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#processComplete * @since 3.0.0 */ processComplete: function () { this.list.clear(); this.inflight.clear(); this.queue.clear(); this.processCallback(); this.state = CONST.LOADER_COMPLETE; this.emit('complete', this, this.storage.size, this.failed.size); // Move to a User setting: // this.removeAllListeners(); }, /** * The Loader has finished. * * @method Phaser.Loader.LoaderPlugin#processCallback * @since 3.0.0 */ processCallback: function () { if (this.storage.size === 0) { return; } // The global Texture Manager var cache = this.scene.sys.cache; var textures = this.scene.sys.textures; var anims = this.scene.sys.anims; // Process multiatlas groups first var file; var fileA; var fileB; for (var key in this._multilist) { var data = []; var images = []; var keys = this._multilist[key]; for (var i = 0; i < keys.length; i++) { file = this.storage.get('key', keys[i]); if (file) { if (file.type === 'image') { images.push(file.data); } else if (file.type === 'json') { data.push(file.data); } this.storage.delete(file); } } // Do we have everything needed? if (images.length + data.length === keys.length) { // Yup, add them to the Texture Manager // Is the data JSON Hash or JSON Array? if (Array.isArray(data[0].textures) || Array.isArray(data[0].frames)) { textures.addAtlasJSONArray(key, images, data); } else { textures.addAtlasJSONHash(key, images, data); } } } // Process all of the files // Because AnimationJSON may require images to be loaded first, we process them last var animJSON = []; this.storage.each(function (file) { switch (file.type) { case 'animationJSON': animJSON.push(file); break; case 'image': case 'svg': case 'html': textures.addImage(file.key, file.data); break; case 'atlasjson': fileA = file.fileA; fileB = file.fileB; if (fileA.type === 'image') { textures.addAtlas(fileA.key, fileA.data, fileB.data); } else { textures.addAtlas(fileB.key, fileB.data, fileA.data); } break; case 'dataimage': fileA = file.fileA; fileB = file.fileB; if (fileA.linkParent) { textures.addImage(fileA.key, fileA.data, fileB.data); } else { textures.addImage(fileB.key, fileB.data, fileA.data); } break; case 'unityatlas': fileA = file.fileA; fileB = file.fileB; if (fileA.type === 'image') { textures.addUnityAtlas(fileA.key, fileA.data, fileB.data); } else { textures.addUnityAtlas(fileB.key, fileB.data, fileA.data); } break; case 'bitmapfont': fileA = file.fileA; fileB = file.fileB; if (fileA.type === 'image') { cache.bitmapFont.add(fileB.key, { data: ParseXMLBitmapFont(fileB.data), texture: fileA.key, frame: null }); textures.addImage(fileA.key, fileA.data); } else { cache.bitmapFont.add(fileA.key, { data: ParseXMLBitmapFont(fileA.data), texture: fileB.key, frame: null }); textures.addImage(fileB.key, fileB.data); } break; case 'spritesheet': textures.addSpriteSheet(file.key, file.data, file.config); break; case 'json': cache.json.add(file.key, file.data); break; case 'xml': cache.xml.add(file.key, file.data); break; case 'text': cache.text.add(file.key, file.data); break; case 'obj': cache.obj.add(file.key, file.data); break; case 'binary': cache.binary.add(file.key, file.data); break; case 'audio': cache.audio.add(file.key, file.data); break; case 'audioSprite': var files = [ file.fileA, file.fileB ]; files.forEach(function (file) { cache[file.type].add(file.key, file.data); }); break; case 'glsl': cache.shader.add(file.key, file.data); break; case 'tilemapCSV': case 'tilemapJSON': cache.tilemap.add(file.key, { format: file.tilemapFormat, data: file.data }); break; } }); animJSON.forEach(function (file) { anims.fromJSON(file.data); }); this.storage.clear(); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#saveJSON * @since 3.0.0 * * @param {*} data - [description] * @param {string} [filename=file.json] - [description] * * @return {Phaser.Loader.LoaderPlugin} This Loader plugin. */ saveJSON: function (data, filename) { return this.save(JSON.stringify(data), filename); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#save * @since 3.0.0 * * @param {*} data - [description] * @param {string} [filename=file.json] - [description] * @param {string} [filetype=application/json] - [description] * * @return {Phaser.Loader.LoaderPlugin} This Loader plugin. */ save: function (data, filename, filetype) { if (filename === undefined) { filename = 'file.json'; } if (filetype === undefined) { filetype = 'application/json'; } var blob = new Blob([ data ], { type: filetype }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.download = filename; a.textContent = 'Download ' + filename; a.href = url; a.click(); return this; }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#reset * @since 3.0.0 */ reset: function () { this.list.clear(); this.inflight.clear(); this.failed.clear(); this.queue.clear(); this.storage.clear(); var gameConfig = this.systems.game.config; var sceneConfig = this.systems.settings.loader; this.setBaseURL(GetFastValue(sceneConfig, 'baseURL', gameConfig.loaderBaseURL)); this.setPath(GetFastValue(sceneConfig, 'path', gameConfig.loaderPath)); this.state = CONST.LOADER_IDLE; }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#loadArray * @since 3.0.0 * * @param {LoaderFileObject[]} files - [description] * * @return {boolean} [description] */ loadArray: function (files) { if (Array.isArray(files)) { for (var i = 0; i < files.length; i++) { this.file(files[i]); } } return (this.list.size > 0); }, /** * [description] * * @method Phaser.Loader.LoaderPlugin#file * @since 3.0.0 * * @param {LoaderFileObject} file - [description] * * @return {Phaser.Loader.File} [description] */ file: function (file) { var entry; var key = file.key; switch (file.type) { case 'spritesheet': entry = this.spritesheet(key, file.url, file.config, file.xhrSettings); break; case 'atlas': entry = this.atlas(key, file.textureURL, file.atlasURL, file.textureXhrSettings, file.atlasXhrSettings); break; case 'bitmapFont': entry = this.bitmapFont(key, file.textureURL, file.xmlURL, file.textureXhrSettings, file.xmlXhrSettings); break; case 'multiatlas': entry = this.multiatlas(key, file.textureURLs, file.atlasURLs, file.textureXhrSettings, file.atlasXhrSettings); break; case 'audioSprite': entry = this.audioSprite(key, file.urls, file.json, file.config, file.audioXhrSettings, file.jsonXhrSettings); break; // image, json, xml, binary, text, glsl, svg, obj default: entry = this[file.type](key, file.url, file.xhrSettings); break; } return entry; }, /** * The Scene that owns this plugin is shutting down. * We need to kill and reset all internal properties as well as stop listening to Scene events. * * @method Phaser.Loader.LoaderPlugin#shutdown * @private * @since 3.0.0 */ shutdown: function () { this.reset(); this.state = CONST.LOADER_SHUTDOWN; var eventEmitter = this.systems.events; eventEmitter.off('shutdown', this.shutdown, this); }, /** * The Scene that owns this plugin is being destroyed. * We need to shutdown and then kill off all external references. * * @method Phaser.Loader.LoaderPlugin#destroy * @private * @since 3.0.0 */ destroy: function () { this.shutdown(); this.state = CONST.LOADER_DESTROYED; this.scene.sys.events.off('start', this.start, this); this.list = null; this.inflight = null; this.failed = null; this.queue = null; this.storage = null; this.scene = null; this.systems = null; } }); PluginManager.register('Loader', LoaderPlugin, 'load'); module.exports = LoaderPlugin;