import { readFile } from 'fs/promises'; enum KATypes { NONE, BOOLEAN, INT32, FLOAT, STRING, WIDE_STRING, BYTE_ARRAY, UINT32, KEYED_ARCHIVE, INT64, UINT64, VECTOR2, VECTOR3, VECTOR4, MATRIX2, MATRIX3, MATRIX4, COLOR, FASTNAME, AABBOX3, FILEPATH, FLOAT64, INT8, UINT8, INT16, UINT16, ARRAY, } const VERTEX_TYPES = [ 'VERTEX', 'NORMAL', 'COLOR', 'TEXCOORD0', 'TEXCOORD1', 'TEXCOORD2', 'TEXCOORD3', 'TANGENT', 'BINORMAL', 'HARD_JOINTINDEX', 'PIVOT4', 'FLEXIBILITY', 'ANGLE_SIN_COS', 'JOINTINDEX', 'JOINTWEIGHT', 'CUBETEXCOORD0', 'CUBETEXCOORD1', 'CUBETEXCOORD2', 'CUBETEXCOORD3', ] as const; type VertexType = (typeof VERTEX_TYPES)[number]; type KAResolvedValue = { buffer: Buffer; value: Type }; interface PolygonGroupRaw { '##name': KAResolvedValue<'PolygonGroup'>; '#id': KAResolvedValue; cubeTextureCoordCount: KAResolvedValue; indexCount: KAResolvedValue; indexFormat: KAResolvedValue<0 | 1>; indices: KAResolvedValue; packing: KAResolvedValue<0 | 1>; primitiveCount: KAResolvedValue; rhi_primitiveType: KAResolvedValue<1 | 2 | 10>; textureCoordCount: KAResolvedValue; vertexCount: KAResolvedValue; vertexFormat: KAResolvedValue; vertices: KAResolvedValue; } interface PolygonGroup extends Omit { indices: number[]; } class SCPGStream { public index = 0; constructor(public buffer: Buffer) {} skip(size: number) { this.index += size; } consume(size: number) { const subarray = this.buffer.subarray(this.index, this.index + size); this.index += size; return subarray; } consumeRemaining() { return this.consume(Number.POSITIVE_INFINITY); } consumeByteArray(size: number) { const buffer = this.consume(size); return { buffer, value: buffer }; } consumeAscii(size: number) { const buffer = this.consume(size); return { buffer, value: buffer.toString('ascii') }; } consumeInt8() { const buffer = this.consume(1); return { buffer, value: buffer.readInt8() }; } consumeInt16() { const buffer = this.consume(2); return { buffer, value: buffer.readInt16LE() }; } consumeInt32() { const buffer = this.consume(4); return { buffer, value: buffer.readInt32LE() }; } consumeUInt8() { const buffer = this.consume(1); return { buffer, value: buffer.readUInt8() }; } consumeUInt16() { const buffer = this.consume(2); return { buffer, value: buffer.readUInt16LE() }; } consumeUInt32() { const buffer = this.consume(4); return { buffer, value: buffer.readUInt32LE() }; } consumeSCGHeader() { return { name: this.consumeAscii(4).value, version: this.consumeUInt32().value, nodeCount: this.consumeUInt32().value, nodeCount2: this.consumeUInt32().value, }; } consumeSCG() { const header = this.consumeSCGHeader(); const polygonGroupsRaw: PolygonGroupRaw[] = []; for (let index = 0; index < header.nodeCount; index++) { const polygonGroupRaw = this.consumeKA(); polygonGroupsRaw.push(polygonGroupRaw); } const polygonGroups = polygonGroupsRaw.map((polygonGroupRaw) => { const indicesStream = new SCPGStream(polygonGroupRaw.indices.value); const indices: number[] = []; for (let index = 0; index < polygonGroupRaw.indexCount.value; index++) { indices.push( polygonGroupRaw.indexFormat.value ? indicesStream.consumeUInt32().value : indicesStream.consumeUInt16().value, ); } const verticesStream = new SCPGStream(polygonGroupRaw.vertices.buffer); const vertexFormat = BigInt( '0x' + polygonGroupRaw.vertexFormat.buffer.toString('hex'), ) .toString(2) .padStart(polygonGroupRaw.vertexFormat.buffer.length * 8, '0') .split('') .map(parseInt) .map((bit, index) => { if (bit) return VERTEX_TYPES[index]; }) .filter(Boolean); const vertices: { type: VertexType; value: number[] }[] = []; for (let index = 0; index < polygonGroupRaw.vertexCount.value; index++) { vertexFormat.forEach((type) => { if (['COLOR', 'FLEXIBILITY', 'HARD_JOINTINDEX'].includes(type)) { vertices.push({ type, value: [verticesStream.consumeUInt32().value], }); } else if ( [ 'TEXCOORD0', 'TEXCOORD1', 'TEXCOORD2', 'TEXCOORD3', 'ANGLE_SIN_COS', ].includes(type) ) { vertices.push({ type, value: [ verticesStream.consumeUInt32().value, verticesStream.consumeUInt32().value, ], }); } else if ( [ 'VERTEX', 'NORMAL', 'TANGENT', 'BINORMAL', 'CUBETEXCOORD0', 'CUBETEXCOORD1', 'CUBETEXCOORD2', 'CUBETEXCOORD3', ].includes(type) ) { vertices.push({ type, value: [ verticesStream.consumeUInt32().value, verticesStream.consumeUInt32().value, verticesStream.consumeUInt32().value, ], }); } else if (['PIVOT4', 'JOINTINDEX', 'JOINTWEIGHT'].includes(type)) { vertices.push({ type, value: [ verticesStream.consumeUInt32().value, verticesStream.consumeUInt32().value, verticesStream.consumeUInt32().value, verticesStream.consumeUInt32().value, ], }); } else { throw new TypeError(`Unknown vertex type: ${type}`); } }); } return { vertices, indices }; }); // writeFileSync( // 'test.txt', // polygonGroups // .map((i) => // i.vertices // .map( // (e) => // `${e.type.padEnd(16, ' ')} ${e.value // .map((j) => j.toString(16).padStart(8, '0')) // .join(' ')}`, // ) // .join('\n'), // ) // .join('\n\n'), // ); return polygonGroups; } consumeKAHeader() { return { name: this.consumeAscii(2).value, version: this.consumeUInt16().value, count: this.consumeUInt32().value, }; } consumeKAValue() { const type = this.consumeUInt8().value; switch (type) { case KATypes.INT32: return this.consumeInt32(); case KATypes.STRING: { const length = this.consumeUInt32(); return this.consumeAscii(length.value); } case KATypes.BYTE_ARRAY: { const length = this.consumeUInt32(); return this.consumeByteArray(length.value); } default: throw new TypeError(`Unknown KA type: ${type}`); } } consumeKAPair() { const name = this.consumeKAValue().value as string; const value = this.consumeKAValue(); return { name, value }; } consumeKA() { const header = this.consumeKAHeader(); const pairs: Record = {}; for (let index = 0; index < header.count; index++) { const { name, value } = this.consumeKAPair(); pairs[name] = value; } return pairs as Type; } } const stream = new SCPGStream(await readFile('test.scg')); stream.consumeSCG(); // console.log(stream.consumeSCG());