mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: migration 2
This commit is contained in:
1 parent
e1c8194beb
commit
6285f33989
305 files changed
+414
-414
No files matched your search
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ReadStream } from './buffer';
|
||||
|
||||
export class WindowsReadStream extends ReadStream {
|
||||
dword() {
|
||||
return this.uint32();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user