Source

RSCache.js

import * as DataViewImport from './helpers/DataView.js'

import CacheDefinitionLoader from './CacheDefinitionLoader.js'
import CacheRequester from './CacheRequester.js'

import Index from './cacheTypes/Index.js'
import CacheLoader from './CacheLoader.js'

import IndexType from './cacheTypes/IndexType.js'
import ConfigType from './cacheTypes/ConfigType.js'

/**
 * @typedef options
 * @property {boolean} threaded Use a Web Worker to read from the cache. Slower than non-threaded since threading overhead is big. Useful for web apps to have a responsive UI
 * @property {boolean} isAnimaya Used while loading animations. Uses a different loading method for Animaya animations. OSRSCacheReader sets this internally on it's own if using ModelDefinition's loadAnimation method
 * @property {boolean} earlyStop Used while loading Animaya animations. Stops the reader early to just get the skeleton info
 * @property {boolean} loadSprites Used while loading Texture defs. Automatically grabs the corresponding sprite file
 * @property {boolean} cacheResults Save def on archive file after decompressing contents. Useful for quicker loading if loading the same thing multiple times but also can increase memory usage
 */

/**
 * Creates a RSCache reader
 * @category Base
 * @param {string} cacheRootDir 
 * @param {function(number):void} progressFunc Progress function callback. Passes 1 parameter which is the amount of progress from the last step (not total progress)
 */
