UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

494 lines (381 loc) • 12.1 kB
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;