@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
494 lines (381 loc) • 12.1 kB
JavaScript
import { assert } from "../../../../../core/assert.js";
const HEADER_TAG_NATIVE_CLASS = 0;
const HEADER_TAG_REF_SCHEMA = 1;
const HEADER_TAG_INLINE_SCHEMA = 2;
const HEADER_TAG_NULL = 4;
const HEADER_TAG_UNDEFINED = 5;
const HEADER_TAG_BOOLEAN_TRUE = 6;
const HEADER_TAG_BOOLEAN_FALSE = 7;
const HEADER_TAG_POSITIVE_INTEGER = 8;
const HEADER_TAG_NEGATIVE_INTEGER = 9;
const HEADER_TAG_FLOAT = 20;
const HEADER_TAG_STRING = 30;
class SchemaRecord {
address = -1
/**
*
* @type {Object|null}
*/
schema = null
/**
*
* @type {string|null}
*/
typeName = null
/**
*
* @type {{name:string}[]}
*/
fields = [];
/**
*
* @param {Object} schema
*/
fromSchema(schema) {
this.schema = schema;
this.fields.splice(0, this.fields.length);
for (const schemaKey in schema) {
this.fields.push({
name: schemaKey
});
}
}
buildSchema() {
const schema = {};
const fields = this.fields;
const field_count = fields.length;
for (let i = 0; i < field_count; i++) {
const field = fields[i];
schema[field.name] = {};
}
this.schema = schema;
}
}
/**
* Generate a schema from a value object
* @param {Object} object
* @returns
*/
function autoSchema(object) {
const schema = {};
for (const objectKey in object) {
const field_value = object[objectKey];
let field_schema;
if (typeof field_value === "object") {
field_schema = autoSchema(field_value);
} else {
// no schema for primitives
field_schema = undefined;
}
schema[objectKey] = field_schema;
}
return schema;
}
class SchemaTable {
/**
*
* @type {Map<number, SchemaRecord>}
*/
#address_map = new Map();
/**
*
* @type {Map<Object, SchemaRecord>}
*/
#schema_map = new Map();
reset() {
this.#address_map.clear();
this.#schema_map.clear();
}
/**
*
* @param {number} address
* @returns {SchemaRecord|undefined}
*/
getSchemaAt(address) {
return this.#address_map.get(address);
}
/**
*
* @param {Object} schema
* @returns {SchemaRecord}
*/
getRecord(schema) {
return this.#schema_map.get(schema);
}
/**
*
* @param {Object} schema
* @returns {number} -1 if not found
*/
getAddress(schema) {
const record = this.#schema_map.get(schema);
if (record === undefined) {
return -1;
}
return record.address;
}
/**
*
* @param {SchemaRecord} record
*/
add(record) {
this.#schema_map.set(record.schema, record);
this.#address_map.set(record.address, record);
}
}
export class BinaryObjectSerializationAdapter2 {
/**
*
* @type {BinarySerializationRegistry|null}
*/
#adapter_registry = null
/**
*
* @type {ModuleRegistry|null}
*/
#class_registry = null
#schema_table = new SchemaTable();
set module_registry(v) {
this.#class_registry = v;
}
get module_registry() {
return this.#class_registry;
}
set adapter_registry(v) {
this.#adapter_registry = v;
}
get adapter_registry() {
return this.#adapter_registry;
}
reset() {
this.#schema_table.reset();
}
/**
*
* @param {BinaryBuffer} buffer
* @param {Object} schema
* @param {string} typeName
* @returns {SchemaRecord}
*/
#writeSchema(buffer, schema, typeName) {
const address = buffer.position;
const record = new SchemaRecord();
record.typeName = typeName;
record.address = address;
record.fromSchema(schema);
// write type name
buffer.writeUTF8String(typeName);
const fields = record.fields;
const field_count = fields.length;
buffer.writeUintVar(field_count);
for (let i = 0; i < field_count; i++) {
// write field
const field = fields[i];
buffer.writeUTF8String(field.name);
}
this.#schema_table.add(record);
return record;
}
/**
*
* @param {BinaryBuffer} buffer
* @returns {SchemaRecord}
*/
#readSchema(buffer) {
const address = buffer.position;
const record = new SchemaRecord();
record.address = address;
// read type name
const typeName = buffer.readUTF8String();
record.typeName = typeName;
const field_count = buffer.readUintVar();
for (let i = 0; i < field_count; i++) {
const field_name = buffer.readUTF8String();
record.fields[i] = {
name: field_name
};
}
record.buildSchema();
this.#schema_table.add(record);
return record;
}
/**
*
* @param {BinaryBuffer} buffer
* @param {number|string|boolean|null|undefined|object} object
* @returns {boolean}
*/
#trySerializePrimitive(buffer, object) {
if (object === null) {
// special case
buffer.writeUint8(HEADER_TAG_NULL);
return true;
}
const typeOfValue = typeof object;
if (typeOfValue === "object") {
return false;
}
switch (typeOfValue) {
case "undefined":
buffer.writeUint8(HEADER_TAG_UNDEFINED);
break;
case "boolean":
buffer.writeUint8(object ? HEADER_TAG_BOOLEAN_TRUE : HEADER_TAG_BOOLEAN_FALSE);
break;
case "number":
if (Number.isInteger(object)) {
if (object >= 0) {
// positive integer
buffer.writeUint8(HEADER_TAG_POSITIVE_INTEGER);
buffer.writeUintVar(object);
} else {
// negative integer
buffer.writeUint8(HEADER_TAG_NEGATIVE_INTEGER);
buffer.writeUintVar(-object);
}
} else {
// float
buffer.writeUint8(HEADER_TAG_FLOAT);
buffer.writeFloat64(object);
}
break;
case "string":
buffer.writeUint8(HEADER_TAG_STRING);
buffer.writeUTF8String(object);
break;
default:
throw new TypeError(`Unsupported value type '${typeOfValue}'`);
}
return true;
}
/**
*
* @param {BinaryBuffer} buffer
* @param {Object} object
* @param {Object} [schema]
*/
serialize(buffer, object, schema) {
if (this.#trySerializePrimitive(buffer, object)) {
// was primitive, serialized
return;
}
const ctor = object.constructor;
const associatedNames = this.#class_registry.findNamesByModule(ctor);
let typeName;
if (associatedNames.length === 1) {
typeName = associatedNames[0];
} else {
// ambiguous, use type name from constructor
typeName = ctor.typeName
}
assert.isString(typeName, 'typeName');
const nativeAdapter = this.#adapter_registry.getAdapter(typeName);
const using_native_serializer = nativeAdapter !== undefined;
if (using_native_serializer) {
// write header indicating that native serializer is being used
buffer.writeUint8(HEADER_TAG_NATIVE_CLASS);
// name of the type
buffer.writeUTF8String(typeName);
// serialize with native
nativeAdapter.serialize(buffer, object);
return;
}
if (schema === undefined) {
// no schema specified, generate one
schema = autoSchema(object);
}
let record = this.#schema_table.getRecord(schema);
if (record === undefined) {
// no record
// indicating that the full schema follows
buffer.writeUint8(HEADER_TAG_INLINE_SCHEMA);
record = this.#writeSchema(buffer, schema, typeName);
} else {
buffer.writeUint8(HEADER_TAG_REF_SCHEMA);
// write address of schema
buffer.writeUintVar(record.address);
}
// write data according to schema
const fields = record.fields;
const field_count = fields.length;
for (let i = 0; i < field_count; i++) {
const field = fields[i];
const field_name = field.name;
const actual_value = object[field_name];
this.serialize(buffer, actual_value, schema[field_name]);
}
}
/**
*
* @template T
* @param {BinaryBuffer} buffer
* @param {number} tag
* @returns {T}
*/
#deserializeObject(buffer, tag) {
let record;
if (tag === HEADER_TAG_INLINE_SCHEMA) {
// read full schema here
record = this.#readSchema(buffer);
} else if (tag === HEADER_TAG_REF_SCHEMA) {
// reference to the schema else-where
const schema_address = buffer.readUintVar();
record = this.#schema_table.getRecord(schema_address);
}
const typeName = record.typeName;
const Klass = this.#class_registry.get(typeName);
const object = new Klass();
const fields = record.fields;
const field_count = fields.length;
for (let i = 0; i < field_count; i++) {
const field = fields[i];
object[field.name] = this.deserialize(buffer);
}
return object;
}
/**
* @template T
* @param {BinaryBuffer} buffer
* @returns {T}
*/
deserialize(buffer) {
// read header
const header = buffer.readUint8();
switch (header) {
case HEADER_TAG_NATIVE_CLASS:
// native serializer being used
const typeName = buffer.readUTF8String();
const adapter = this.#adapter_registry.getAdapter(typeName);
const Klass = this.#class_registry.get(typeName);
const object = new Klass();
adapter.deserialize(buffer, object);
return object;
case HEADER_TAG_INLINE_SCHEMA:
case HEADER_TAG_REF_SCHEMA:
return this.#deserializeObject(buffer, header);
case HEADER_TAG_NULL:
return null;
case HEADER_TAG_UNDEFINED:
return undefined;
case HEADER_TAG_BOOLEAN_TRUE:
return true;
case HEADER_TAG_BOOLEAN_FALSE:
return false;
case HEADER_TAG_POSITIVE_INTEGER:
return buffer.readUintVar();
case HEADER_TAG_NEGATIVE_INTEGER:
return -buffer.readUintVar();
case HEADER_TAG_FLOAT:
return buffer.readFloat64();
case HEADER_TAG_STRING:
return buffer.readUTF8String();
default:
throw new Error(`Unsupported tag '${header}'`);
}
}
}
/**
* @readonly
* @type {number}
*/
BinaryObjectSerializationAdapter2.prototype.version = 0;