From 43b6453fcbbd4ea7fabd90415495334d14bc7dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:06:26 -0600 Subject: [PATCH] feat: bkon read --- .vscode/settings.json | 3 + docs/guide/bkon.md | 163 +++++++++++++++++++++++++++++++++++++ src/core/streams/bkon.ts | 146 +++++++++++++++++++++++++++++++++ src/core/streams/buffer.ts | 5 +- src/core/streams/scpg.ts | 4 +- 5 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 docs/guide/bkon.md create mode 100644 src/core/streams/bkon.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a205dbee..d9cacd40f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,7 @@ "bepaint", "BINORMAL", "Binormals", + "BKON", "bkwd", "blitzstars", "Camo", @@ -100,6 +101,8 @@ "statsfull", "steamapps", "Stringifying", + "struct", + "structs", "strvk", "subnation", "Synx", diff --git a/docs/guide/bkon.md b/docs/guide/bkon.md new file mode 100644 index 000000000..a6a2f2ca5 --- /dev/null +++ b/docs/guide/bkon.md @@ -0,0 +1,163 @@ +# Blitzkrieg Object Notation + +A compact binary representation of JSON objects. This documentation uses a frankenstein version of C++ structs that I came up with. Deal with it. + +## Primitives + +```cpp +struct String { + uint32 length; + ascii[length] value; +} +``` + +## Reading + +- Everything is little endian +- Magic is always `"BKON"` + +```cpp +struct BlitzkriegObjectNotation { + ascii[4] magic; + Header header; + Body body; +} +``` + +### Header + +- Version is always `1` + +```cpp +struct Header { + uint16 version; + StringTable stringTable; +} + +struct StringTable { + uint32 count; + uint32[count] ids; + String[count] strings; +} +``` + +### Body + +```cpp +// identical to ValueObject except with no type +struct Body { + uint32 count; + (ValueString | ValueFastString)[count] keys; + Value[count] values; +} + +enum ValueType { + Null, + Boolean, + Uint8, + Uint16, + Uint32, + Uint64, + Int8, + Int16, + Int32, + Int64, + Float32, + Float64, + String, + FastString, + Array, + Object, +} + +struct Value { + uint8 type; + // what comes next depends on type +} + +struct ValueNull { + ValueType.Null type; +} + +struct ValueBoolean { + ValueType.Boolean type; + uint8 value; +} + +struct ValueUint8 { + ValueType.Uint8 type; + uint8 value; +} + +struct ValueUint16 { + ValueType.Uint16 type; + uint16 value; +} + +struct ValueUint32 { + ValueType.Uint32 type; + uint32 value; +} + +struct ValueUint64 { + ValueType.Uint64 type; + uint64 value; +} + +struct ValueInt8 { + ValueType.Int8 type; + int8 value; +} + +struct ValueInt16 { + ValueType.Int16 type; + int16 value; +} + +struct ValueInt32 { + ValueType.Int32 type; + int32 value; +} + +struct ValueInt64 { + ValueType.Int64 type; + int64 value; +} + +struct ValueFloat32 { + ValueType.Float32 type; + float32 value; +} + +struct ValueFloat64 { + ValueType.Float64 type; + float64 value; +} + +struct ValueString { + ValueType.String type; + String value; +} + +struct ValueFastString { + ValueType.FastString type; + uint32 id; +} + +struct ValueArray { + ValueType.Array type; + uint32 count; + Value[count] values; +} + +struct ValueObject { + ValueType.Object type; + uint32 count; + (ValueString | ValueFastString)[count] keys; + Value[count] values; +} +``` + +## Writing + +It's pretty obvious how to write. Remember that a `FastString` should only be applied to the string table if the string is greater than 4 characters and shows up more than once in the body. Otherwise, there's no point and `String` should be used. diff --git a/src/core/streams/bkon.ts b/src/core/streams/bkon.ts new file mode 100644 index 000000000..9b4b14d18 --- /dev/null +++ b/src/core/streams/bkon.ts @@ -0,0 +1,146 @@ +import { times } from 'lodash'; +import { BufferStream } from './buffer'; + +enum ValueType { + Null, + Boolean, + Uint8, + Uint16, + Uint32, + Uint64, + Int8, + Int16, + Int32, + Int64, + Float32, + Float64, + String, + FastString, + Array, + Object, +} + +type Value = + | null + | boolean + | number + | bigint + | string + | Value[] + | { [key: string]: Value }; + +export class BkonStream extends BufferStream { + bkon() { + this.magic(); + const header = this.header(); + const body = this.body(header.stringTable); + + return body; + } + + magic() { + if (this.ascii(4) !== 'BKON') { + throw new Error('Invalid BKON magic number'); + } + } + + header() { + return { + version: this.uint16(), + stringTable: this.stringTable(), + }; + } + + body(stringTable: Record) { + const count = this.uint32(); + const keys = times(count, () => this.value(stringTable) as string); + const values = times(count, () => this.value(stringTable)); + const body: Record = {}; + + keys.forEach((key, index) => (body[key] = values[index])); + + return body; + } + + stringTable() { + const count = this.uint32(); + const ids = times(count, this.uint32); + const strings = times(count, this.string); + const table: Record = {}; + + ids.forEach((id, index) => (table[id] = strings[index])); + + return table; + } + + string() { + const length = this.uint32(); + const string = this.ascii(length); + return string; + } + + value(stringTable: Record): Value { + const type = this.uint8(); + + 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: + return stringTable[this.uint32()]; + + case ValueType.Array: + return times(this.uint32(), () => this.value(stringTable)); + + case ValueType.Object: { + const count = this.uint32(); + const keys = times(count, () => this.value(stringTable) as string); + const values = times(count, () => this.value(stringTable)); + const object: Record = {}; + + keys.forEach((string, index) => (object[string] = values[index])); + + return object; + } + + default: + throw new Error(`Invalid value type: ${type}`); + } + } +} diff --git a/src/core/streams/buffer.ts b/src/core/streams/buffer.ts index 266056c7f..507878ab7 100644 --- a/src/core/streams/buffer.ts +++ b/src/core/streams/buffer.ts @@ -60,9 +60,12 @@ export class BufferStream { uint64() { return this.buffer.readBigUInt64LE(this.increment(8)); } - float() { + float32() { return this.buffer.readFloatLE(this.increment(4)); } + float64() { + return this.buffer.readDoubleLE(this.increment(8)); + } boolean() { return this.buffer.readUInt8(this.increment(1)) === 1; diff --git a/src/core/streams/scpg.ts b/src/core/streams/scpg.ts index b1142b501..fe8f7e15f 100644 --- a/src/core/streams/scpg.ts +++ b/src/core/streams/scpg.ts @@ -57,7 +57,7 @@ export enum VertexAttribute { export class ScpgStream extends BufferStream { vectorN(size: number) { - return times(size, () => this.float()); + return times(size, () => this.float32()); } vector2() { return this.vectorN(2); @@ -107,7 +107,7 @@ export class ScpgStream extends BufferStream { } case KAType.FLOAT: - return this.float(); + return this.float32(); case KAType.ARRAY: { const length = this.uint32();