UNPKG

oxygen-core

Version:

Oxygen game engine (Xenon Core for browsers)

509 lines (456 loc) 14.5 kB
import System from '../System'; import Asset from './Asset'; import Events from '../../utils/Events'; import parser from '../../utils/jsonParser'; export { Asset }; const _pathRegex = /(\w+)(\:\/\/)(.*)/; /** * Assets database and loader. * * @example * const system = new AssetSystem('assets/', { cache: 'no-store' }, AssetSystem.fetchArrayView); */ export default class AssetSystem extends System { /** * Default browser fetch mechanism. * * @param {*} args - Fetch engine parameters. * * @return {Promise} Promise that fetches file. */ static fetch(...args) { return fetch(...args); } /** * Custom fetch mechanism that loads data from array view. * * @param {ArrayBufferView} view - Data array buffer view. * @param {string} path - Asset path. * @param {*} options - Fetch engine options. * @param {Function} fallbackEngine - Fallback fetch engine. * * @return {Promise} Promise that fetches file from array view. */ static fetchArrayView(view, path, options = {}, fallbackEngine = null) { if (!!view && !ArrayBuffer.isView(view)) { throw new Error('`view` is not type of ArrayView!'); } if (typeof path !== 'string') { throw new Error('`path` is not type of String!'); } if (!!fallbackEngine && !(fallbackEngine instanceof Function)) { throw new Error('`fallbackEngine` is not type of Function!'); } try { if (!view) { if (!!fallbackEngine) { return fallbackEngine(path, options); } else { return Promise.resolve(new Response(new Blob(), { status: 404 })); } } else { return Promise.resolve( new Response(new Blob([ view ]), { status: 200 }) ); } } catch(error) { return Promise.reject(error); } } /** * Web fetch mechanism generator that loads data from specified address. * * @param {string} address - Assets hosting address. * @param {Function} fallbackEngine - Fallback fetch engine. * * @return {Function} Function that fetches file from web. */ static makeFetchEngineWeb(address, fallbackEngine = AssetSystem.fetch) { if (typeof address !== 'string') { throw new Error('`address` is not type of String!'); } return (path, options) => AssetSystem.fetch( `${address}/${path}`, options, fallbackEngine ); } /** @type {string} */ get pathPrefix() { return this._pathPrefix; } /** @type {*} */ get fetchOptions() { return this._fetchOptions; } /** @type {Function} */ get fetchEngine() { return this._fetchEngine; } /** @type {Function} */ set fetchEngine(value) { if (!(value instanceof Function)) { throw new Error('`value` is not type of Function!'); } this._fetchEngine = value; } /** @type {Events} */ get events() { return this._events; } /** * Constructor. * * @param {string|null} pathPrefix - Path prefix used for every requested asset or null no path prefix. * @param {*|null} fetchOptions - Custom fetch options or null if default will be used. * @param {Function|null} fetchEngine - Custom fetch engine or null if default will be used. */ constructor(pathPrefix, fetchOptions, fetchEngine) { super(); if (!!pathPrefix && typeof pathPrefix !== 'string') { throw new Error('`pathPrefix` is not type of String!'); } this._pathPrefix = pathPrefix || ''; this._fetchOptions = fetchOptions || {}; this._fetchEngine = fetchEngine || AssetSystem.fetch; this._assets = new Map(); this._loaders = new Map(); this._events = new Events(); this._loaded = 0; this._toLoad = 0; } /** * Destructor (dispose internal resources and clear assets database). * * @example * system.dispose(); * system = null; */ dispose() { const { _assets, _loaders, _events } = this; for (const asset of _assets.values()) { asset.dispose(); } _assets.clear(); _loaders.clear(); _events.dispose(); } /** * Register assets loader under given protocol. * * @param {string} protocol - Assets loader protocol name. * @param {Function} assetConstructor - Asset factory. * * @example * system.registerProtocol('json', JSONAsset.factory); */ registerProtocol(protocol, assetConstructor) { if (typeof protocol !== 'string') { throw new Error('`protocol` is not type of String!'); } if (!(assetConstructor instanceof Function)) { throw new Error('`assetConstructor` is not type of Function!'); } const { _loaders } = this; if (_loaders.has(protocol)) { throw new Error(`There is already registered protocol: ${protocol}`); } _loaders.set(protocol, assetConstructor); } /** * Unregister given assets loader protocol. * * @param {string} protocol - Assets loader protocol name. * * @example * system.unregisterProtocol('json'); */ unregisterProtocol(protocol) { if (typeof protocol !== 'string') { throw new Error('`protocol` is not type of String!'); } if (!this._loaders.delete(protocol)) { throw new Error(`There is no registered protocol: ${protocol}`); } } /** * Get asset by it's full path. * * @param {string} path - Asset path (with protocol). * * @return {Asset|null} Asset instance if found or null otherwise. * * @example * const config = system.get('json://config.json'); */ get(path) { if (typeof path !== 'string') { throw new Error('`path` is not type of String!'); } return this._assets.get(path) || null; } /** * Load asset from given path. * * @param {string} path - Asset path (with protocol). * * @return {Promise} Promise of fetch engine loader. * * @example * system.load('json://config.json').then(asset => console.log(asset.data)); */ load(path) { this._events.trigger('progress', this._loaded, this._toLoad); ++this._toLoad; return this._load(path) .then(asset => { ++this._loaded; this._events.trigger('progress', this._loaded, this._toLoad); --this._loaded; --this._toLoad; this._events.trigger('progress', this._loaded, this._toLoad); return asset; }) .catch(error => { ++this._loaded; this._events.trigger('progress', this._loaded, this._toLoad); --this._loaded; --this._toLoad; this._events.trigger('progress', this._loaded, this._toLoad); throw error; }); } /** * Load list of assets in sequence (one by one). * * @param {Array.<string>} paths - Array of assets paths. * * @return {Promise} Promise of fetch engine loader. * * @example * const list = [ 'json://config.json', 'text://hello.txt' ]; * system.loadSequence(list).then(assets => console.log(assets.map(a => a.data))); */ async loadSequence(paths) { if (!(paths instanceof Array)) { throw new Error('`paths` is not type of Array!'); } const result = []; this._events.trigger('progress', this._loaded, this._toLoad); this._toLoad += paths.length; for (let i = 0, c = paths.length; i < c; ++i) { result.push( await this._load(paths[i]) .then(asset => { ++this._loaded; this._events.trigger('progress', this._loaded, this._toLoad); return asset; }) .catch(error => { ++this._loaded; this._events.trigger('progress', this._loaded, this._toLoad); throw error; }) ); } this._loaded -= paths.length; this._toLoad -= paths.length; this._events.trigger('progress', this._loaded, this._toLoad); return result; } /** * Load list of assets possibly all at the same time (asynchronously). * * @param {Array.<string>} paths - Array of assets paths. * * @return {Promise} Promise of fetch engine loader. * * @example * const list = [ 'json://config.json', 'text://hello.txt' ]; * system.loadAll(list).then(assets => console.log(assets.map(a => a.data))); */ async loadAll(paths) { if (!(paths instanceof Array)) { throw new Error('`paths` is not type of Array!'); } this._events.trigger('progress', this._loaded, this._toLoad); this._toLoad += paths.length; return Promise.all(paths.map( path => this._load(path) .then(asset => { ++this._loaded; this._events.trigger('progress', this._loaded, this._toLoad); return asset; }) .catch(error => { ++this._loaded; this._events.trigger('progress', this._loaded, this._toLoad); throw error; }) )) .then(assets => { this._loaded -= paths.length; this._toLoad -= paths.length; this._events.trigger('progress', this._loaded, this._toLoad); return assets; }) .catch(error => { this._loaded -= paths.length; this._toLoad -= paths.length; this._events.trigger('progress', this._loaded, this._toLoad); throw error; }); } /** * Load asset from given path with specified fetch engine. * * @param {string} path - Asset path (with protocol). * @param {Function} fetchEngine - Fetch engine used to fetch asset. * * @return {Promise} Promise of fetch engine loader. * * @example * system.loadWithFetchEngine('json://config.json', AssetSystem.fetch).then(asset => console.log(asset.data)); */ async loadWithFetchEngine(path, fetchEngine) { const fe = this.fetchEngine; this.fetchEngine = fetchEngine; const result = await this.load(path); this.fetchEngine = fe; return result; } /** * Load list of assets in sequence (one by one) with specified fetch engine. * * @param {Array.<string>} paths - Array of assets paths. * @param {Function} fetchEngine - Fetch engine used to fetch assets. * * @return {Promise} Promise of fetch engine loader. * * @example * const list = [ 'json://config.json', 'text://hello.txt' ]; * system.loadSequenceWithFetchEngine(list, AssetSystem.fetch).then(assets => console.log(assets.map(a => a.data))); */ async loadSequenceWithFetchEngine(paths, fetchEngine) { const fe = this.fetchEngine; this.fetchEngine = fetchEngine; const result = await this.loadSequence(paths); this.fetchEngine = fe; return result; } /** * Load list of assets possibly all at the same time (asynchronously) with specified fetch engine. * * @param {Array.<string>} paths - Array of assets paths. * @param {Function} fetchEngine - Fetch engine used to fetch assets. * * @return {Promise} Promise of fetch engine loader. * * @example * const list = [ 'json://config.json', 'text://hello.txt' ]; * system.loadAllWithFetchEngine(list, AssetSystem.fetch).then(assets => console.log(assets.map(a => a.data))); */ async loadAllWithFetchEngine(paths, fetchEngine) { const fe = this.fetchEngine; this.fetchEngine = fetchEngine; const result = await this.loadAll(paths); this.fetchEngine = fe; return result; } /** * Unload asset by path and remove from database. * * @param {string} path - Asset path (with protocol). * * @example * system.unload('json://config.json'); */ unload(path) { const { _assets } = this; const asset = _assets.get(path); if (!asset) { throw new Error(`Trying to unload non-existing asset: ${path}`); } this._events.trigger('unload', asset); asset.dispose(); _assets.delete(path); } /** * Unload all assets from paths list. * * @param {Array.<string>} paths - Array of assets paths. */ unloadAll(paths) { if (!(paths instanceof Array)) { throw new Error('`paths` is not type of Array!'); } for (const path of paths) { this.unload(path); } } /** * @override */ onUnregister() { dispose(); } _load(path) { if (typeof path !== 'string') { throw new Error('`path` is not type of String!'); } const found = path.lastIndexOf('|'); if (found < 0) { return this._loadPart(path); } else { const prefix = path.substr(0, found).trim(); const container = this._assets.get(prefix); if (!container) { throw new Error(`There is no loaded subassets container: ${prefix}`); } const oldEngine = this.fetchEngine; const postfix = path.substr(found + 1).trim(); this.fetchEngine = container.makeFetchEngine(oldEngine); return this._loadPart(postfix, path).finally(() => { this.fetchEngine = oldEngine; }); } } _loadPart(path, key = null) { if (typeof path !== 'string') { throw new Error('`path` is not type of String!'); } let options = null; const found = path.indexOf('?'); if (found >= 0) { options = parser.parse(path.substr(found + 1)); path = path.substr(0, found); } if (!key) { key = path; } const result = _pathRegex.exec(path); if (!result) { throw new Error(`\`path\` does not conform asset path name rules: ${path}`); } const [ , protocol,, filename ] = result; const loader = this._loaders.get(protocol); if (!loader) { throw new Error(`There is no registered protocol: ${protocol}`); } const { _assets } = this; if (_assets.has(path)) { return Promise.resolve(_assets.get(path)); } const asset = loader(this, protocol, filename, options); if (!(asset instanceof Asset)) { throw new Error( `Cannot create asset for file: ${filename} of protocol: ${protocol}` ); } return asset.load().then(data => { this._assets.set(key, asset); this._events.trigger('load', asset); return data; }); } }