feat: migration 2

This commit is contained in:
TrèsAbhi committed 2024-09-06 23:17:53 -05:00
1 parent e1c8194beb
commit 6285f33989
305 files changed
+414 -414

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
import { times } from 'lodash';
import { ReadStream, WriteStream } from './buffer';
export enum BkrlFormat {
Base,
Superset1,
}
export interface BkrlBaseEntry {
id: number;
score: number;
}
export interface BkrlSuperset1Entry {
id: number;
score: number;
battles: number;
wins: number;
survived: number;
damage: number;
kills: number;
}
export type BkrlDiscriminatedEntries =
| {
format: BkrlFormat.Base;
entries: BkrlBaseEntry[];
}
| {
format: BkrlFormat.Superset1;
entries: BkrlSuperset1Entry[];
};
export class BkrlReadStream extends ReadStream {
bkrl() {
this.magic();
const header = this.header();
return this.body(header);
}
body(header: ReturnType<typeof this.header>): BkrlDiscriminatedEntries {
switch (header.format) {
case BkrlFormat.Base: {
return {
format: BkrlFormat.Base,
entries: times(
header.count,
() =>
({
id: this.uint32(),
score: this.uint16(),
}) satisfies BkrlBaseEntry,
),
};
}
case BkrlFormat.Superset1: {
return {
format: BkrlFormat.Superset1,
entries: times(
header.count,
() =>
({
id: this.uint32(),
score: this.uint16(),
battles: this.uint32(),
wins: this.uint32(),
survived: this.uint32(),
damage: this.uint32(),
kills: this.uint32(),
}) satisfies BkrlSuperset1Entry,
),
};
}
}
}
header() {
return {
version: this.uint16(),
format: this.uint8() as BkrlFormat,
count: this.uint32(),
};
}
magic() {
const magic = this.utf8(4);
if (magic !== 'BKRL') {
throw new Error(`Invalid BKRL magic number: "${magic}"`);
}
}
}
export class BkrlWriteStream extends WriteStream {
bkrl(discriminatedEntries: BkrlDiscriminatedEntries) {
this.magic();
this.header(discriminatedEntries);
this.body(discriminatedEntries);
}
body(discriminatedEntries: BkrlDiscriminatedEntries) {
switch (discriminatedEntries.format) {
case BkrlFormat.Base: {
discriminatedEntries.entries.forEach((entry) => {
this.uint32(entry.id);
this.uint16(entry.score);
});
break;
}
case BkrlFormat.Superset1: {
discriminatedEntries.entries.forEach((entry) => {
this.uint32(entry.id);
this.uint16(entry.score);
this.uint32(entry.battles);
this.uint32(entry.wins);
this.uint32(entry.survived);
this.uint32(entry.damage);
this.uint32(entry.kills);
});
break;
}
}
}
header(discriminatedEntries: BkrlDiscriminatedEntries) {
this.uint16(1);
this.uint8(discriminatedEntries.format);
this.uint32(discriminatedEntries.entries.length);
}
magic() {
this.utf8('BKRL');
}
}
+165
View File
@@ -0,0 +1,165 @@
export class ReadStream {
public index = 0;
private dataView: DataView;
private textDecoderAscii = new TextDecoder('ascii');
private textDecoderUtf8 = new TextDecoder('utf8');
constructor(public buffer: ArrayBuffer) {
this.dataView = new DataView(buffer);
}
seek(size: number) {
this.index += size;
}
read(size: number) {
return this.buffer.slice(this.index, this.index + size);
}
consume(size: number) {
const subarray = this.read(size);
this.seek(size);
return subarray;
}
consumeUint8Array(size: number) {
return new Uint8Array(this.consume(size));
}
increment(size: number) {
return (this.index += size) - size;
}
readRemaining() {
return this.read(Number.POSITIVE_INFINITY);
}
consumeRemaining() {
return this.consume(Number.POSITIVE_INFINITY);
}
byte(size: number) {
return this.consume(size);
}
ascii(size: number) {
return this.textDecoderAscii.decode(this.consume(size));
}
utf8(size: number) {
return this.textDecoderUtf8.decode(this.consume(size));
}
int8() {
return this.dataView.getInt8(this.increment(1));
}
int16() {
return this.dataView.getInt16(this.increment(2), true);
}
int32() {
return this.dataView.getInt32(this.increment(4), true);
}
int64() {
return this.dataView.getBigInt64(this.increment(8), true);
}
uint8() {
return this.dataView.getUint8(this.increment(1));
}
uint16() {
return this.dataView.getUint16(this.increment(2), true);
}
uint32() {
return this.dataView.getUint32(this.increment(4), true);
}
uint64() {
return this.dataView.getBigUint64(this.increment(8), true);
}
float32() {
return this.dataView.getFloat32(this.increment(4), true);
}
float64() {
return this.dataView.getFloat64(this.increment(8), true);
}
boolean() {
return this.dataView.getUint8(this.increment(1)) === 1;
}
}
export class WriteStream {
private array: number[] = [];
private textEncoder = new TextEncoder();
get uint8Array() {
return new Uint8Array(this.array);
}
byte(value: Uint8Array) {
this.array.push(...value);
return this;
}
ascii(value: string) {
for (let index = 0; index < value.length; index++) {
this.array.push(value.charCodeAt(index));
}
return this;
}
utf8(value: string) {
this.array.push(...this.textEncoder.encode(value));
return this;
}
private uintN(bytes: number, value: number) {
for (let index = 0; index < bytes; index++) {
this.array.push((value >>> (8 * index)) & 0xff);
}
return this;
}
uint8(value: number) {
this.uintN(1, value);
return this;
}
uint16(value: number) {
this.uintN(2, value);
return this;
}
uint32(value: number) {
this.uintN(4, value);
return this;
}
uint64(value: bigint) {
for (let index = 0n; index < 8n; index++) {
this.uint8(Number((value >> (8n * index)) & 0xffn));
}
return this;
}
private intN(bytes: number, value: number) {
for (let index = 0; index < bytes; index++) {
this.array.push((value >> (8 * index)) & 0xff);
}
return this;
}
int8(value: number) {
this.intN(1, value);
return this;
}
int16(value: number) {
this.intN(2, value);
return this;
}
int32(value: number) {
this.intN(4, value);
return this;
}
int64(value: bigint) {
for (let index = 0n; index < 8n; index++) {
this.int8(Number((value >> (8n * index)) & 0xffn));
}
return this;
}
float32(value: number) {
const array = new Float32Array([value]);
this.array.push(...new Uint8Array(array.buffer));
return this;
}
float64(value: number) {
const array = new Float64Array([value]);
this.array.push(...new Uint8Array(array.buffer));
return this;
}
}
+375
View File
@@ -0,0 +1,375 @@
import { times } from 'lodash';
import { ReadStream, WriteStream } from './buffer';
enum ValueType {
Null,
Boolean,
Uint8,
Uint16,
Uint32,
Uint64,
Int8,
Int16,
Int32,
Int64,
Float32,
Float64,
String,
FastString,
Array,
Object,
}
enum FastStringFormat {
Format8,
Format16,
Format32,
}
export type CdonValue =
| undefined
| null
| boolean
| number
| bigint
| string
| CdonValue[]
| { [key: string]: CdonValue };
export class CdonReadStream extends ReadStream {
cdon() {
this.magic();
const header = this.header();
const body = this.body(header.fastStringFormat, header.stringTable);
return body;
}
magic() {
const magic = this.utf8(4);
if (magic !== 'CDON') {
throw new Error(`Invalid CDON magic number: "${magic}"`);
}
}
header() {
const version = this.uint16();
const fastStringFormat = this.uint8() as FastStringFormat;
return {
version,
fastStringFormat,
stringTable: this.stringTable(fastStringFormat),
};
}
body(
fastStringFormat: FastStringFormat,
stringTable: Record<number, string>,
) {
return this.value(fastStringFormat, stringTable);
}
stringTable(fastStringFormat: FastStringFormat) {
let count: number;
switch (fastStringFormat) {
case FastStringFormat.Format8:
count = this.uint8();
break;
case FastStringFormat.Format16:
count = this.uint16();
break;
case FastStringFormat.Format32:
count = this.uint32();
break;
}
const strings: string[] = [];
for (let index = 0; index < count; index++) {
strings.push(this.string());
}
const table: Record<number, string> = {};
times(count, (index) => (table[index] = strings[index]));
return table;
}
string() {
const length = this.uint32();
const string = this.utf8(length);
return string;
}
value(
fastStringFormat: FastStringFormat,
stringTable: Record<number, string>,
): CdonValue {
const type = this.uint8() as ValueType;
switch (type) {
case ValueType.Null:
return null;
case ValueType.Boolean:
return this.boolean();
case ValueType.Uint8:
return this.uint8();
case ValueType.Uint16:
return this.uint16();
case ValueType.Uint32:
return this.uint32();
case ValueType.Uint64:
return this.uint64();
case ValueType.Int8:
return this.int8();
case ValueType.Int16:
return this.int16();
case ValueType.Int32:
return this.int32();
case ValueType.Int64:
return this.int64();
case ValueType.Float32:
return this.float32();
case ValueType.Float64:
return this.float64();
case ValueType.String:
return this.string();
case ValueType.FastString: {
switch (fastStringFormat) {
case FastStringFormat.Format8:
return stringTable[this.uint8()];
case FastStringFormat.Format16:
return stringTable[this.uint16()];
case FastStringFormat.Format32:
return stringTable[this.uint32()];
}
}
case ValueType.Array:
return times(this.uint32(), () =>
this.value(fastStringFormat, stringTable),
);
case ValueType.Object: {
const count = this.uint32();
const keys = times(
count,
() => this.value(fastStringFormat, stringTable) as string,
);
const values = times(count, () =>
this.value(fastStringFormat, stringTable),
);
const object: Record<string, CdonValue> = {};
keys.forEach((string, index) => (object[string] = values[index]));
return object;
}
default:
throw new Error(`Invalid value type: ${type}`);
}
}
}
export class CdonWriteStream extends WriteStream {
cdon(object: CdonValue) {
this.magic();
const header = this.header(object);
this.body(header.fastStringFormat, object, header.stringTable);
return this;
}
body(
fastStringFormat: FastStringFormat,
object: CdonValue,
stringTable: Map<string, number>,
) {
this.value(fastStringFormat, object, stringTable);
return this;
}
value(
fastStringFormat: FastStringFormat,
object: CdonValue,
stringTable: Map<string, number>,
) {
if (object === null || object === undefined) {
this.uint8(ValueType.Null);
} else if (typeof object === 'boolean') {
this.uint8(ValueType.Boolean);
this.uint8(object ? 1 : 0);
} else if (typeof object === 'number') {
if (object % 1 === 0) {
if (object < 0) {
if (object < -0x80000000) {
this.uint8(ValueType.Int64);
this.int64(BigInt(object));
} else if (object < -0x8000) {
this.uint8(ValueType.Int32);
this.int32(object);
} else if (object < -0x80) {
this.uint8(ValueType.Int16);
this.int16(object);
} else {
this.uint8(ValueType.Int8);
this.int8(object);
}
} else {
if (object > 0x7fffffff) {
this.uint8(ValueType.Uint64);
this.uint64(BigInt(object));
} else if (object > 0x7fff) {
this.uint8(ValueType.Uint32);
this.uint32(object);
} else if (object > 0x7f) {
this.uint8(ValueType.Uint16);
this.uint16(object);
} else {
this.uint8(ValueType.Uint8);
this.uint8(object);
}
}
} else {
if (object < -0x80000000 || object > 0x7fffffff) {
this.uint8(ValueType.Float64);
this.float64(object);
} else {
this.uint8(ValueType.Float32);
this.float32(object);
}
}
} else if (typeof object === 'bigint') {
if (object < 0) {
this.int64(object);
} else {
this.uint64(object);
}
} else if (typeof object === 'string') {
if (stringTable.has(object)) {
this.uint8(ValueType.FastString);
const id = stringTable.get(object)!;
switch (fastStringFormat) {
case FastStringFormat.Format8:
this.uint8(id);
break;
case FastStringFormat.Format16:
this.uint16(id);
break;
case FastStringFormat.Format32:
this.uint32(id);
}
} else {
this.uint8(ValueType.String);
this.string(object);
}
} else if (Array.isArray(object)) {
this.uint8(ValueType.Array);
this.uint32(object.length);
object.forEach((value) =>
this.value(fastStringFormat, value, stringTable),
);
} else {
const entries = Object.entries(object).filter(
([, value]) => value !== undefined,
);
this.uint8(ValueType.Object);
this.uint32(entries.length);
entries.forEach(([key]) =>
this.value(fastStringFormat, key, stringTable),
);
entries.forEach(([, value]) =>
this.value(fastStringFormat, value, stringTable),
);
}
}
header(object: CdonValue) {
this.uint16(1);
return this.stringTable(object);
}
stringTable(object: CdonValue) {
const stringTable = this.buildStringTable(
this.collectStringCounts(object, new Map()),
);
let fastStringFormat: FastStringFormat;
if (stringTable.size <= 0xff) {
fastStringFormat = FastStringFormat.Format8;
this.uint8(FastStringFormat.Format8);
this.uint8(stringTable.size);
} else if (stringTable.size <= 0xffff) {
fastStringFormat = FastStringFormat.Format16;
this.uint8(FastStringFormat.Format16);
this.uint16(stringTable.size);
} else {
fastStringFormat = FastStringFormat.Format32;
this.uint8(FastStringFormat.Format32);
this.uint32(stringTable.size);
}
stringTable.forEach((_index, string) => this.string(string));
return { fastStringFormat, stringTable };
}
buildStringTable(stringCounts: Map<string, number>) {
const stringTable = new Map<string, number>();
stringCounts.forEach((count, string) => {
if (count > 1) stringTable.set(string, stringTable.size);
});
return stringTable;
}
collectStringCounts(object: CdonValue, stringCounts: Map<string, number>) {
if (typeof object === 'string') {
if (stringCounts.has(object)) {
stringCounts.set(object, stringCounts.get(object)! + 1);
} else {
stringCounts.set(object, 1);
}
} else if (object instanceof Array) {
object.forEach((value) => this.collectStringCounts(value, stringCounts));
} else if (object instanceof Object) {
Object.entries(object).forEach((strings) =>
this.collectStringCounts(strings, stringCounts),
);
}
return stringCounts;
}
string(value: string) {
this.uint32(Buffer.byteLength(value));
this.utf8(value);
}
magic() {
this.utf8('CDON');
}
}
+487
View File
@@ -0,0 +1,487 @@
import { times } from 'lodash';
import { Vector3Tuple, Vector4Tuple } from 'three';
import { WindowsReadStream } from './windows';
enum DxgiFormat {
UNKNOWN = 0,
R32G32B32A32_TYPELESS = 1,
R32G32B32A32_FLOAT = 2,
R32G32B32A32_UINT = 3,
R32G32B32A32_SINT = 4,
R32G32B32_TYPELESS = 5,
R32G32B32_FLOAT = 6,
R32G32B32_UINT = 7,
R32G32B32_SINT = 8,
R16G16B16A16_TYPELESS = 9,
R16G16B16A16_FLOAT = 10,
R16G16B16A16_UNORM = 11,
R16G16B16A16_UINT = 12,
R16G16B16A16_SNORM = 13,
R16G16B16A16_SINT = 14,
R32G32_TYPELESS = 15,
R32G32_FLOAT = 16,
R32G32_UINT = 17,
R32G32_SINT = 18,
R32G8X24_TYPELESS = 19,
D32_FLOAT_S8X24_UINT = 20,
R32_FLOAT_X8X24_TYPELESS = 21,
X32_TYPELESS_G8X24_UINT = 22,
R10G10B10A2_TYPELESS = 23,
R10G10B10A2_UNORM = 24,
R10G10B10A2_UINT = 25,
R11G11B10_FLOAT = 26,
R8G8B8A8_TYPELESS = 27,
R8G8B8A8_UNORM = 28,
R8G8B8A8_UNORM_SRGB = 29,
R8G8B8A8_UINT = 30,
R8G8B8A8_SNORM = 31,
R8G8B8A8_SINT = 32,
R16G16_TYPELESS = 33,
R16G16_FLOAT = 34,
R16G16_UNORM = 35,
R16G16_UINT = 36,
R16G16_SNORM = 37,
R16G16_SINT = 38,
R32_TYPELESS = 39,
D32_FLOAT = 40,
R32_FLOAT = 41,
R32_UINT = 42,
R32_SINT = 43,
R24G8_TYPELESS = 44,
D24_UNORM_S8_UINT = 45,
R24_UNORM_X8_TYPELESS = 46,
X24_TYPELESS_G8_UINT = 47,
R8G8_TYPELESS = 48,
R8G8_UNORM = 49,
R8G8_UINT = 50,
R8G8_SNORM = 51,
R8G8_SINT = 52,
R16_TYPELESS = 53,
R16_FLOAT = 54,
D16_UNORM = 55,
R16_UNORM = 56,
R16_UINT = 57,
R16_SNORM = 58,
R16_SINT = 59,
R8_TYPELESS = 60,
R8_UNORM = 61,
R8_UINT = 62,
R8_SNORM = 63,
R8_SINT = 64,
A8_UNORM = 65,
R1_UNORM = 66,
R9G9B9E5_SHAREDEXP = 67,
R8G8_B8G8_UNORM = 68,
G8R8_G8B8_UNORM = 69,
BC1_TYPELESS = 70,
BC1_UNORM = 71,
BC1_UNORM_SRGB = 72,
BC2_TYPELESS = 73,
BC2_UNORM = 74,
BC2_UNORM_SRGB = 75,
BC3_TYPELESS = 76,
BC3_UNORM = 77,
BC3_UNORM_SRGB = 78,
BC4_TYPELESS = 79,
BC4_UNORM = 80,
BC4_SNORM = 81,
BC5_TYPELESS = 82,
BC5_UNORM = 83,
BC5_SNORM = 84,
B5G6R5_UNORM = 85,
B5G5R5A1_UNORM = 86,
B8G8R8A8_UNORM = 87,
B8G8R8X8_UNORM = 88,
R10G10B10_XR_BIAS_A2_UNORM = 89,
B8G8R8A8_TYPELESS = 90,
B8G8R8A8_UNORM_SRGB = 91,
B8G8R8X8_TYPELESS = 92,
B8G8R8X8_UNORM_SRGB = 93,
BC6H_TYPELESS = 94,
BC6H_UF16 = 95,
BC6H_SF16 = 96,
BC7_TYPELESS = 97,
BC7_UNORM = 98,
BC7_UNORM_SRGB = 99,
AYUV = 100,
Y410 = 101,
Y416 = 102,
NV12 = 103,
P010 = 104,
P016 = 105,
'420_OPAQUE' = 106,
YUY2 = 107,
Y210 = 108,
Y216 = 109,
NV11 = 110,
AI44 = 111,
IA44 = 112,
P8 = 113,
A8P8 = 114,
B4G4R4A4_UNORM = 115,
P208 = 130,
V208 = 131,
V408 = 132,
SAMPLER_FEEDBACK_MIN_MIP_OPAQUE,
SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE,
FORCE_UINT = 0xffffffff,
}
enum DdsDimension {
UNKNOWN = 0,
BUFFER = 1,
TEXTURE1D = 2,
TEXTURE2D = 3,
TEXTURE3D = 4,
}
enum DdsdFlags {
CAPS = 0x1,
HEIGHT = 0x2,
WIDTH = 0x4,
PITCH = 0x8,
PIXELFORMAT = 0x1000,
MIPMAPCOUNT = 0x20000,
LINEARSIZE = 0x80000,
DEPTH = 0x800000,
}
enum DdpfFlags {
ALPHA = 0x1,
FOURCC = 0x4,
RGB = 0x40,
YUV = 0x200,
LUMINANCE = 0x20000,
}
enum DdsCaps {
COMPLEX = 0x8,
MIPMAP = 0x400000,
TEXTURE = 0x1000,
}
enum DdsCaps2 {
CUBEMAP = 0x200,
CUBEMAP_POSITIVEX = 0x400,
CUBEMAP_NEGATIVEX = 0x800,
CUBEMAP_POSITIVEY = 0x1000,
CUBEMAP_NEGATIVEY = 0x2000,
CUBEMAP_POSITIVEZ = 0x4000,
CUBEMAP_NEGATIVEZ = 0x8000,
VOLUME = 0x200000,
}
export class DdsReadStream extends WindowsReadStream {
async dds() {
this.magicNumber();
const header = this.header();
const dxgiFormat = this.resolveDxgiFormat(header);
switch (dxgiFormat) {
case DxgiFormat.BC1_TYPELESS:
case DxgiFormat.BC1_UNORM:
case DxgiFormat.BC1_UNORM_SRGB: {
return this.iterateBlocks(header.width, header.height, () => {
const colorInterpolations = this.bc1ColorInterpolations();
const colorKeys = this.bc1ColorKeys();
return (index) => colorInterpolations[colorKeys[index]];
});
}
case DxgiFormat.BC2_TYPELESS:
case DxgiFormat.BC2_UNORM:
case DxgiFormat.BC2_UNORM_SRGB: {
return this.iterateBlocks(header.width, header.height, () => {
const alphaInterpolations = this.bc2AlphaInterpolations();
const colorInterpolations = this.bc2ColorInterpolations();
const colorKeys = this.bc1ColorKeys();
return (index) => {
return [
...colorInterpolations[colorKeys[index]],
alphaInterpolations[index],
];
};
});
}
case DxgiFormat.BC3_TYPELESS:
case DxgiFormat.BC3_UNORM:
case DxgiFormat.BC3_UNORM_SRGB: {
return this.iterateBlocks(header.width, header.height, () => {
const alphaInterpolations = this.bc3AlphaInterpolations();
const alphaKeys = this.bc3AlphaKeys();
const colorInterpolations = this.bc2ColorInterpolations();
const colorKeys = this.bc1ColorKeys();
return (index) => [
...colorInterpolations[colorKeys[index]],
alphaInterpolations[alphaKeys[index]],
];
});
}
default:
throw new TypeError(
`Unsupported DXGI format: ${DxgiFormat[dxgiFormat]} (${dxgiFormat})`,
);
}
}
magicNumber() {
const magic = this.dword();
if (magic !== 0x20534444) {
throw new Error(`Invalid DDS magic number: 0x${magic.toString(16)}`);
}
return magic;
}
pixelFormat() {
return {
size: this.dword(),
flags: this.dword() as DdpfFlags,
fourCC: this.ascii(4),
rgbBitCount: this.dword(),
rBitMask: this.dword(),
gBitMask: this.dword(),
bBitMask: this.dword(),
aBitMask: this.dword(),
};
}
header() {
return {
size: this.dword(),
flags: this.dword() as DdsdFlags,
height: this.dword(),
width: this.dword(),
pitchOrLinearSize: this.dword(),
depth: this.dword(),
mipMapCount: this.dword(),
reserved1: times(11, () => this.dword()),
pf: this.pixelFormat(),
caps: this.dword() as DdsCaps,
caps2: this.dword() as DdsCaps2,
caps3: this.dword(),
caps4: this.dword(),
reserved2: times(1, () => this.dword()),
};
}
headerDxt10() {
return {
dxgiFormat: this.dword() as DxgiFormat,
resourceDimension: this.dword() as DdsDimension,
miscFlag: this.dword(),
arraySize: this.dword(),
miscFlags2: this.dword(),
};
}
resolveDxgiFormat(header: ReturnType<typeof this.header>) {
switch (header.pf.fourCC) {
case 'DXT1':
return DxgiFormat.BC1_UNORM;
case 'DXT3':
return DxgiFormat.BC2_UNORM;
case 'DXT5':
return DxgiFormat.BC3_UNORM;
case 'DX10':
return this.headerDxt10().dxgiFormat;
default:
throw new TypeError(`Unsupported FourCC: ${header.pf.fourCC}`);
}
}
assetFactor4(width: number, height: number) {
if (width % 4 !== 0 || height % 4 !== 0) {
throw new RangeError('Width and height must be divisible by 4');
}
}
iterateBlocks(
width: number,
height: number,
blockIterator: () => (index: number) => Vector4Tuple,
) {
this.assetFactor4(width, height);
const data = Buffer.alloc(width * height * 4);
const blocksX = width / 4;
const blocksY = height / 4;
times(blocksX * blocksY, (index) => {
const blockPosX = (index % blocksX) * 4;
const blockPosY = Math.floor(index / blocksX) * 4;
const pixelIterator = blockIterator();
times(16, (pixelIndex) => {
const pixelPosX = pixelIndex % 4;
const pixelPosY = Math.floor(pixelIndex / 4);
const x = blockPosX + pixelPosX;
const y = blockPosY + pixelPosY;
const bufferIndex = 4 * (y * width + x);
pixelIterator(pixelIndex).forEach((channel, index) => {
data[bufferIndex + index] = Math.round(channel * 255);
});
});
});
return { data, width, height, channels: 4 as const };
}
readR5G6B5Int() {
return new DataView(this.read(2)).getUint16(0, true);
}
R5G6B5() {
const buffer = this.consumeUint8Array(2);
const r = (buffer[1] & 0b11111000) >>> 3;
const g = ((buffer[1] & 0b111) << 3) | ((buffer[0] & 0b11100000) >>> 5);
const b = buffer[0] & 0b11111;
return [r / 31, g / 63, b / 31] as Vector3Tuple;
}
R5G6B5A0() {
return [...this.R5G6B5(), 1] as Vector4Tuple;
}
A8() {
return this.uint8() / 255;
}
bc1ColorInterpolations() {
const int0 = this.readR5G6B5Int();
const color0 = this.R5G6B5A0();
const int1 = this.readR5G6B5Int();
const color1 = this.R5G6B5A0();
const alpha = int0 < int1;
const color2 = alpha
? (color0.map(
(channel0, index) => (channel0 + color1[index]) / 2,
) as Vector4Tuple)
: (color0.map(
(channel0, index) => (2 * channel0 + color1[index]) / 3,
) as Vector4Tuple);
const color3 = alpha
? ([0, 0, 0, 0] as Vector4Tuple)
: (color0.map(
(channel0, index) => (channel0 + 2 * color1[index]) / 3,
) as Vector4Tuple);
return {
[0b00]: color0,
[0b01]: color1,
[0b10]: color2,
[0b11]: color3,
} as Record<number, Vector4Tuple>;
}
bc1ColorKeys() {
const buffer = this.consumeUint8Array(4);
const d = (buffer[0] & 0b11000000) >>> 6;
const c = (buffer[0] & 0b110000) >>> 4;
const b = (buffer[0] & 0b1100) >>> 2;
const a = buffer[0] & 0b11;
const h = (buffer[1] & 0b11000000) >>> 6;
const g = (buffer[1] & 0b110000) >>> 4;
const f = (buffer[1] & 0b1100) >>> 2;
const e = buffer[1] & 0b11;
const l = (buffer[2] & 0b11000000) >>> 6;
const k = (buffer[2] & 0b110000) >>> 4;
const j = (buffer[2] & 0b1100) >>> 2;
const i = buffer[2] & 0b11;
const p = (buffer[3] & 0b11000000) >>> 6;
const o = (buffer[3] & 0b110000) >>> 4;
const n = (buffer[3] & 0b1100) >>> 2;
const m = buffer[3] & 0b11;
return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p];
}
bc2AlphaInterpolations() {
const buffer = this.consumeUint8Array(8);
const d = (buffer[1] & 0b11110000) >>> 4;
const c = buffer[1] & 0b1111;
const b = (buffer[0] & 0b11110000) >>> 4;
const a = buffer[0] & 0b1111;
const h = (buffer[3] & 0b11110000) >>> 4;
const g = buffer[3] & 0b1111;
const f = (buffer[2] & 0b11110000) >>> 4;
const e = buffer[2] & 0b1111;
const l = (buffer[5] & 0b11110000) >>> 4;
const k = buffer[5] & 0b1111;
const j = (buffer[4] & 0b11110000) >>> 4;
const i = buffer[4] & 0b1111;
const p = (buffer[7] & 0b11110000) >>> 4;
const o = buffer[7] & 0b1111;
const n = (buffer[6] & 0b11110000) >>> 4;
const m = buffer[6] & 0b1111;
return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p].map(
(alpha) => alpha / 15,
);
}
bc2ColorInterpolations() {
const color0 = this.R5G6B5();
const color1 = this.R5G6B5();
const color2 = color0.map(
(channel0, index) => (2 * channel0 + color1[index]) / 3,
) as Vector3Tuple;
const color3 = color0.map(
(channel0, index) => (channel0 + 2 * color1[index]) / 3,
) as Vector3Tuple;
return {
[0b00]: color0,
[0b01]: color1,
[0b10]: color2,
[0b11]: color3,
} as Record<number, Vector3Tuple>;
}
bc3AlphaInterpolations() {
const alpha0 = this.A8();
const alpha1 = this.A8();
const full = alpha0 > alpha1;
return {
[0b000]: alpha0,
[0b001]: alpha1,
...(full
? {
[0b010]: (6 * alpha0 + 1 * alpha1) / 7,
[0b011]: (5 * alpha0 + 2 * alpha1) / 7,
[0b100]: (4 * alpha0 + 3 * alpha1) / 7,
[0b101]: (3 * alpha0 + 4 * alpha1) / 7,
[0b110]: (2 * alpha0 + 5 * alpha1) / 7,
[0b111]: (1 * alpha0 + 6 * alpha1) / 7,
}
: {
[0b010]: (4 * alpha0 + 1 * alpha1) / 5,
[0b011]: (3 * alpha0 + 2 * alpha1) / 5,
[0b100]: (2 * alpha0 + 3 * alpha1) / 5,
[0b101]: (1 * alpha0 + 4 * alpha1) / 5,
[0b110]: 0,
[0b111]: 255,
}),
} as Record<number, number>;
}
bc3AlphaKeys() {
const buffer = this.consumeUint8Array(6);
const h = (buffer[2] & 0b11100000) >>> 5;
const g = (buffer[2] & 0b11100) >>> 2;
const f = ((buffer[2] & 0b11) << 1) | ((buffer[1] & 0b10000000) >>> 7);
const e = (buffer[1] & 0b1110000) >>> 4;
const d = (buffer[1] & 0b1110) >>> 1;
const c = ((buffer[1] & 0b1) << 2) | ((buffer[0] & 0b11000000) >>> 6);
const b = (buffer[0] & 0b111000) >>> 3;
const a = buffer[0] & 0b111;
const p = (buffer[5] & 0b11100000) >>> 5;
const o = (buffer[5] & 0b11100) >>> 2;
const n = ((buffer[5] & 0b11) << 1) | ((buffer[4] & 0b10000000) >>> 7);
const m = (buffer[4] & 0b1110000) >>> 4;
const l = (buffer[4] & 0b1110) >>> 1;
const k = ((buffer[4] & 0b1) << 2) | ((buffer[3] & 0b11000000) >>> 6);
const j = (buffer[3] & 0b111000) >>> 3;
const i = buffer[3] & 0b111;
return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p];
}
}
+51
View File
@@ -0,0 +1,51 @@
import { times } from 'lodash';
import { ReadStream, WriteStream } from './buffer';
export class DidsReadStream extends ReadStream {
dids() {
this.magic();
this.header();
return this.body();
}
magic() {
const magic = this.utf8(4);
if (magic !== 'DIDS') {
throw new Error(`Invalid DIDS magic number: "${magic}"`);
}
}
header() {
return {
version: this.uint16(),
};
}
body() {
const count = this.uint32();
return times(count, () => this.uint32());
}
}
export class DidsWriteStream extends WriteStream {
dids(ids: number[]) {
this.magic();
this.header();
this.body(ids);
return this;
}
magic() {
this.utf8('DIDS');
}
header() {
this.uint16(1);
}
body(ids: number[]) {
this.uint32(ids.length);
ids.forEach((id) => this.uint32(id));
}
}
+11
View File
@@ -0,0 +1,11 @@
export * from './bkrl';
export * from './buffer';
export * from './cdon';
export * from './dds';
export * from './dids';
export * from './pvr';
export * from './rts';
export * from './sc2';
export * from './scg';
export * from './scpg';
export * from './windows';
+239
View File
@@ -0,0 +1,239 @@
import { clamp, isEqual, times } from 'lodash';
import { ReadStream } from './buffer';
export enum PvrFlags {
NoFlag = 0,
PreMultiplied = 0x02,
}
export enum PvrPixelFormat {
PVRTC_2bpp_RGB = 0,
PVRTC_2bpp_RGBA = 1,
PVRTC_4bpp_RGB = 2,
PVRTC_4bpp_RGBA = 3,
PVRTC_II_2bpp = 4,
PVRTC_II_4bpp = 5,
ETC1 = 6,
DXT1_BC1 = 7,
DXT2 = 8,
DXT3_BC2 = 9,
DXT4 = 10,
DXT5_BC3 = 11,
BC4 = 12,
BC5 = 13,
BC6 = 14,
BC7 = 15,
UYVY = 16,
YUY2 = 17,
BW1bpp = 18,
R9G9B9E5_SharedExponent = 19,
RGBG8888 = 20,
GRGB8888 = 21,
ETC2_RGB = 22,
ETC2_RGBA = 23,
ETC2_RGB_A1 = 24,
EAC_R11 = 25,
EAC_RG11 = 26,
ASTC_4x4 = 27,
ASTC_5x4 = 28,
ASTC_5x5 = 29,
ASTC_6x5 = 30,
ASTC_6x6 = 31,
ASTC_8x5 = 32,
ASTC_8x6 = 33,
ASTC_8x8 = 34,
ASTC_10x5 = 35,
ASTC_10x6 = 36,
ASTC_10x8 = 37,
ASTC_10x10 = 38,
ASTC_12x10 = 39,
ASTC_12x12 = 40,
ASTC_3x3x3 = 41,
ASTC_4x3x3 = 42,
ASTC_4x4x3 = 43,
ASTC_4x4x4 = 44,
ASTC_5x4x4 = 45,
ASTC_5x5x4 = 46,
ASTC_5x5x5 = 47,
ASTC_6x5x5 = 48,
ASTC_6x6x5 = 49,
ASTC_6x6x6 = 50,
BASISU_ETC1S = 51,
BASISU_UASTC = 52,
RGBM = 53,
RGBD = 54,
}
enum PvrColorSpace {
LinearRGB = 0,
sRGB = 1,
}
enum PvrChannelType {
UnsignedByteNormalised = 0,
SignedByteNormalised = 1,
UnsignedByte = 2,
SignedByte = 3,
UnsignedShortNormalised = 4,
SignedShortNormalised = 5,
UnsignedShort = 6,
SignedShort = 7,
UnsignedIntegerNormalised = 8,
SignedIntegerNormalised = 9,
UnsignedInteger = 10,
SignedInteger = 11,
SignedFloat = 12,
UnsignedFloat = 13,
}
enum ResolvedBitRatePixelFormat {
R4G4B4A4,
R5G6B5A0,
}
export class PvrReadStream extends ReadStream {
pvr() {
const header = this.header();
this.metadata();
if (typeof header.pixelFormat === 'number') {
switch (header.pixelFormat) {
default:
throw new TypeError(
`Unhandled pixel format ${PvrPixelFormat[header.pixelFormat]} (${
header.pixelFormat
})`,
);
}
} else {
const resolvedPixelFormat = this.resolveBitRatePixelFormat(
header.pixelFormat,
);
const pixelCount = header.width * header.height;
switch (resolvedPixelFormat) {
case ResolvedBitRatePixelFormat.R4G4B4A4: {
const data = Buffer.alloc(pixelCount * 4);
times(pixelCount, (index) => {
const buffer = this.consumeUint8Array(2);
const bufferIndex = index * 4;
const r = ((buffer[1] & 0b11110000) >>> 4) / 15;
const g = (buffer[1] & 0b1111) / 15;
const b = ((buffer[0] & 0b11110000) >>> 4) / 15;
const a = (buffer[0] & 0b1111) / 15;
data[bufferIndex] = Math.round(clamp(r * 255, 0, 255));
data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255));
data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255));
data[bufferIndex + 3] = Math.round(clamp(a * 255, 0, 255));
});
return {
data,
width: header.width,
height: header.height,
channels: 4 as const,
};
}
case ResolvedBitRatePixelFormat.R5G6B5A0: {
const data = Buffer.alloc(pixelCount * 3);
times(pixelCount, (index) => {
const buffer = this.consumeUint8Array(2);
const bufferIndex = index * 4;
const r = ((buffer[1] & 0b11111000) >>> 3) / 31;
const g =
((buffer[1] & 0b111) << 3) |
(((buffer[0] & 0b11100000) >>> 5) / 31);
const b = buffer[0] & 0b11111;
data[bufferIndex] = Math.round(clamp(r * 255, 0, 255));
data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255));
data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255));
});
return {
data,
width: header.width,
height: header.height,
channels: 3 as const,
};
}
default:
throw new TypeError(
`Unhandled resolved pixel format ${resolvedPixelFormat} (${header.pixelFormat})`,
);
}
}
}
header() {
const header = {
version: this.ascii(4),
flags: this.uint32() as PvrFlags,
pixelFormat: this.pixelFormat(),
colorSpace: this.uint32() as PvrColorSpace,
channelType: this.uint32() as PvrChannelType,
height: this.uint32(),
width: this.uint32(),
depth: this.uint32(),
surfaces: this.uint32(),
faces: this.uint32(),
mipMaps: this.uint32(),
metadataSize: this.uint32(),
};
if (header.version !== 'PVR\x03') throw new TypeError('Endian mismatch');
return header;
}
pixelFormat() {
this.seek(4);
const usesPresetFormat = this.uint32() === 0;
this.seek(-8);
return usesPresetFormat
? (Number(this.uint64()) as PvrPixelFormat)
: {
order: this.ascii(4),
bitRates: times(4, () => this.uint8()),
};
}
resolveBitRatePixelFormat(
pixelFormat: Exclude<ReturnType<typeof this.pixelFormat>, number>,
) {
if (pixelFormat.order === 'rgba') {
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
return ResolvedBitRatePixelFormat.R4G4B4A4;
} else
throw new TypeError(
`Unhandled rgba bit rate ${pixelFormat.bitRates.join(', ')}`,
);
} else if (pixelFormat.order === 'rgb\0') {
if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) {
return ResolvedBitRatePixelFormat.R5G6B5A0;
} else
throw new TypeError(
`Unhandled rgb bit rate ${pixelFormat.bitRates.join(', ')}`,
);
} else {
throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`);
}
}
metadata() {
const fourCC = this.ascii(4);
const key = this.uint32();
const dataSize = this.uint32();
const data = this.consume(dataSize);
return {
fourCC,
key,
dataSize,
data,
};
}
}
+183
View File
@@ -0,0 +1,183 @@
import { times } from 'lodash';
import { IndividualTankStats } from '../../types/tanksStats';
import { BlitzStats } from '../statistics/compositeStats/constants';
import { ReadStream, WriteStream } from './buffer';
export interface BlitzkitTankStats extends BlitzStats {
id: number;
battle_life_time: number;
last_battle_time: number;
mark_of_mastery: number;
}
export class RtsmReadStream extends ReadStream {
rtsm() {
this.magic();
this.header();
return this.body();
}
magic() {
const magic = this.utf8(4);
if (magic !== 'RTSM') {
throw new Error(`Invalid RTSM magic number: "${magic}"`);
}
}
header() {
return {
version: this.uint16(),
};
}
body() {
return {
time: this.uint32(),
bases: this.uint8(),
};
}
}
export class RtsmWriteStream extends WriteStream {
rtsm(bases: number) {
this.magic();
this.header();
this.body(bases);
return this;
}
magic() {
this.utf8('RTSM');
}
header() {
this.uint16(1);
}
body(base: number) {
this.uint32(Math.round(Date.now() / 1000));
this.uint8(base);
}
}
export class RtscReadStream extends ReadStream {
rtsc() {
this.magic();
const header = this.header();
return this.body(header.playerCount);
}
magic() {
const magic = this.utf8(4);
if (magic !== 'RTSC') {
throw new Error(`Invalid RTSC magic number: "${magic}"`);
}
}
header() {
return {
version: this.uint16(),
playerCount: this.uint32(),
};
}
body(playerCount: number) {
return times(playerCount, () => this.player());
}
player() {
const id = this.uint32();
const tankCount = this.uint16();
const tanks = times(tankCount, () => this.tank());
return {
id,
tankCount,
tanks,
};
}
tank() {
return {
id: this.uint32(),
battle_life_time: this.uint32(),
last_battle_time: this.uint32(),
mark_of_mastery: this.uint8(),
battles: this.uint32(),
capture_points: this.uint32(),
damage_dealt: this.uint32(),
damage_received: this.uint32(),
dropped_capture_points: this.uint32(),
frags: this.uint32(),
frags8p: this.uint32(),
hits: this.uint32(),
losses: this.uint32(),
max_frags: this.uint8(),
max_xp: this.uint16(),
shots: this.uint32(),
spotted: this.uint32(),
survived_battles: this.uint32(),
win_and_survived: this.uint32(),
wins: this.uint32(),
xp: this.uint32(),
} satisfies BlitzkitTankStats;
}
}
export interface RtscWritePlayer {
id: number;
tanks: IndividualTankStats[];
}
export class RtscWriteStream extends WriteStream {
rtsc(players: RtscWritePlayer[]) {
this.magic();
this.header(players.length);
this.body(players);
return this;
}
magic() {
this.utf8('RTSC');
}
header(playerCount: number) {
this.uint16(1);
this.uint32(playerCount);
}
body(players: RtscWritePlayer[]) {
players.forEach((player) => {
this.uint32(player.id);
this.uint16(player.tanks.length);
player.tanks.forEach((tank) => {
this.uint32(tank.tank_id);
this.uint32(tank.battle_life_time);
this.uint32(tank.last_battle_time);
this.uint8(tank.mark_of_mastery);
this.uint32(tank.all.battles);
this.uint32(tank.all.capture_points);
this.uint32(tank.all.damage_dealt);
this.uint32(tank.all.damage_received);
this.uint32(tank.all.dropped_capture_points);
this.uint32(tank.all.frags);
this.uint32(tank.all.frags8p);
this.uint32(tank.all.hits);
this.uint32(tank.all.losses);
this.uint8(tank.all.max_frags);
this.uint16(tank.all.max_xp);
this.uint32(tank.all.shots);
this.uint32(tank.all.spotted);
this.uint32(tank.all.survived_battles);
this.uint32(tank.all.win_and_survived);
this.uint32(tank.all.wins);
this.uint32(tank.all.xp);
});
});
}
}
+321
View File
@@ -0,0 +1,321 @@
import { Vector3Tuple, Vector4Tuple } from 'three';
import { ScpgReadStream } from './scpg';
export interface SC2 {
'#dataNodes': DataNode[];
'#hierarchy': Hierarchy[];
'#sceneComponents': SceneComponents;
}
type DataNode = {
'##name': 'NMaterial';
'#id': ArrayBuffer;
materialName: string;
parentMaterialKey?: bigint;
qualityGroup?: string;
fxName?: string;
textures?: Textures;
} & (
| ({
configCount: number;
} & Record<`configArchive_${number}`, ConfigArchive>)
| {
configCount: undefined;
}
);
interface ConfigArchive {
configName: string;
fxName: string;
properties: Properties;
textureSampleStates: TextureSampleStates;
textures: Textures;
customCullMode?: number;
enabledPresets?: EnabledPresets;
flags?: Flags;
}
interface EnabledPresets {
AlphaTest: boolean;
}
interface Flags {
AMBIENT_ATTENUATION_BOX: number;
BLENDING: number;
}
interface Properties {
alphatestThreshold: ArrayBuffer;
attenuationBoxHalfSize: ArrayBuffer;
attenuationBoxSmoothness: ArrayBuffer;
baseColorFactor?: ArrayBuffer;
decalTileCoordScale?: ArrayBuffer;
inGlossiness: ArrayBuffer;
inSpecularity: ArrayBuffer;
metalFresnelReflectance: ArrayBuffer;
metallicRoughnessFactor?: ArrayBuffer;
}
interface TextureSampleStates {
albedo: number;
baseColorMap: number;
baseNormalMap: number;
baseRMMap: number;
decalmask?: number;
maskMap: number;
miscMap: number;
normalmap: number;
}
export interface Textures {
albedo: string;
baseColorMap?: string;
baseNormalMap?: string;
baseRMMap?: string;
decalmask?: string;
maskMap?: string;
miscMap?: string;
normalmap?: string;
}
export interface Hierarchy {
'##name': 'Entity';
'#hierarchy'?: Hierarchy[];
components: Components;
flags: number;
id: bigint;
name: string;
}
// 0001, 0002, 0003...: Component
type Components = {
count: number;
} & Record<string, Component>;
// 0001, 0002, 0003...
type RoBatches = Record<
string,
{
'##name': 'RenderBatch';
'rb.aabbox': RbAabbox;
'rb.classname': 'RenderBatch';
'rb.datasource': bigint;
'rb.nmatname': bigint;
'rb.sortingKey': number;
}
>;
interface RbAabbox {
minimum: number[];
maximum: number[];
}
export type Component =
| {
'comp.typename': 'TransformComponent';
'tc.localRotation': Vector4Tuple;
'tc.localScale': Vector3Tuple;
'tc.localTranslation': Vector3Tuple;
'tc.worldRotation': Vector4Tuple;
'tc.worldScale': Vector3Tuple;
'tc.worldTranslation': Vector3Tuple;
}
| {
'comp.typename': 'RenderComponent';
'rc.renderObj': {
'##name': 'Mesh';
'ro.batchCount': number;
'ro.batches': RoBatches;
'ro.debugflags': number;
'ro.flags': number;
'ro.notShadowOnly': boolean;
'ro.sOclCull': boolean;
'ro.sOclIndex': number;
} & Record<`rb${number}.${'lodIndex' | 'switchIndex'}`, number>;
}
| {
'comp.typename': 'SlotComponent';
'sc.attachmentRotation': number[];
'sc.attachmentScale': number[];
'sc.attachmentTranslation': number[];
'sc.configFilePath': string;
'sc.slotName': string;
'sc.template': string;
'sc.typeFiltersCount': number;
}
| {
'comp.typename': 'LodComponent';
'lc.loddist': Record<`distance${number}`, number>;
}
| {
'comp.typename': 'DecorItemComponent';
drawOrder: number;
shouldApplyCamo: boolean;
vanishTargetAlpha: number;
vanishWhenArmorShown: boolean;
}
| {
'comp.typename': 'NewSlotComponent';
slotItemsCount: number;
slots: {
[key: string]: {
configFilePath: string;
name: string;
transform: {
position: Vector3Tuple;
scale: Vector3Tuple;
rotation: Vector4Tuple;
};
};
};
}
| {
'comp.typename': 'ScenarioComponent';
'scenario.scriptId': string;
}
| ({
'comp.typename': 'StateSwitcherComponent';
'ssc.activeState': number;
'ssc.statesCount': number;
} & Record<`ssc.state${number}`, string>)
| {
'comp.typename': 'CustomPropertiesComponent';
'cpc.properties.archive': {
'editor.donotremove': number;
};
}
| ({
'comp.typename': 'ActionComponent';
'ac.actionCount': number;
} & Record<
string, // 0000, 0001, ...
{
'act.delay': number;
'act.delayVariation': number;
'act.entityName': string;
'act.event': number;
'act.eventToTrigger': string;
'act.motionFrameTime': number;
'act.motionSpeed': number;
'act.stateActivated': number;
'act.stopAfterNRepeats': number;
'act.stopWhenEmpty': boolean;
'act.switchIndex': number;
'act.type': number;
'act.userEventId': string;
}
>)
| {
'comp.typename': 'ParticleEffectComponent';
'pe.clearOnRestart': boolean;
'pe.effectDuration': number;
'pe.emitters': {
'##name': string;
'emitter.data': {
'emitter.id': `${number}`;
'emitter.quality': string;
}[];
'emitter.position': Vector3Tuple;
'emitter.rotation': Vector4Tuple;
'emitter.scale': Vector3Tuple;
}[];
'pe.inheritScale': boolean;
'pe.nestedEmitters': boolean;
'pe.repeatsCount': number;
'pe.startFromTime': number;
'pe.stopWhenEmpty': boolean;
'pe.version': number;
'ro.flags': number;
}
| {
'comp.typename': 'TankElementComponent';
isDestructible: boolean;
shouldCastShadow: boolean;
shouldGenerateSilhouette: boolean;
}
| {
'comp.typename': 'MotionComponent';
'motion.filepath': string;
'motion.playbackRate': number;
'simpleMotion.repeatsCount': number;
}
| {
'comp.typename': 'SkeletonComponent';
joints: Record<
string, // 0000, 0001, ...
{
'joint.bbox.max': Vector3Tuple;
'joint.bbox.min': Vector3Tuple;
'joint.bindPose': [
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
];
'joint.invBindPose': [
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
];
'joint.name': string;
'joint.parentIndex': number;
'joint.uid': string;
}
>;
jointsCount: number;
}
| {
'comp.typename': 'AnimationComponent';
animation: bigint;
animationTimeScale: number;
repeatsCount: number;
}
| {
'comp.typename': 'SkeletonJointBindingComponent';
count: number;
map: Record<
string, // 0000, 0001, ...
{
Uid: string;
jointUid: string;
}
>;
};
interface SceneComponents {
count: number;
}
export class Sc2ReadStream extends ScpgReadStream {
header() {
return {
name: this.ascii(4),
version: this.uint32(),
nodeCount: this.uint32(),
};
}
versionTags() {
return this.ka();
}
descriptor() {
const size = this.uint32();
const fileType = this.uint32();
// other felids that we don't have any info about
this.seek(size - 4);
return { size, fileType };
}
sc2() {
this.header();
this.versionTags();
this.descriptor();
return this.ka() as SC2;
}
}
+135
View File
@@ -0,0 +1,135 @@
import { ScpgReadStream, VertexAttribute } from './scpg';
interface PolygonGroupRaw {
'##name': 'PolygonGroup';
'#id': ArrayBuffer;
cubeTextureCoordCount: number;
indexCount: number;
indexFormat: 0 | 1;
indices: ArrayBuffer;
packing: 0 | 1;
primitiveCount: number;
rhi_primitiveType: 1 | 2 | 10;
textureCoordCount: number;
vertexCount: number;
vertexFormat: number;
vertices: ArrayBuffer;
}
type BlitzkitVertex = { attribute: VertexAttribute; value: number[] }[];
interface BlitzkitPolygonGroup {
vertices: BlitzkitVertex[];
indices: number[];
}
const vertexAttributesArray = Object.values(VertexAttribute).filter(
(value) => typeof value === 'number',
);
export const vertexAttributeVectorSizes = {
[VertexAttribute.VERTEX]: 3,
[VertexAttribute.NORMAL]: 3,
[VertexAttribute.COLOR]: 1,
[VertexAttribute.TEXCOORD0]: 2,
[VertexAttribute.TEXCOORD1]: 2,
[VertexAttribute.TEXCOORD2]: 2,
[VertexAttribute.TEXCOORD3]: 2,
[VertexAttribute.TANGENT]: 3,
[VertexAttribute.BINORMAL]: 3,
[VertexAttribute.HARD_JOINTINDEX]: 1,
[VertexAttribute.CUBETEXCOORD0]: 3,
[VertexAttribute.CUBETEXCOORD1]: 3,
[VertexAttribute.CUBETEXCOORD2]: 3,
[VertexAttribute.CUBETEXCOORD3]: 3,
[VertexAttribute.PIVOT4]: 4,
[VertexAttribute.PIVOT_DEPRECATED]: 3,
[VertexAttribute.FLEXIBILITY]: 1,
[VertexAttribute.ANGLE_SIN_COS]: 2,
[VertexAttribute.JOINTINDEX]: 4,
[VertexAttribute.JOINTWEIGHT]: 4,
} as const;
export class ScgReadStream extends ScpgReadStream {
header() {
return {
name: this.ascii(4),
version: this.uint32(),
nodeCount: this.uint32(),
nodeCount2: this.uint32(),
};
}
scg() {
const header = this.header();
const polygonGroupsRaw: PolygonGroupRaw[] = [];
const polygonGroups = new Map<bigint, BlitzkitPolygonGroup>();
for (let index = 0; index < header.nodeCount; index++) {
const polygonGroupRaw = this.ka() as PolygonGroupRaw;
polygonGroupsRaw.push(polygonGroupRaw);
}
polygonGroupsRaw.forEach((polygonGroupRaw) => {
const indicesStream = new ScpgReadStream(polygonGroupRaw.indices);
const indices: number[] = [];
for (let index = 0; index < polygonGroupRaw.indexCount; index++) {
indices.push(
polygonGroupRaw.indexFormat === 0
? indicesStream.uint16()
: indicesStream.uint32(),
);
}
const verticesStream = new ScpgReadStream(polygonGroupRaw.vertices);
const vertices: BlitzkitVertex[] = [];
const { format, stride } = ScgReadStream.parseVertexFormat(
polygonGroupRaw.vertexFormat,
);
const strideBasedSize = stride * polygonGroupRaw.vertexCount;
if (verticesStream.buffer.byteLength !== strideBasedSize) {
console.warn(
`Vertex stride mismatch; expected ${strideBasedSize}, got ${verticesStream.buffer.byteLength}; skipping...`,
);
return;
}
for (let index = 0; index < polygonGroupRaw.vertexCount; index++) {
vertices[index] = format.map((attribute) => ({
attribute,
value: verticesStream.vectorN(vertexAttributeVectorSizes[attribute]),
}));
}
polygonGroups.set(
new DataView(polygonGroupRaw['#id']).getBigUint64(0, true),
{
vertices,
indices,
},
);
});
return polygonGroups;
}
static parseVertexFormat(formatInt: number) {
const format: VertexAttribute[] = [];
let stride = 0;
vertexAttributesArray.forEach((attributeUntyped) => {
const attribute = attributeUntyped as VertexAttribute;
const mask = 1 << attribute;
if (formatInt & mask) {
format.push(attribute);
stride += vertexAttributeVectorSizes[attribute] * 4;
}
});
return { format, stride };
}
}
+263
View File
@@ -0,0 +1,263 @@
import { times } from 'lodash';
import { ReadStream } from './buffer';
enum KAType {
NONE = 0,
BOOLEAN = 1,
INT32 = 2,
FLOAT = 3,
STRING = 4,
WIDE_STRING = 5,
BYTE_ARRAY = 6,
UINT32 = 7,
KEYED_ARCHIVE = 8,
INT64 = 9,
UINT64 = 10,
VECTOR2 = 11,
VECTOR3 = 12,
VECTOR4 = 13,
MATRIX2 = 14,
MATRIX3 = 15,
MATRIX4 = 16,
COLOR = 17,
FASTNAME = 18,
AABBOX3 = 19,
FILEPATH = 20,
FLOAT64 = 21,
INT8 = 22,
UINT8 = 23,
INT16 = 24,
UINT16 = 25,
ARRAY = 27,
TRANSFORM = 29,
}
export enum VertexAttribute {
VERTEX = 0,
NORMAL = 1,
COLOR = 2,
TEXCOORD0 = 3,
TEXCOORD1 = 4,
TEXCOORD2 = 5,
TEXCOORD3 = 6,
TANGENT = 7,
BINORMAL = 8,
HARD_JOINTINDEX = 9,
PIVOT4 = 10,
PIVOT_DEPRECATED = 11,
FLEXIBILITY = 12,
ANGLE_SIN_COS = 13,
JOINTINDEX = 14,
JOINTWEIGHT = 15,
CUBETEXCOORD0 = 16,
CUBETEXCOORD1 = 17,
CUBETEXCOORD2 = 18,
CUBETEXCOORD3 = 19,
}
export class ScpgReadStream extends ReadStream {
vectorN(size: number) {
return times(size, () => this.float32());
}
vector2() {
return this.vectorN(2);
}
vector3() {
return this.vectorN(3);
}
vector4() {
return this.vectorN(4);
}
matrixN(size: number) {
return times(size, () => this.vectorN(size));
}
matrix2() {
return this.matrixN(2);
}
matrix3() {
return this.matrixN(3);
}
matrix4() {
return this.matrixN(4);
}
kaValue(
type: KAType = this.uint8(),
stringTable?: Record<number, string>,
): unknown {
switch (type) {
case KAType.NONE:
return undefined;
case KAType.FILEPATH:
case KAType.STRING: {
if (stringTable) {
const id = this.uint32();
return stringTable[id];
} else {
const length = this.uint32();
return this.ascii(length);
}
}
case KAType.BYTE_ARRAY: {
const length = this.uint32();
return this.byte(length);
}
case KAType.FLOAT:
return this.float32();
case KAType.ARRAY: {
const length = this.uint32();
const value = [];
for (let index = 0; index < length; index++) {
value.push(this.kaValue(undefined, stringTable));
}
return value;
}
case KAType.KEYED_ARCHIVE: {
this.seek(4); // UInt32 length of the nested archive in bytes
return this.ka(stringTable);
}
case KAType.INT8:
return this.int8();
case KAType.INT16:
return this.int16();
case KAType.INT32:
return this.int32();
case KAType.INT64:
return this.int64();
case KAType.UINT8:
return this.uint8();
case KAType.UINT16:
return this.uint16();
case KAType.UINT32:
return this.uint32();
case KAType.UINT64:
return this.uint64();
case KAType.BOOLEAN:
return this.boolean();
case KAType.VECTOR2:
return this.vector2();
case KAType.VECTOR3:
return this.vector3();
case KAType.VECTOR4:
return this.vector4();
case KAType.COLOR:
return this.vector4(); // as RGBA
case KAType.MATRIX2:
return this.matrix2();
case KAType.MATRIX3:
return this.matrix3();
case KAType.MATRIX4:
return this.matrix4();
case KAType.AABBOX3: {
const minimum = this.vector3();
const maximum = this.vector3();
return { minimum, maximum };
}
case KAType.FASTNAME: {
if (!stringTable) throw new Error('No string table provided');
const index = this.uint32();
return stringTable[index];
}
case KAType.TRANSFORM: {
const position = this.vector3();
const scale = this.vector3();
const rotation = this.vector4();
return {
position,
scale,
rotation,
};
}
default:
throw new TypeError(
`Unhandled KA type: ${type} (${KAType[type] ?? 'unknown type'})`,
);
}
}
ka(stringTable?: Record<number, string>) {
this.seek(2); // "KA"
const version = this.uint16();
const pairs: Record<string, any> = {};
if (version === 0x0001) {
const count = this.uint32();
for (let index = 0; index < count; index++) {
const name = this.kaValue(undefined, stringTable) as string;
const value = this.kaValue(undefined, stringTable);
pairs[name] = value;
}
} else if (version === 0x0002) {
const count = this.uint32();
const strings: string[] = [];
const stringTable: Record<number, string> = {};
for (let index = 0; index < count; index++) {
const length = this.uint16();
const string = this.ascii(length);
strings.push(string);
}
for (let index = 0; index < count; index++) {
const id = this.uint32();
stringTable[id] = strings[index];
}
const nodeCount = this.uint32();
for (let index = 0; index < nodeCount; index++) {
const keyId = this.uint32();
const value = this.kaValue(undefined, stringTable);
const key = stringTable[keyId];
pairs[key] = value;
}
} else if (version === 0x0102) {
if (!stringTable) throw new Error('No string table provided');
const count = this.uint32();
for (let index = 0; index < count; index++) {
const keyIndex = this.uint32();
const key = stringTable[keyIndex];
const valueType = this.uint8();
if (valueType === KAType.STRING) {
const valueIndex = this.uint32();
const value = stringTable[valueIndex];
pairs[key] = value;
} else {
const value = this.kaValue(valueType, stringTable);
pairs[key] = value;
}
}
} else if (version === 0xff02) {
// same as 0x0102 but empty and with no count
return {};
} else throw new RangeError(`Unhandled KA version: ${version}`);
return pairs;
}
}
+7
View File
@@ -0,0 +1,7 @@
import { ReadStream } from './buffer';
export class WindowsReadStream extends ReadStream {
dword() {
return this.uint32();
}
}