ts-ritofile
Version:
TypeScript library for reading and writing League of Legends game file formats
238 lines (212 loc) • 7.82 kB
text/typescript
/**
* 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;
}
}