UNPKG

ts-ritofile

Version:

TypeScript library for reading and writing League of Legends game file formats

238 lines (212 loc) 7.82 kB
/** * TEX (Texture) format implementation * Handles League of Legends texture files with various compression formats */ import * as fs from 'fs'; import { BinStream } from '../stream/bin-stream'; import { JsonSerializable } from '../core/json-encoder'; export enum TEXFormat { ETC1 = 1, ETC2 = 2, ETC2_EAC = 3, DXT1 = 10, DXT1_ = 11, DXT5 = 12, RGBA8 = 20 } export class TEX implements JsonSerializable { public signature?: number; public width?: number; public height?: number; public format?: TEXFormat; public unknown1?: number; public unknown2?: number; public mipmaps?: boolean; public data?: Buffer[]; constructor( signature?: number, width?: number, height?: number, format?: TEXFormat, unknown1?: number, unknown2?: number, mipmaps: boolean = false, data?: Buffer[] ) { this.signature = signature; this.width = width; this.height = height; this.format = format; this.unknown1 = unknown1; this.unknown2 = unknown2; this.mipmaps = mipmaps; this.data = data; } /** * Create stream from path or raw data - matches Python stream method exactly */ private stream(path: string, mode: string, raw?: Buffer | boolean): BinStream { if (raw !== undefined) { if (raw === true) { // Python: return BinStream(BytesIO()) return BinStream.create(); } else if (Buffer.isBuffer(raw)) { // Python: return BinStream(BytesIO(raw)) return BinStream.fromBuffer(raw, mode.includes('w')); } } // Python: return BinStream(open(path, mode)) return BinStream.fromFile(path, mode); } /** * Read TEX file from path or buffer - matches Python read method exactly */ read(path?: string, raw?: Buffer): void { const bs = this.stream(path || '', 'rb', raw); try { // Read headers - exactly matching Python sequence this.signature = bs.read_u32(1)[0]; if (this.signature !== 0x00584554) { throw new Error( `pyRitoFile: Failed: Read TEX ${path || ''}: Wrong file signature: ${this.signature ? '0x' + this.signature.toString(16) : 'undefined'}` ); } // Python: self.width, self.height = bs.read_u16(2) const dimensions = bs.read_u16(2); this.width = dimensions[0]; this.height = dimensions[1]; // Python: self.unknown1, self.format, self.unknown2 = bs.read_u8(3) const bytes = bs.read_u8(3); this.unknown1 = bytes[0]; this.format = bytes[1] as TEXFormat; this.unknown2 = bytes[2]; // Python: self.mipmaps, = bs.read_b() this.mipmaps = bs.read_b(1)[0] !== 0; // Read data - exactly matching Python logic if (this.mipmaps && [TEXFormat.DXT1, TEXFormat.DXT1_, TEXFormat.DXT5, TEXFormat.RGBA8].includes(this.format)) { // If mipmaps and supported format let blockSize: number; let bytesPerBlock: number; if (this.format === TEXFormat.DXT1 || this.format === TEXFormat.DXT1_) { blockSize = 4; bytesPerBlock = 8; } else if (this.format === TEXFormat.DXT5) { blockSize = 4; bytesPerBlock = 16; } else { // RGBA8 blockSize = 1; bytesPerBlock = 4; } // Python: mipmap_count = 32 - len(f'{max(self.width, self.height):032b}'.split('1', 1)[0]) const maxDimension = Math.max(this.width, this.height); const binaryStr = maxDimension.toString(2).padStart(32, '0'); const mipmapCount = 32 - binaryStr.split('1')[0].length; // Check if we have enough data for mipmaps, otherwise treat as single data block const remaining = bs.end() - bs.tell(); let totalExpectedSize = 0; // Calculate total expected size for all mipmap levels for (let i = mipmapCount - 1; i >= 0; i--) { const currentWidth = Math.max(Math.floor(this.width / (1 << i)), 1); const currentHeight = Math.max(Math.floor(this.height / (1 << i)), 1); const blockWidth = Math.floor((currentWidth + blockSize - 1) / blockSize); const blockHeight = Math.floor((currentHeight + blockSize - 1) / blockSize); const currentSize = bytesPerBlock * blockWidth * blockHeight; totalExpectedSize += currentSize; } if (remaining >= totalExpectedSize) { // We have enough data for mipmaps this.data = []; for (let i = mipmapCount - 1; i >= 0; i--) { const currentWidth = Math.max(Math.floor(this.width / (1 << i)), 1); const currentHeight = Math.max(Math.floor(this.height / (1 << i)), 1); const blockWidth = Math.floor((currentWidth + blockSize - 1) / blockSize); const blockHeight = Math.floor((currentHeight + blockSize - 1) / blockSize); const currentSize = bytesPerBlock * blockWidth * blockHeight; this.data.push(bs.read(currentSize)); } } else { // Not enough data for mipmaps, treat as single block this.data = [bs.read(remaining)]; } } else { // Python: self.data = [bs.read(-1)] const remaining = bs.end() - bs.tell(); this.data = [bs.read(remaining)]; } } finally { bs.close(); } } /** * Write TEX file to path or return buffer - matches Python write method exactly */ write(path?: string, raw?: boolean): Buffer | void { const bs = this.stream(path || '', 'wb', raw); try { // Write headers - exactly matching Python sequence bs.write_u32(0x00584554); // TEX signature bs.write_u16(this.width || 0, this.height || 0); bs.write_u8(this.unknown1 || 1, (this.format as number) || 0, this.unknown2 || 0); bs.write_b(this.mipmaps ? 1 : 0); // Write data - exactly matching Python logic if (this.data) { if (this.mipmaps && [TEXFormat.DXT1, TEXFormat.DXT1_, TEXFormat.DXT5, TEXFormat.RGBA8].includes(this.format!)) { // Python: for block_data in self.data: bs.write(block_data) for (const blockData of this.data) { bs.write(blockData); } } else { // Python: bs.write(self.data[0]) bs.write(this.data[0]); } } if (path && !raw) { fs.writeFileSync(path, bs.getBuffer()); } else { return bs.getBuffer(); } } finally { bs.close(); } } /** * JSON serialization - matches Python __json__ method exactly */ __json__(): any { const result: any = {}; const slots = ['signature', 'width', 'height', 'format', 'unknown1', 'unknown2', 'mipmaps', 'data']; for (const key of slots) { const value = (this as any)[key]; if (key === 'format' && value !== undefined) { result[key] = TEXFormat[value]; } else if (key === 'data' && value !== undefined) { result[key] = value.map((buffer: Buffer) => Array.from(buffer)); } else { result[key] = value; } } return result; } /** * JSON serialization for compatibility */ toJSON(): any { return this.__json__(); } /** * Create TEX instance from file */ static fromFile(path: string): TEX { const tex = new TEX(); tex.read(path); return tex; } /** * Create TEX instance from buffer */ static fromBuffer(buffer: Buffer): TEX { const tex = new TEX(); tex.read('', buffer); return tex; } }