UNPKG

sc4

Version:

A command line utility for automating SimCity 4 modding tasks & modifying savegames

173 lines (172 loc) 5.82 kB
// # fsh.ts import FileType from './file-types.js'; import Stream from './stream.js'; import { kFileType } from './symbols.js'; import { decompress8bit, decompress32bit, decompress24bit, decompressDXT1, decompressDXT3, decompress1555, decompress565, decompress444, } from './bitmap-decompression.js'; // # FSH // A parser for the FSH format. Note that by default we don't decompress the // images contained in it. We only parse all "entries" in the FSH, and if you // want to get the raw decompressed data, you'll have to call the `decompress()` // method on this. export class FSH { static [kFileType] = FileType.FSH; size = 0; directoryId = ''; entries = []; parse(streamOrBuffer) { let rs = new Stream(streamOrBuffer); const id = rs.string(4); if (id !== 'SHPI') { throw new Error('Invalid FSH file'); } this.size = rs.uint32(); const numImages = rs.uint32(); this.directoryId = rs.string(4); // Read in the directory. const index = []; for (let i = 0; i < numImages; i++) { const name = rs.string(4); const offset = rs.uint32(); index.push({ name, offset }); } // From the directory, we can read in all entries. this.entries = []; for (let { name, offset } of index) { let entry = new FSHEntry({ name }); let stream = new Stream(rs.internalUint8Array.subarray(offset)); entry.parse(stream); this.entries.push(entry); } return this; } // ## get image() // For convenience, the `.image` accessor can be used to read the image // ata. Note that an FSH can contain multiple images though, in which case // you can read them with `entries`. get image() { return this.entries[0].image; } *[Symbol.iterator]() { yield* this.entries; } } export default FSH; export class FSHEntry { name = '0000'; id = 0x00; size = 0; width = 0; height = 0; center = [0, 0]; offset = [0, 0]; mipmaps = []; constructor(opts) { this.name = opts.name ?? '0000'; } // ## get image() // Image is just an alias for the first image data in the mipmaps array, // which is normally always present. get image() { return this.mipmaps[0]; } // ## get code() get code() { return this.id & 0x7f; } // ## get format() // Alias for .code get format() { return this.code; } // ## *[Symbol.iterator]() *[Symbol.iterator]() { yield* this.mipmaps; } // ## parse(rs) parse(rs) { this.id = rs.byte(); this.size = rs.byte() + (rs.byte() << 8) + (rs.byte() << 16); let width = this.width = rs.uint16(); let height = this.height = rs.uint16(); this.center = [rs.uint16(), rs.uint16()]; let ox = rs.uint16(); let oy = rs.uint16(); this.offset = [(ox & 0xffffff) >>> 0, (oy & 0xffffff) >>> 0]; // The size of the image data that follows depends on the id of the // entry. let { code } = this; let sizeFactor = getSizeFactor(code); let image = new FSHImageData({ code, width, height, data: rs.readUint8Array(width * height * sizeFactor), }); // Read in all mipmaps too. let numMipMaps = oy >>> 24; this.mipmaps = [image]; for (let i = 0; i < numMipMaps; i++) { let factor = 2 ** (i + 1); let width = this.width / factor; let height = this.height / factor; let mipmap = new FSHImageData({ code, width, height, data: rs.readUint8Array(width * height * sizeFactor), }); this.mipmaps.push(mipmap); } return this; } } function getSizeFactor(code) { switch (code) { case 0x7b: return 1; case 0x7d: return 4; case 0x7f: return 3; case 0x60: return 0.5; case 0x61: return 1; } throw new Error(`Unknown FSH format 0x${code.toString(16)}`); } // # FSHImageData // The FSHImageData class contains the raw, encoded and potentially compressed // image data. This is the entry point for actually getting the raw bitmap. // Note: if we're rendering a texture with Three.js, we don't need to decompress // it to a bitmap first because Three.js has support for decompressing textures // on the GPU. This can be done by creating a CompressedTexture in Three.js! class FSHImageData { code = 0x00; width = 0; height = 0; data; bitmap; constructor(opts) { this.code = opts.code; this.width = opts.width; this.height = opts.height; this.data = opts.data; this.bitmap = opts.bitmap; } get format() { return this.code; } decompress() { let { width, height } = this; if (this.bitmap) return this.bitmap; let data = this.data; switch (this.code) { case 0x07b: return this.bitmap = decompress8bit(data); case 0x07d: return this.bitmap = decompress32bit(data); case 0x07f: return this.bitmap = decompress24bit(data); case 0x07e: return this.bitmap = decompress1555(data); case 0x78: return this.bitmap = decompress565(data); case 0x6d: return this.bitmap = decompress444(data); case 0x60: return this.bitmap = decompressDXT1(data, width, height); case 0x61: return this.bitmap = decompressDXT3(data, width, height); } throw new Error(`Unknown bitmap format 0x${this.code.toString(16)}`); } }