mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 03:29:51 +02:00
feat: code split streams
This commit is contained in:
1 parent
510eebd2f4
commit
cb87a3067c
9 files changed
+856
-832
No files matched your search
@@ -0,0 +1,67 @@
|
||||
export class PrimitiveStream {
|
||||
public index = 0;
|
||||
constructor(public buffer: Buffer) {}
|
||||
|
||||
skip(size: number) {
|
||||
this.index += size;
|
||||
}
|
||||
read(size: number) {
|
||||
return this.buffer.subarray(this.index, this.index + size);
|
||||
}
|
||||
consume(size: number) {
|
||||
const subarray = this.read(size);
|
||||
this.skip(size);
|
||||
|
||||
return subarray;
|
||||
}
|
||||
|
||||
readRemaining() {
|
||||
return this.read(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
readRemainingLength() {
|
||||
return this.buffer.length - this.index;
|
||||
}
|
||||
consumeRemaining() {
|
||||
return this.consume(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
consumeByteArray(size: number) {
|
||||
return this.consume(size);
|
||||
}
|
||||
|
||||
consumeAscii(size: number) {
|
||||
return this.consume(size).toString('ascii');
|
||||
}
|
||||
|
||||
int8() {
|
||||
return this.consume(1).readInt8();
|
||||
}
|
||||
int16() {
|
||||
return this.consume(2).readInt16LE();
|
||||
}
|
||||
int32() {
|
||||
return this.consume(4).readInt32LE();
|
||||
}
|
||||
int64() {
|
||||
return this.consume(8).readBigInt64LE();
|
||||
}
|
||||
uint8() {
|
||||
return this.consume(1).readUInt8();
|
||||
}
|
||||
uint16() {
|
||||
return this.consume(2).readUInt16LE();
|
||||
}
|
||||
uint32() {
|
||||
return this.consume(4).readUInt32LE();
|
||||
}
|
||||
uint64() {
|
||||
return this.consume(8).readBigUInt64LE();
|
||||
}
|
||||
float() {
|
||||
return this.consume(4).readFloatLE();
|
||||
}
|
||||
|
||||
boolean() {
|
||||
return this.consume(1).readUInt8() === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { Vector3Tuple, Vector4Tuple } from 'three';
|
||||
import { ScpgStream } from './scpg';
|
||||
|
||||
export interface SC2 {
|
||||
'#dataNodes': DataNode[];
|
||||
'#hierarchy': Hierarchy[];
|
||||
'#sceneComponents': SceneComponents;
|
||||
}
|
||||
|
||||
type DataNode = {
|
||||
'##name': 'NMaterial';
|
||||
'#id': Buffer;
|
||||
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;
|
||||
}
|
||||
|
||||
interface Properties {
|
||||
alphatestThreshold: Buffer;
|
||||
attenuationBoxHalfSize: Buffer;
|
||||
attenuationBoxSmoothness: Buffer;
|
||||
baseColorFactor?: Buffer;
|
||||
decalTileCoordScale?: Buffer;
|
||||
inGlossiness: Buffer;
|
||||
inSpecularity: Buffer;
|
||||
metalFresnelReflectance: Buffer;
|
||||
metallicRoughnessFactor?: Buffer;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
interface SceneComponents {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class Sc2Stream extends ScpgStream {
|
||||
header() {
|
||||
return {
|
||||
name: this.consumeAscii(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.skip(size - 4);
|
||||
|
||||
return { size, fileType };
|
||||
}
|
||||
|
||||
sc2() {
|
||||
this.header();
|
||||
this.versionTags();
|
||||
this.descriptor();
|
||||
|
||||
return this.ka() as SC2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { ScpgStream, VertexAttribute } from './scpg';
|
||||
|
||||
interface PolygonGroupRaw {
|
||||
'##name': 'PolygonGroup';
|
||||
'#id': Buffer;
|
||||
cubeTextureCoordCount: number;
|
||||
indexCount: number;
|
||||
indexFormat: 0 | 1;
|
||||
indices: Buffer;
|
||||
packing: 0 | 1;
|
||||
primitiveCount: number;
|
||||
rhi_primitiveType: 1 | 2 | 10;
|
||||
textureCoordCount: number;
|
||||
vertexCount: number;
|
||||
vertexFormat: number;
|
||||
vertices: Buffer;
|
||||
}
|
||||
|
||||
type BlitzkriegVertex = { attribute: VertexAttribute; value: number[] }[];
|
||||
|
||||
interface BlitzkriegPolygonGroup {
|
||||
vertices: BlitzkriegVertex[];
|
||||
indices: 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 ScgStream extends ScpgStream {
|
||||
header() {
|
||||
return {
|
||||
name: this.consumeAscii(4),
|
||||
version: this.uint32(),
|
||||
nodeCount: this.uint32(),
|
||||
nodeCount2: this.uint32(),
|
||||
};
|
||||
}
|
||||
|
||||
scg() {
|
||||
const header = this.header();
|
||||
const polygonGroupsRaw: PolygonGroupRaw[] = [];
|
||||
const polygonGroups = new Map<bigint, BlitzkriegPolygonGroup>();
|
||||
|
||||
for (let index = 0; index < header.nodeCount; index++) {
|
||||
const polygonGroupRaw = this.ka() as PolygonGroupRaw;
|
||||
polygonGroupsRaw.push(polygonGroupRaw);
|
||||
}
|
||||
|
||||
polygonGroupsRaw.forEach((polygonGroupRaw) => {
|
||||
const indicesStream = new ScpgStream(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 ScpgStream(polygonGroupRaw.vertices);
|
||||
const vertices: BlitzkriegVertex[] = [];
|
||||
const stride =
|
||||
ScgStream.vertexStride(polygonGroupRaw.vertexFormat) *
|
||||
polygonGroupRaw.vertexCount;
|
||||
|
||||
if (verticesStream.buffer.length !== stride) {
|
||||
console.warn(
|
||||
`Vertex stride mismatch; expected ${stride}, got ${verticesStream.buffer.length}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexFormat = (
|
||||
polygonGroupRaw.vertexFormat
|
||||
.toString(2)
|
||||
.split('')
|
||||
.map((bitString, index) =>
|
||||
bitString === '1' ? index : null,
|
||||
) as VertexAttribute[]
|
||||
).filter((type) => type !== null);
|
||||
|
||||
for (let index = 0; index < polygonGroupRaw.vertexCount; index++) {
|
||||
vertices[index] = vertexFormat.map((attribute) => {
|
||||
return {
|
||||
attribute,
|
||||
value: verticesStream.vectorN(
|
||||
vertexAttributeVectorSizes[attribute],
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (verticesStream.readRemainingLength() !== 0) {
|
||||
throw new RangeError('Vertices stream was not fully consumed');
|
||||
}
|
||||
|
||||
polygonGroups.set(polygonGroupRaw['#id'].readBigUInt64LE(), {
|
||||
vertices,
|
||||
indices,
|
||||
});
|
||||
});
|
||||
|
||||
return polygonGroups;
|
||||
}
|
||||
|
||||
static vertexStride(vertexFormat: number) {
|
||||
const flags = vertexFormat.toString(2);
|
||||
|
||||
return Object.values(VertexAttribute)
|
||||
.filter((value) => typeof value === 'number')
|
||||
.reduce(
|
||||
(accumulator, attribute) =>
|
||||
flags[attribute as VertexAttribute] === '1'
|
||||
? accumulator +
|
||||
vertexAttributeVectorSizes[attribute as VertexAttribute] * 4
|
||||
: accumulator,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { times } from 'lodash';
|
||||
import { PrimitiveStream } from './primitive';
|
||||
|
||||
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 ScpgStream extends PrimitiveStream {
|
||||
vectorN(size: number) {
|
||||
return times(size, () => this.float());
|
||||
}
|
||||
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.consumeAscii(length);
|
||||
}
|
||||
}
|
||||
|
||||
case KAType.BYTE_ARRAY: {
|
||||
const length = this.uint32();
|
||||
return this.consumeByteArray(length);
|
||||
}
|
||||
|
||||
case KAType.FLOAT:
|
||||
return this.float();
|
||||
|
||||
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.skip(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.skip(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.consumeAscii(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,3 @@
|
||||
import { PrimitiveStream } from './primitive';
|
||||
|
||||
export class WindowsStream extends PrimitiveStream {}
|
||||
Reference in new issue
Block a user