diff --git a/.vscode/settings.json b/.vscode/settings.json index ceab1196e..458f0f7c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,6 +33,7 @@ "researchable", "Resvg", "roboto", + "SCPG", "searchclans", "searchplayers", "searchtanks", diff --git a/package.json b/package.json index 42d36bddd..52823815b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build:docs": "vitepress build docs", "build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard", "build:assets": "vite-node --script scripts/buildAssets", + "build:test": "vite-node --script scripts/buildTest", "build:ratingsplot": "vite-node --script scripts/buildRatingsPlot", "dev:bot": "yarn build:bot && yarn start:bot", "dev:docs": "vitepress dev docs", diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index f74b26176..0d960f3f8 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -1,5 +1,5 @@ import { config } from 'dotenv'; -import { readFile, readdir } from 'fs/promises'; +import { readdir } from 'fs/promises'; import { argv } from 'process'; import { TankType } from '../src/components/Tanks'; import { NATION_IDS } from '../src/constants/nations'; @@ -16,102 +16,6 @@ import { config(); -class SCGStream { - public index = 0; - - constructor(public buffer: Buffer) {} - - consume(size: number) { - const subarray = this.buffer.subarray(this.index, this.index + size); - this.index += size; - - return subarray; - } - - consumeAscii(size: number) { - return this.consume(size).toString('ascii'); - } - - consumeByteArray(size: number) { - return this.consume(size); - } - - consumeInt8() { - return this.consume(1).readInt8(); - } - consumeInt16() { - return this.consume(2).readInt16LE(); - } - consumeInt32() { - return this.consume(4).readInt32LE(); - } - - consumeSCGHeader() { - return { - name: this.consumeAscii(4), - version: this.consumeInt32(), - nodeCount: this.consumeInt32(), - nodeCount2: this.consumeInt32(), - }; - } - - consumeKAHeader() { - return { - name: this.consumeAscii(2), - version: this.consumeInt16(), - count: this.consumeInt32(), - }; - } - - consumeKA() { - const type = this.consumeInt8(); - - if (type === 2) { - // int32 - return { type, value: this.consumeInt32() }; - } else if (type === 4) { - // string - const length = this.consumeInt32(); - const value = this.consumeAscii(length); - - return { type, length, value }; - } else if (type === 6) { - // byte array - const length = this.consumeInt32(); - const value = this.consumeByteArray(length); - - return { type, length, value }; - } - { - throw new TypeError(`Unknown KA type: ${type}`); - } - } - - consumeRemaining() { - return this.consume(Number.POSITIVE_INFINITY); - } - - skip(size: number) { - this.index += size; - } -} - -async function readSCG(file: string) { - const stream = new SCGStream(await readFile(file)); - - const header = stream.consumeSCGHeader(); - - for (let i = 0; i < header.nodeCount; i++) { - const ka = stream.consumeKAHeader(); - - for (let KAIndex = 0; KAIndex < ka.count; KAIndex++) { - console.log(stream.consumeKA()); - } - } -} - -await readSCG('test.scg'); - interface VehicleDefinitionList { [key: string]: { id: number; diff --git a/scripts/buildTest.ts b/scripts/buildTest.ts new file mode 100644 index 000000000..eabfbfd6f --- /dev/null +++ b/scripts/buildTest.ts @@ -0,0 +1,304 @@ +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());