UNPKG

@runejs/filestore

Version:

Tools for managing the RuneJS filestore.

316 lines (315 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpriteStore = exports.SpritePack = exports.Sprite = void 0; exports.toRgb = toRgb; const common_1 = require("@runejs/common"); const node_fs_1 = require("node:fs"); const pngjs_1 = require("pngjs"); const filestore_1 = require("../filestore"); const util_1 = require("../util"); function toRgb(inputNum) { let num = inputNum; num >>>= 0; const b = num & 0xff; const g = (num & 0xff00) >>> 8; const r = (num & 0xff0000) >>> 16; return [r, g, b]; } /** * A single Sprite within a SpritePack. */ class Sprite { spriteId; maxWidth; maxHeight; offsetX; offsetY; width; height; pixelIdx; palette; pixels; constructor(spriteId, width, height) { this.spriteId = spriteId; this.maxWidth = this.width = width; this.maxHeight = this.height = height; } resizeToLibSize() { if (this.width !== this.maxWidth || this.height !== this.maxHeight) { const resizedPixels = new Array(this.maxWidth * this.maxHeight); let pixelCount = 0; for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { resizedPixels[x + this.offsetX + (y + this.offsetY) * this.maxWidth] = this.pixelIdx[pixelCount++]; } } this.pixelIdx = resizedPixels; this.width = this.maxWidth; this.height = this.maxHeight; this.offsetX = 0; this.offsetY = 0; } } /** * First converts the Sprite into a base64 PNG image. */ async toBase64() { return await (0, util_1.pngToBase64)(this.toPng()); } /** * Converts the Sprite into a PNG image and returns the resulting PNG object. */ toPng() { const png = new pngjs_1.PNG({ width: this.width, height: this.height, filterType: -1, }); for (let x = 0; x < this.width; x++) { for (let y = 0; y < this.height; y++) { const pixel = this.pixels[this.width * y + x]; const [r, g, b] = toRgb(pixel); const pngIndex = (this.width * y + x) << 2; png.data[pngIndex] = r; png.data[pngIndex + 1] = g; png.data[pngIndex + 2] = b; png.data[pngIndex + 3] = pixel >> 24; } } return png; } /** * Converts the Sprite's pixels into a Uint8ClampedArray. */ getPixels() { return new Uint8ClampedArray(this.toPng().data); } } exports.Sprite = Sprite; /** * A package of one or many Sprite objects. */ class SpritePack { fileData; _sprites; constructor(fileData) { this.fileData = fileData; } async writeToDisk() { return new Promise((resolve, reject) => { try { const fileName = (0, filestore_1.getFileName)(this.fileData.nameHash).replace(/ /g, '_'); if (!(0, node_fs_1.existsSync)('./unpacked/sprite-packs')) { (0, node_fs_1.mkdirSync)('./unpacked/sprite-packs'); } if (this._sprites.length > 1) { if (!(0, node_fs_1.existsSync)(`./unpacked/sprite-packs/${this.fileData.fileId}_${fileName}`)) { (0, node_fs_1.mkdirSync)(`./unpacked/sprite-packs/${this.fileData.fileId}_${fileName}`); } for (let i = 0; i < this._sprites.length; i++) { try { const sprite = this._sprites[i]; let png; if (!sprite) { png = new pngjs_1.PNG({ width: 1, height: 1, fill: false, bgColor: { red: 0, green: 0, blue: 0, }, }); } else { png = sprite.toPng(); } png.pack(); const pngBuffer = pngjs_1.PNG.sync.write(png); (0, node_fs_1.writeFileSync)(`./unpacked/sprite-packs/${this.fileData.fileId}_${fileName}/${i}.png`, pngBuffer); } catch (e) { common_1.logger.error('Error writing sprite to disk', e); } } } else if (this._sprites.length === 1) { const sprite = this._sprites[0]; if (!sprite) { reject('No sprite data found.'); } else { const png = sprite.toPng(); png.pack(); const pngBuffer = pngjs_1.PNG.sync.write(png); (0, node_fs_1.writeFileSync)(`./unpacked/sprite-packs/${this.fileData.fileId}_${fileName}.png`, pngBuffer); } } resolve(); } catch (error) { reject(error); } }); } /** * Decodes the sprite pack file. */ decode() { const buffer = this.fileData.decompress(); if (buffer.length === 0) { throw new Error(`Empty file content for Sprite Pack ${this.fileData.fileId}.`); } buffer.readerIndex = buffer.length - 2; const spriteCount = buffer.get('SHORT', 'UNSIGNED'); const sprites = new Array(spriteCount); buffer.readerIndex = buffer.length - 7 - spriteCount * 8; const width = buffer.get('SHORT', 'UNSIGNED'); const height = buffer.get('SHORT', 'UNSIGNED'); const paletteLength = buffer.get('BYTE', 'UNSIGNED') + 1; for (let i = 0; i < spriteCount; i++) { sprites[i] = new Sprite(i, width, height); } for (let i = 0; i < spriteCount; i++) { sprites[i].offsetX = buffer.get('SHORT', 'UNSIGNED'); } for (let i = 0; i < spriteCount; i++) { sprites[i].offsetY = buffer.get('SHORT', 'UNSIGNED'); } for (let i = 0; i < spriteCount; i++) { sprites[i].width = buffer.get('SHORT', 'UNSIGNED'); } for (let i = 0; i < spriteCount; i++) { sprites[i].height = buffer.get('SHORT', 'UNSIGNED'); } buffer.readerIndex = buffer.length - 7 - spriteCount * 8 - (paletteLength - 1) * 3; const palette = new Array(paletteLength); for (let i = 1; i < paletteLength; i++) { palette[i] = buffer.get('INT24'); if (palette[i] === 0) { palette[i] = 1; } } buffer.readerIndex = 0; for (let i = 0; i < spriteCount; i++) { const sprite = sprites[i]; const spriteWidth = sprite.width; const spriteHeight = sprite.height; const dimension = spriteWidth * spriteHeight; const pixelPaletteIndicies = new Array(dimension); const pixelAlphas = new Array(dimension); sprite.palette = palette; const flags = buffer.get('BYTE', 'UNSIGNED'); if ((flags & 0b01) === 0) { for (let j = 0; j < dimension; j++) { pixelPaletteIndicies[j] = buffer.get('BYTE'); } } else { for (let x = 0; x < spriteWidth; x++) { for (let y = 0; y < spriteHeight; y++) { pixelPaletteIndicies[spriteWidth * y + x] = buffer.get('BYTE'); } } } if ((flags & 0b10) === 0) { for (let j = 0; j < dimension; j++) { const index = pixelPaletteIndicies[j]; if (index !== 0) { pixelAlphas[j] = 0xff; } } } else { if ((flags & 0b01) === 0) { for (let j = 0; j < dimension; j++) { pixelAlphas[j] = buffer.get('BYTE'); } } else { for (let x = 0; x < spriteWidth; x++) { for (let y = 0; y < spriteHeight; y++) { pixelAlphas[spriteWidth * y + x] = buffer.get('BYTE'); } } } } sprite.pixelIdx = pixelPaletteIndicies; sprite.pixels = new Array(dimension); for (let j = 0; j < dimension; j++) { const index = pixelPaletteIndicies[j] & 0xff; sprite.pixels[j] = palette[index] | (pixelAlphas[j] << 24); } } this._sprites = sprites; return this; } get sprites() { return this._sprites; } get packId() { return this.fileData?.fileId || -1; } } exports.SpritePack = SpritePack; /** * Controls SpritePack file storage. */ class SpriteStore { fileStore; constructor(fileStore) { this.fileStore = fileStore; } async writeToDisk() { (0, node_fs_1.rmdirSync)('./unpacked/sprite-packs', { recursive: true }); const spritePacks = this.decodeSpriteStore(); for (const spritePack of spritePacks) { try { await spritePack.writeToDisk(); } catch (e) { common_1.logger.error(`Error writing spritepack ${spritePack.packId} to disk.`); common_1.logger.error(e); } } } /** * Decodes the specified sprite pack. * @param nameOrId The name or ID of the sprite pack file. * @returns The decoded SpritePack object, or null if the file is not found. */ getSpritePack(nameOrId) { if (!nameOrId) { return null; } const spritePackIndex = this.fileStore.getIndex('sprites'); const fileData = spritePackIndex.getFile(nameOrId) || null; return fileData ? new SpritePack(fileData) : null; } /** * Decodes all sprite packs within the filestore. * @returns The list of decoded SpritePack objects from the sprite store. */ decodeSpriteStore() { const spritePackIndex = this.fileStore.getIndex('sprites'); const packCount = spritePackIndex.files.size; const spritePacks = new Array(packCount); for (let spritePackId = 0; spritePackId < packCount; spritePackId++) { const fileData = spritePackIndex.getFile(spritePackId); if (!fileData) { spritePacks[spritePackId] = null; common_1.logger.warn(`No file found for sprite pack ID ${spritePackId}.`); continue; } const spritePack = new SpritePack(fileData); spritePack.decode(); spritePacks[spritePackId] = spritePack; } return spritePacks; } } exports.SpriteStore = SpriteStore;