class RSCache {
	constructor(cacheRootDir = "./", progressFunc = () => {}) {
		this.indicies = {};
		this.progressFunc = progressFunc;

		const cacheLoader = new CacheLoader(cacheRootDir);

		this.onload = cacheLoader.getResults().then(result => {
			this.cacheRequester = new CacheRequester(result.datFile);

			return this.#loadCacheFiles(result.indexFiles).then(() => {
				this.cacheRequester.setXteas(result.xteas);
			});

		});
	}

	#progress(amount) {
		this.progressFunc(amount);
	}

	/**
	 * Get a cache Index file. 
	 * @method
	 * @param {(Number | IndexType)} index 
	 * @returns [Index]{@link Index}
	 */
	getIndex(index) {
		let indexId;
		if (index.constructor.name === "Object") {
			indexId = index.id;
		} else if (!isNaN(index)) {
			indexId = Number(index);
		}

		index = this.indicies[indexId];
		if (index == undefined) {
			throw "Index " + indexId + " does not exist";
		}

		return index;
	}

	#checkIfCachingResults(options, indexType) {
		if (options.cacheResults == undefined) {
			if (indexType.id == IndexType.MODELS.id || indexType.id == IndexType.MAPS.id) { // dont save models and maps if cacheResults isnt set
				options.cacheResults = false;
			} else {
				options.cacheResults = true;
			}
		}
	}

	/**
	 * Gets all of the files from an archive and loads their definitions if possible.
	 * @param {(Number | IndexType)} indexId Can be a number or IndexType
	 * @param {(Number)} archiveId Can be a number but also can be a ConfigType if IndexType is CONFIG
	 * @param {options} options 
	 * @returns Array<[File]{@link File}>
	 */
	async getAllFiles(indexId, archiveId, options = {}) {
		try {
			let index = this.getIndex(indexId);
			let archive = index.getArchive(archiveId);
			this.#checkIfCachingResults(options, index);

			if (archive.filesLoaded) {
				return archive.files;
			}

			let data;
			if (options.threaded) {
				data = await this.cacheRequester.readDataThreaded(index, index.indexSegments[archive.id].size, index.indexSegments[archive.id].segment, archive.id);
			} else {
				data = await this.cacheRequester.readData(index, index.indexSegments[archive.id].size, index.indexSegments[archive.id].segment, archive.id);
			}
			archive = index.archives[data.archiveId];
			archive.loadFiles(data.decompressedData);

			let filePromise = new CacheDefinitionLoader(data.index.id, archive, options).loadAllFiles(this);

			if (options.cacheResults) { //it will readData again since filesLoaded will be false
				archive.filesLoaded = true;
			}

			return filePromise;
		} catch (e) {
			console.log(e)
		}
	}

	/**
	 * Gets a single file from an archive and load its definition if possible.
	 * @param {(Number | IndexType)} indexId Can be a number or IndexType
	 * @param {(Number)} archiveId Can be a number but also can be a ConfigType if IndexType is CONFIG
	 * @param {Number} fileId Id of the file to get from the archive
	 * @param {options} options 
	 * @returns [File]{@link File}
	 */
	async getFile(indexId, archiveId, fileId = 0, options = {}) {
		return this.getAllFiles(indexId, archiveId, options).then((x) => x[fileId]);
	}

	/**
	 * Helper method to map getAllFiles results to definitions
	 * @param {(Number | IndexType)} indexId Can be a number or IndexType
	 * @param {(Number)} archiveId Can be a number but also can be a ConfigType if IndexType is CONFIG
	 * @param {options} options 
	 * @returns Definition
	 */
	async getAllDefs(indexId, archiveId, options = {}) {
		try {
			let files = await this.getAllFiles(indexId, archiveId, options);
			return files.map(x => x.def);
		} catch (e) {
			console.log(e)
		}
	}

	/**
	 * Helper method to map getFile results to its definition
	 * @param {(Number | IndexType)} indexId Can be a number or IndexType
	 * @param {(Number)} archiveId Can be a number but also can be a ConfigType if IndexType is CONFIG
	 * @param {Number} fileId Id of the definition to get from the archive
	 * @param {options} options 
	 * @returns Definition
	 */
	async getDef(indexId, archiveId, fileId = 0, options = {}) {
		return this.getAllDefs(indexId, archiveId, options).then((x) => x[fileId]);
	}

	/**
	 * Helper method to get a NPC definition
	 * @param {Number} id NPC Id
	 * @param {options} options 
	 * @returns [NpcDefinition]{@link NpcDefinition}
	 */
	async getNPC(id, options = {}) {
		return this.getDef(IndexType.CONFIGS, ConfigType.NPC, id, options);
	}

	/**
	 * Helper method to get an Item definition
	 * @param {Number} id Item Id
	 * @param {options} options 
	 * @returns ItemDefinition
	 */
	async getItem(id, options = {}) {
		return this.getDef(IndexType.CONFIGS, ConfigType.ITEM, id, options);
	}

	/**
	 * Helper method to get an Object definition
	 * @param {Number} id Object Id
	 * @param {options} options 
	 * @returns ObjectDefinition
	 */
	async getObject(id, options = {}) {
		return this.getDef(IndexType.CONFIGS, ConfigType.OBJECT, id, options);
	}

	/** Closes the cache and cleans up the web worker pool */
	close() {
		this.cacheRequester.workerPool.finish();
	}

	#loadCacheFiles(indexFiles) {
		let idx255Data = indexFiles[indexFiles.length - 1];
		let idxFileData = indexFiles.slice(0, indexFiles.length - 1);

		//theres probably a better way of doing this
		//also not completely sure yet if this really needs to be done for index 255
		//return Promise.all(idxFiles).then((idxFileData) => {
		for (let i = 0; i <= idxFileData.length; i++) {
			let dataview;
			if (i == idxFileData.length) { //ugly fix, needs to be improved
				dataview = new DataView(idx255Data.buffer);
				i = 255;
			} else {
				if (idxFileData[i] == undefined) continue;
				dataview = new DataView(idxFileData[i].buffer);
			}
			this.indicies[i] = new Index(i);
			for (let j = 0; j < dataview.byteLength; j += 6) {
				let size = dataview.readUint24();
				let segment = dataview.readUint24();
				this.indicies[i].indexSegments.push({ size, segment });
			}

		};

		this.#progress(40);
		return this.#loadIndicies(idx255Data);
	}

	#loadIndicies(idxData) {
		let dataview = new DataView(idxData.buffer);
		//could probably use the indexSegments or remove the weird i = 255 part from loadCacheFiles
		//might look better if j++, but works for now

		let indexPromises = [];

		for (let j = 0; j < dataview.byteLength; j += 6) {
			let size = dataview.readUint24();
			let segment = dataview.readUint24();
			let index = this.indicies[j / 6];
			if (index == undefined) continue;
			let data = this.cacheRequester.readData(index, size, segment);
			//since this is async now the onload is considered complete before its completed
			//this call is completed before loadIndexData is completed
			//the onload promise needs to complete when all of the loadIndexDatas have completed
			data.then(x => {
				this.indicies[x.index.id].loadIndexData(x.decompressedData);
				this.#progress((60) / (dataview.byteLength / 6));
			});

			indexPromises.push(data);
		}

		this.indexPromises = indexPromises;
		return Promise.all(indexPromises);
	}

}

export default RSCache;