AssetManager.js

/**
 * This class is instantiated by Platypus at `platypus.assetCache` to track assets: loading assets needed for particular layers and unloading assets once they're no longer needed.
 *
 * @memberof platypus
 * @class AssetManager
**/
/* global platypus, setTimeout */
import Data from './Data.js';
import DataMap from './DataMap.js';
import {Assets} from 'pixi.js';
import {arrayCache} from './utils/array.js';

const
    fn = /^(?:\w+:\/{2}\w+(?:\.\w+)*\/?)?(?:[\/.]*?(?:[^?]+)?\/)?(?:([^\/?]+?)(?:\.(\w+|{\w+(?:,\w+)*}))?)(?:\?\S*)?$/,
    formatAsset = function (asset) { //TODO: Make this behavior less opaque.
        const
            path = asset.src ?? asset,
            match = path.match(fn);
            
        if (asset.id) {
            platypus.debug.warn(`AssetManager: Use "alias" instead of "id" for asset identification. (${asset.id})`);
        }

        return Data.setUp(
            'alias', asset.alias ?? [asset.id ?? (match ? match[1] : path)],
            'src', path,
            'data', asset.data ?? null
        );
    };

export default class AssetManager {
    constructor () {
        this.assets = DataMap.setUp();
        this.counts = Data.setUp();
    }

    /**
     * This method removes an asset from memory if it's the last needed instance of the asset.
     *
     * @method platypus.AssetManager#delete
     * @param {*} alias
     * @return {Boolean} Returns `true` if asset was removed from asset cache.
     */
    delete (alias) {
        const assets = this.assets;

        if (assets.has(alias)) {
            const counts = this.counts;

            counts[alias] -= 1;
            if (counts[alias] === 0) {
                const asset = assets.get(alias);
                
                if (asset && asset.src) {
                    asset.src = '';
                }
                assets.delete(alias);
            }

            return !counts[alias];
        } else {
            return false;
        }
    }

    /**
     * Returns a loaded instance of a given asset.
     *
     * @method platypus.AssetManager#get
     * @param {*} alias
     * @return {Object} Returns the asset if defined.
     */
    get (alias) {
        return this.assets.get(alias);
    }

    /**
     * Returns alias for given path.
     *
     * @method platypus.AssetManager#getFileId
     * @param {*} path
     * @return {String} Returns alias generated from path.
     */
    getFileId (path) {
        const match = path.match(fn);

        return match ? match[1] : path;
    }

    /**
     * Returns whether a given asset is currently loaded by the AssetManager.
     *
     * @method platypus.AssetManager#has
     * @param {*} alias
     * @return {Object} Returns `true` if the asset is loaded and `false` if not.
     */
    has (alias) {
        return this.assets.has(alias);
    }

    /**
     * Sets a mapping between an alias and a loaded asset. If the mapping already exists, simply increment the count for a given alias.
     *
     * @method platypus.AssetManager#set
     * @param {*} alias
     * @param {*} value The loaded asset.
     * @param {Number} count The number of assets needed.
     */
    set (alias, value, count = 1) {
        const
            assets = this.assets,
            counts = this.counts;

        if (assets.has(alias)) {
            counts[alias] += count;
        } else {
            assets.set(alias, value);
            counts[alias] = count;
        }
    }

    /**
     * Loads a list of assets.
     *
     * @method platypus.AssetManager#load
     * @param {Array} list A list of assets to load.
     * @param {Function} one This function is called as each asset is loaded.
     * @param {Function} all This function is called once all assets in the list are loaded.
     */
    async load (list, one, all) {
        const
            counts = this.counts,
            needsLoading = arrayCache.setUp(),
            adds = Data.setUp();

        if (platypus.game?.options?.images ?? platypus.game?.options?.audio) {
            platypus.debug.warn('AssetManager: Default asset folders are no longer specified via game options. Include pathing in asset imports.');
        }

        // work-around for Pixi v7 Spine PMA issue https://github.com/pixijs/pixijs/issues/9141
        Assets.setPreferences({
            preferCreateImageBitmap: false,
            preferWorker: false
        });

        for (let i = 0; i < list.length; i++) {
            const
                item = formatAsset(list[i]),
                alias = item.alias?.[0] ?? item.id ?? item.src ?? item;

            if (this.has(alias)) {
                counts[alias] += 1;
            } else if (adds.hasOwnProperty(alias)) {
                adds[alias] += 1;
            } else {
                adds[alias] = 1;
                needsLoading.push(item);
            }
        }

        if (needsLoading.length) {
            // Do this first to pass `data` property if needed
            needsLoading.forEach((asset) => Assets.add(asset));

            const
                aliases = needsLoading.map((asset) => asset.alias[0]),
                loadedList = await Assets.load(aliases, one);

            aliases.forEach((alias) => {
                const
                    response = loadedList[alias];

                this.set(alias, response, adds[alias]);
            });

            if (all) {
                all();
            }
        } else {
            setTimeout(() => { // To run in same async sequence as above.
                if (one) {
                    one(1);
                }
                if (all) {
                    all();
                }
            }, 1);
        }

        arrayCache.recycle(needsLoading);
    }

    /**
     * Unloads a list of assets. This is identical to passing each item in the list to `.delete()`.
     *
     * @method platypus.AssetManager#unload
     * @param {Array} list A list of assets to unload.
     */
    unload (list) {
        let i = list.length;

        while (i--) {
            this.delete(list[i].alias?.[0] ?? list[i].id ?? list[i]);
        }
    }
}