mirror of
https://github.com/skillbert/rsmv.git
synced 2026-02-02 09:31:21 -05:00
373 lines
17 KiB
TypeScript
373 lines
17 KiB
TypeScript
type PrimitiveInt = {
|
|
primitive: "int",
|
|
unsigned: boolean,
|
|
bytes: number,
|
|
variable: boolean,
|
|
endianness: "big" | "little"
|
|
};
|
|
type PrimitiveBool = {
|
|
primitive: "bool"
|
|
}
|
|
type PrimitiveValue<T> = {
|
|
primitive: "value",
|
|
value: T
|
|
}
|
|
type PrimitiveString = {
|
|
primitive: "string",
|
|
encoding: "latin1",
|
|
termination: null
|
|
}
|
|
type PrimitiveSwitch = {
|
|
primitive: "switch",
|
|
offset: number,
|
|
switch: { [op: number]: string }
|
|
}
|
|
|
|
export type SimplePrimitive<T> = PrimitiveInt | PrimitiveBool | PrimitiveString | PrimitiveValue<T>;
|
|
export type Primitive<T> = SimplePrimitive<T> | PrimitiveSwitch;
|
|
export type ChunkType<T> = Primitive<T> | string;//keyof typeof typedef;
|
|
|
|
export type SimpleComposedChunk = string
|
|
| ["array", ComposedChunk[]]
|
|
| ["map", string, ComposedChunk]
|
|
| [...ComposedChunk[]]
|
|
//typescript complains about circular refence otherwise if these aren't split up
|
|
export type ComposedChunk = SimpleComposedChunk
|
|
| ["struct", ...([string, SimpleComposedChunk])[]]
|
|
|
|
|
|
export type OpcodeMap = { [opcode: string]: { name: string, read: ComposedChunk } };
|
|
|
|
type TypeDef = { [name: string]: ChunkType<any> };
|
|
|
|
export class Reader {
|
|
typedef: TypeDef;
|
|
opcodes: OpcodeMap;
|
|
//TODO this is kinda dumb
|
|
_read = _read;
|
|
_readPrimitive = _readPrimitive;
|
|
_write = _write;
|
|
_writePrimitive = _writePrimitive;
|
|
constructor(typedef: TypeDef, opcodes: OpcodeMap) {
|
|
this.typedef = typedef;
|
|
this.opcodes = opcodes;
|
|
}
|
|
|
|
/**
|
|
* @param {Buffer} buffer The decompressed buffer of the item
|
|
*/
|
|
read(bufferin: Buffer) {
|
|
let buffer = bufferin as Buffer & { scan: number };
|
|
var output = {};
|
|
buffer.scan = 0;
|
|
var history: string[] = [];
|
|
try {
|
|
//TODO why is last terminating byte skipped?
|
|
while (buffer.scan < buffer.length - 1) {
|
|
var opcode = buffer.readUInt8(buffer.scan); buffer.scan++;
|
|
//if (opcode == 0x0) break;
|
|
if (!(opcode in this.opcodes)) {
|
|
throw `Unsupported opcode '0x${opcode.toString(16)}' at 0x${(buffer.scan - 1).toString(16)}`;
|
|
}
|
|
//if (opcode == 0x6a) throw `Found morphs_1`;
|
|
history.push(`0x${opcode.toString(16).padStart(2, "0")} (${this.opcodes[opcode].name}) at 0x${(buffer.scan - 1).toString(16).padStart(4, "0")}`);
|
|
output[this.opcodes[opcode].name] = this._read(buffer, this.opcodes[opcode].read);
|
|
}
|
|
} catch (e) {
|
|
console.log(output);
|
|
console.log(history);
|
|
throw e;
|
|
}
|
|
return output;
|
|
}
|
|
write(obj: Object) {
|
|
let buffer = Buffer.alloc(10 * 1024) as Buffer & { scan: number };
|
|
buffer.scan = 0;
|
|
for (let prop in obj) {
|
|
//TODO move this logic to a reverse lookup map
|
|
let op: ComposedChunk | null = null;
|
|
let opcodeid = 0;
|
|
for (let opcode in this.opcodes) {
|
|
if (this.opcodes[opcode].name == prop) {
|
|
op = this.opcodes[opcode].read;
|
|
opcodeid = +opcode;
|
|
}
|
|
}
|
|
if (!op) {
|
|
throw new Error(`no opcode found for prop ${prop}`);
|
|
}
|
|
buffer.writeUInt8(opcodeid, buffer.scan++);
|
|
this._write(buffer, op, obj[prop]);
|
|
}
|
|
buffer.writeUInt8(0x00, buffer.scan++);
|
|
|
|
return buffer.slice(0, buffer.scan);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Buffer} buffer The decompressed buffer of the item
|
|
*/
|
|
function _readPrimitive(this: Reader, buffer: Buffer & { scan: number }, primitive: Primitive<any>) {
|
|
if (!("primitive" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', needs to specify its datatype (e.g. "primitive": "int")`;
|
|
switch (primitive.primitive) {
|
|
case "bool":
|
|
let boolint = buffer.readUInt8(buffer.scan++);
|
|
if (boolint != 1 && boolint != 0) throw `value parsed as bool was not 0x00 or 0x01`
|
|
return boolint != 0;
|
|
case "int":
|
|
validateIntType(primitive);
|
|
var unsigned = primitive.unsigned;
|
|
var bytes = primitive.bytes;
|
|
var variable = primitive.variable;
|
|
var endianness = primitive.endianness;
|
|
let output = 0;
|
|
if (variable) {
|
|
var firstByte = buffer.readUInt8(buffer.scan);
|
|
|
|
var mask = 0xFF;
|
|
if ((firstByte & 0x80) != 0x80) bytes >>= 1; // Floored division by two when we don't have a continuation bit
|
|
else mask = 0x7F;
|
|
|
|
buffer[buffer.scan] &= mask;
|
|
if (!unsigned && (firstByte & 0x40) == 0x40) buffer[buffer.scan] |= 0x80; // If the number is signed and second-most-significant bit is 1, set the most-significant bit to 1 since it's no longer a continuation bit
|
|
output = buffer[`read${unsigned ? "U" : ""}Int${endianness.charAt(0).toUpperCase()}E`](buffer.scan, bytes); buffer.scan += bytes;
|
|
buffer[buffer.scan - bytes] = firstByte; // Set it back to what it was originally
|
|
} else {
|
|
output = buffer[`read${unsigned ? "U" : ""}Int${endianness.charAt(0).toUpperCase()}E`](buffer.scan, bytes); buffer.scan += bytes;
|
|
}
|
|
return output;
|
|
case "string":
|
|
validateStringType(primitive);
|
|
var encoding = primitive.encoding;
|
|
var termination = primitive.termination;
|
|
var end = buffer.scan;
|
|
for (; end < buffer.length; ++end) if ((termination === null && buffer.readUInt8(end) == 0x0) || (end - buffer.scan) == termination) break;
|
|
let outputstr = buffer.toString(encoding, buffer.scan, end);
|
|
buffer.scan = end + 1;
|
|
return outputstr;
|
|
case "switch":
|
|
if (!("offset" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'switch' variables need to specify an 'offset' to the switch`;
|
|
if (!("switch" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'switch' variables need to specify a 'switch' table`;
|
|
var firstByte = buffer.readUInt8(buffer.scan + primitive.offset);
|
|
if (!(firstByte.toString() in primitive.switch)) throw `Unexpected byte '${firstByte}' in a value typed as a switch`;
|
|
|
|
return this._read(buffer, primitive.switch[firstByte]);
|
|
case "value":
|
|
if (!("value" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'value' variables need to specify a 'value'`;
|
|
return primitive.value;
|
|
default:
|
|
//@ts-ignore
|
|
throw `Unsupported primitive '${primitive.primitive}' in typedef.json`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Buffer} buffer The decompressed buffer of the item
|
|
*/
|
|
function _read(this: Reader, buffer: Buffer & { scan: number }, readAs: ComposedChunk) {
|
|
const typedef = this.typedef;
|
|
switch (typeof readAs) {
|
|
case "string":
|
|
return this._readPrimitive(buffer, resolveAlias(readAs, typedef));
|
|
case "object":
|
|
if (!Array.isArray(readAs)) throw `Objects are unsupported as 'read' variables due to inconsistencies in variable order across different languages: ${JSON.stringify(readAs)}`;
|
|
if (readAs.length == 0) throw `'read' variables must either be a valid type-defining string, an array of type-defining strings / objects, or a valid type-defining object: ${JSON.stringify(readAs)}`;
|
|
switch (readAs[0]) {
|
|
case "array":
|
|
if (readAs.length == 1) throw `'read' variables interpretted as an array must contain items: ${JSON.stringify(readAs)}`;
|
|
var output: any[] = [];
|
|
var count = this._readPrimitive(buffer, resolveAlias("variable unsigned short", typedef));//buffer.readUInt8(buffer.scan); buffer.scan++;
|
|
for (var i = 0; i < count; ++i) output.push(this._read(buffer, readAs[1]));
|
|
return output;
|
|
case "map":
|
|
if (readAs.length == 1) throw `'read' variables interpretted as a map must contain items: ${JSON.stringify(readAs)}`;
|
|
var outputobj: Object = {}
|
|
var keycount = buffer.readUInt8(buffer.scan); buffer.scan++;
|
|
for (var i = 0; i < keycount; ++i) outputobj["$"+this._read(buffer, readAs[1])] = this._read(buffer, readAs[2]);
|
|
return outputobj;
|
|
case "struct":
|
|
if (readAs.length == 1) throw `'read' variables interpretted as a struct must contain items: ${JSON.stringify(readAs)}`;
|
|
var outputobj: Object = {};
|
|
for (var i = 1; i < readAs.length; ++i) {
|
|
let [name, type] = readAs[i] as [string, ComposedChunk];
|
|
outputobj[name] = this._read(buffer, type);
|
|
}
|
|
return outputobj;
|
|
default: // Tuple
|
|
var output: any[] = [];
|
|
for (var i = 0; i < readAs.length; ++i) output.push(this._read(buffer, readAs[i]));
|
|
return output;
|
|
}
|
|
default:
|
|
throw `'read' variables must either be a valid type-defining string, an array of type-defining strings / objects, or a valid type-defining object: ${JSON.stringify(readAs)}`;
|
|
}
|
|
}
|
|
|
|
function resolveAlias(typename: string, typedef: TypeDef) {
|
|
let newtype: Primitive<any> | string = typename;
|
|
for (let redirects = 0; redirects < 1024; redirects++) {
|
|
if (!Object.prototype.hasOwnProperty.call(typedef, newtype)) {
|
|
throw `Type '${typename}' not found in typedef.json`;
|
|
}
|
|
newtype = typedef[newtype];
|
|
if (typeof newtype != "string") {
|
|
return newtype;
|
|
}
|
|
}
|
|
throw `Couldn't resolve alias stack for '${typename}', perhaps due to an infinite loop - last known alias was '${newtype!}'`;
|
|
}
|
|
function _write(this: Reader, buffer: Buffer & { scan: number }, writeAs: ComposedChunk, value: unknown) {
|
|
const typedef = this.typedef;
|
|
switch (typeof writeAs) {
|
|
case "string":
|
|
return this._writePrimitive(buffer, resolveAlias(writeAs, typedef), value);
|
|
case "object":
|
|
if (!Array.isArray(writeAs)) throw `Objects are unsupported as 'read' variables due to inconsistencies in variable order across different languages: ${JSON.stringify(writeAs)}`;
|
|
if (writeAs.length == 0) throw `'read' variables must either be a valid type-defining string, an array of type-defining strings / objects, or a valid type-defining object: ${JSON.stringify(writeAs)}`;
|
|
switch (writeAs[0]) {
|
|
case "array":
|
|
if (writeAs.length == 1) throw `'read' variables interpretted as an array must contain items: ${JSON.stringify(writeAs)}`;
|
|
if (!Array.isArray(value)) throw `array expected`;
|
|
this._writePrimitive(buffer, resolveAlias("variable unsigned short", typedef), value.length);
|
|
for (let val of value) {
|
|
this._write(buffer, writeAs[1], val);
|
|
}
|
|
break;
|
|
case "map":
|
|
if (writeAs.length == 1) throw `'read' variables interpretted as a map must contain items: ${JSON.stringify(writeAs)}`;
|
|
if (typeof value != "object" || !value) throw `object expected`;
|
|
|
|
buffer.writeUInt8(Object.keys(value).length, buffer.scan++);
|
|
for (let key of Object.keys(value)) {
|
|
this._write(buffer, writeAs[1], +key.slice(1));
|
|
this._write(buffer, writeAs[2], value[key]);
|
|
}
|
|
break;
|
|
case "struct":
|
|
if (writeAs.length == 1) throw `'read' variables interpretted as a struct must contain items: ${JSON.stringify(writeAs)}`;
|
|
if (typeof value != "object" || !value) throw `object expected`;
|
|
for (var i = 1; i < writeAs.length; ++i) {
|
|
let [name, type] = writeAs[i] as [string, ComposedChunk];
|
|
this._write(buffer, type, value[name]);
|
|
}
|
|
break;
|
|
default: // Tuple
|
|
if (!Array.isArray(value)) throw `array expected`;
|
|
if (value.length != writeAs.length) throw `wrong number of values in tuple, ${value.length} received, ${writeAs.length} expected`;
|
|
for (var i = 0; i < writeAs.length; ++i) {
|
|
this._write(buffer, writeAs[i], value[i]);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
throw `'read' variables must either be a valid type-defining string, an array of type-defining strings / objects, or a valid type-defining object: ${JSON.stringify(writeAs)}`;
|
|
}
|
|
}
|
|
|
|
//TODO validate these at startup instead of during decode/encode?
|
|
function validateIntType(primitive: PrimitiveInt) {
|
|
var hasUnsigned = "unsigned" in primitive;
|
|
var hasBytes = "bytes" in primitive;
|
|
var hasVariable = "variable" in primitive;
|
|
var hasEndianness = "endianness" in primitive;
|
|
if (!(hasUnsigned && hasBytes && hasVariable && hasEndianness)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'int' variables need to specify 'unsigned', 'bytes', 'variable', and 'endianness'`;
|
|
if (typeof primitive.unsigned !== "boolean") throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'unsigned' must be a boolean`;
|
|
if (typeof primitive.bytes !== "number") throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'bytes' must be an integer`;
|
|
if (typeof primitive.variable !== "boolean") throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'variable' must be a boolean`;
|
|
if (primitive.endianness !== "big" && primitive.endianness !== "little") throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'endianness' must be "big" or "little"`;
|
|
}
|
|
|
|
function validateStringType(primitive: PrimitiveString) {
|
|
var hasEncoding = "encoding" in primitive;
|
|
if (!hasEncoding) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'string' variables need to specify 'encoding'`;
|
|
if (typeof primitive.encoding !== "string") throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'encoding' must be a string`;
|
|
if (!(primitive.termination === null || typeof primitive.termination === "number")) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'termination' must be null or the string's length in bytes`;
|
|
}
|
|
|
|
function _writePrimitive(this: Reader, buffer: Buffer & { scan: number }, primitive: Primitive<any>, value: unknown) {
|
|
if (!("primitive" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', needs to specify its datatype (e.g. "primitive": "int")`;
|
|
switch (primitive.primitive) {
|
|
case "bool":
|
|
if (typeof value != "boolean") throw `boolean expected`;
|
|
buffer.writeUInt8(value ? 1 : 0, buffer.scan++);
|
|
break;
|
|
case "int":
|
|
if (typeof value != "number" || value % 1 != 0) throw `integer expected`;
|
|
validateIntType(primitive);
|
|
var unsigned = primitive.unsigned;
|
|
var bytes = primitive.bytes;
|
|
var variable = primitive.variable;
|
|
var endianness = primitive.endianness;
|
|
let output = 0;
|
|
if (variable) {
|
|
if (endianness != "big") throw `variable length int only accepts big endian`;
|
|
let fitshalf = true;
|
|
if (unsigned) {
|
|
if (value >= 1 << (bytes * 4 - 1)) { fitshalf = false; }
|
|
} else {
|
|
if (value >= 1 << (bytes * 4 - 2)) { fitshalf = false; }
|
|
if (value < -1 << (bytes * 4 - 2)) { fitshalf = false; }
|
|
}
|
|
|
|
if (fitshalf) bytes >>= 1; // Floored division by two when we don't have a continuation bit
|
|
|
|
let mask = ~(~0 << (bytes * 8 - 1));
|
|
let int = (value & mask) | ((fitshalf ? 0 : 1) << (bytes * 8 - 1));
|
|
//always write as signed since bitwise operations in js cast to int32
|
|
buffer[`writeIntBE`](int, buffer.scan, bytes);
|
|
buffer.scan += bytes;
|
|
} else {
|
|
output = buffer[`write${unsigned ? "U" : ""}Int${endianness.charAt(0).toUpperCase()}E`](value, buffer.scan, bytes);
|
|
buffer.scan += bytes;
|
|
}
|
|
break;
|
|
case "string":
|
|
if (typeof value != "string") throw `string expected`;
|
|
validateStringType(primitive);
|
|
var encoding = primitive.encoding;
|
|
var termination = primitive.termination;
|
|
let strbuf = Buffer.from(value, encoding);
|
|
//either pad with 0's to fixed length and truncate and longer strings, or add a single 0 at the end
|
|
let strbinbuf = Buffer.alloc(termination == null ? strbuf.byteLength + 1 : termination, 0);
|
|
strbuf.copy(strbinbuf, 0, 0, Math.max(strbuf.byteLength, strbinbuf.byteLength));
|
|
strbinbuf.copy(buffer, buffer.scan);
|
|
buffer.scan += strbinbuf.byteLength;
|
|
break;
|
|
case "switch":
|
|
if (!("offset" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'switch' variables need to specify an 'offset' to the switch`;
|
|
if (!("switch" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'switch' variables need to specify a 'switch' table`;
|
|
|
|
let switchbyte = "";
|
|
let switchprim: Primitive<any> | null = null;
|
|
for (let optbyte in primitive.switch) {
|
|
let opttypename = primitive.switch[optbyte];
|
|
let opttype = resolveAlias(opttypename, this.typedef);
|
|
//check if this switch opt is applicable (good enough hopefully)
|
|
if (typeof value == "number" && opttype.primitive != "int") { continue; }
|
|
if (typeof value == "boolean" && opttype.primitive != "bool") { continue; }
|
|
if (typeof value == "string" && opttype.primitive != "string") { continue; }
|
|
|
|
if (switchprim) { throw `multiple possible switch options while writing switch type`; }
|
|
switchprim = opttype;
|
|
switchbyte = optbyte;
|
|
}
|
|
if (!switchprim) throw `no compatible switch option found for value ${value}`;
|
|
|
|
//only currently used in [items|npcs|objects].extra which is a map with int32 keys, the first byte of that key is the switch
|
|
//it is the responsibility of that key to have the right flag currently
|
|
var firstByte = buffer.readUInt8(buffer.scan + primitive.offset);
|
|
if (firstByte != +switchbyte) throw `previously written switch byte did not match expected switch type while writing the value`;
|
|
this._writePrimitive(buffer, switchprim, value);
|
|
break;
|
|
case "value":
|
|
if (!("value" in primitive)) throw `Invalid primitive definition '${JSON.stringify(primitive)}', 'value' variables need to specify a 'value'`;
|
|
if (primitive.value != value) throw `expected constant ${primitive.value} was not present during write`;
|
|
break;
|
|
default:
|
|
//@ts-ignore
|
|
throw `Unsupported primitive '${primitive.primitive}' in typedef.json`;
|
|
}
|
|
}
|