diff --git a/app/ecs/component.js b/app/ecs/component.js index 96275ca..bac1f87 100644 --- a/app/ecs/component.js +++ b/app/ecs/component.js @@ -38,7 +38,8 @@ export default class Component { return []; } const allocated = this.allocateMany(entries.length); - const {properties} = this.constructor.schema.specification; + const {properties} = this.constructor.schema.specification.concrete; + const Schema = this.constructor.schema.constructor; const keys = Object.keys(properties); const promises = []; for (let i = 0; i < entries.length; ++i) { @@ -47,13 +48,15 @@ export default class Component { this.data[allocated[i]].entity = entityId; for (let k = 0; k < keys.length; ++k) { const j = keys[k]; - const {defaultValue} = properties[j]; const instance = this.data[allocated[i]]; if (j in values) { instance[j] = values[j]; } - else if ('undefined' !== typeof defaultValue) { - instance[j] = defaultValue; + else { + const defaultValue = Schema.defaultValue(properties[j]); + if ('undefined' !== typeof defaultValue) { + instance[j] = defaultValue; + } } } promises.push(this.load(this.data[allocated[i]])); @@ -67,7 +70,7 @@ export default class Component { } deserialize(entityId, view, offset) { - const {properties} = this.constructor.schema.specification; + const {properties} = this.constructor.schema.specification.concrete; const instance = this.get(entityId); const deserialized = this.constructor.schema.deserialize(view, offset); for (const key in properties) { @@ -96,11 +99,11 @@ export default class Component { } static filterDefaults(instance) { - const {properties} = this.schema.specification; + const {properties} = this.schema.specification.concrete; + const Schema = this.schema.constructor; const json = {}; for (const key in properties) { - const {defaultValue} = properties[key]; - if (key in instance && instance[key] !== defaultValue) { + if (key in instance && instance[key] !== Schema.defaultValue(properties[key])) { json[key] = instance[key]; } } @@ -136,16 +139,16 @@ export default class Component { instanceFromSchema() { const Component = this; - const {specification} = Component.constructor.schema; + const {concrete} = Component.constructor.schema.specification; + const Schema = Component.constructor.schema.constructor; const Instance = class { $$entity = 0; constructor() { this.$$reset(); } $$reset() { - for (const key in specification.properties) { - const {defaultValue} = specification.properties[key]; - this[`$$${key}`] = defaultValue; + for (const key in concrete.properties) { + this[`$$${key}`] = Schema.defaultValue(concrete.properties[key]); } } destroy() {} @@ -166,7 +169,7 @@ export default class Component { this.$$reset(); }, }; - for (const key in specification.properties) { + for (const key in concrete.properties) { properties[key] = { get: function get() { return this[`$$${key}`]; diff --git a/app/ecs/schema-types/array.js b/app/ecs/schema-types/array.js new file mode 100644 index 0000000..3f5dec6 --- /dev/null +++ b/app/ecs/schema-types/array.js @@ -0,0 +1,40 @@ +export default function (Schema) { + return { + defaultValue: () => [], + deserialize: (view, offset, {subtype}) => { + const length = view.getUint32(offset.value, true); + offset.value += 4; + const value = []; + for (let i = 0; i < length; ++i) { + value.push(Schema.deserialize(view, offset, subtype)); + } + return value; + }, + normalize: ({subtype}) => { + return { + subtype: Schema.normalize(subtype), + } + }, + serialize: (source, view, offset, {subtype}) => { + view.setUint32(offset, source.length, true); + let size = 4; + for (const element of source) { + size += Schema.serialize( + element, + view, + offset + size, + subtype, + ); + } + return size; + }, + sizeOf: (instance, {subtype}) => { + let size = 4; + for (const element of instance) { + size += Schema.sizeOf(element, subtype); + } + return size; + }, + staticSizeOf: () => 0, + }; +} diff --git a/app/ecs/schema-types/float32.js b/app/ecs/schema-types/float32.js new file mode 100644 index 0000000..c5d4fa9 --- /dev/null +++ b/app/ecs/schema-types/float32.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getFloat32(offset.value, true); + offset.value += 4; + return value; + }, + serialize: (source, view, offset) => { + view.setFloat32(offset, source, true); + return 4; + }, + sizeOf: () => 4, + staticSizeOf: () => 4, + }; +} diff --git a/app/ecs/schema-types/float64.js b/app/ecs/schema-types/float64.js new file mode 100644 index 0000000..a54d7b8 --- /dev/null +++ b/app/ecs/schema-types/float64.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getFloat64(offset.value, true); + offset.value += 8; + return value; + }, + serialize: (source, view, offset) => { + view.setFloat64(offset, source, true); + return 8; + }, + sizeOf: () => 8, + staticSizeOf: () => 8, + }; +} diff --git a/app/ecs/schema-types/int16.js b/app/ecs/schema-types/int16.js new file mode 100644 index 0000000..e025f56 --- /dev/null +++ b/app/ecs/schema-types/int16.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getInt16(offset.value, true); + offset.value += 2; + return value; + }, + serialize: (source, view, offset) => { + view.setInt16(offset, source, true); + return 2; + }, + sizeOf: () => 2, + staticSizeOf: () => 2, + }; +} diff --git a/app/ecs/schema-types/int32.js b/app/ecs/schema-types/int32.js new file mode 100644 index 0000000..bcfae50 --- /dev/null +++ b/app/ecs/schema-types/int32.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getInt32(offset.value, true); + offset.value += 4; + return value; + }, + serialize: (source, view, offset) => { + view.setInt32(offset, source, true); + return 4; + }, + sizeOf: () => 4, + staticSizeOf: () => 4, + }; +} diff --git a/app/ecs/schema-types/int64.js b/app/ecs/schema-types/int64.js new file mode 100644 index 0000000..b380ad0 --- /dev/null +++ b/app/ecs/schema-types/int64.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0n, + deserialize: (view, offset) => { + const value = view.getBigInt64(offset.value, true); + offset.value += 8; + return value; + }, + serialize: (source, view, offset) => { + view.setBigInt64(offset, source, true); + return 8; + }, + sizeOf: () => 8, + staticSizeOf: () => 8, + }; +} diff --git a/app/ecs/schema-types/int8.js b/app/ecs/schema-types/int8.js new file mode 100644 index 0000000..786c9d1 --- /dev/null +++ b/app/ecs/schema-types/int8.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getInt8(offset.value, true); + offset.value += 1; + return value; + }, + serialize: (source, view, offset) => { + view.setInt8(offset, source, true); + return 1; + }, + sizeOf: () => 1, + staticSizeOf: () => 1, + }; +} diff --git a/app/ecs/schema-types/map.js b/app/ecs/schema-types/map.js new file mode 100644 index 0000000..71be98e --- /dev/null +++ b/app/ecs/schema-types/map.js @@ -0,0 +1,50 @@ +export default function (Schema) { + return { + defaultValue: () => ({}), + deserialize: (view, offset, concrete) => { + const length = view.getUint32(offset.value, true); + offset.value += 4; + const value = {}; + for (let i = 0; i < length; ++i) { + const key = Schema.deserialize(view, offset, concrete.key); + value[key] = Schema.deserialize(view, offset, concrete.value); + } + return value; + }, + normalize: ({value}) => { + return { + key: Schema.normalize({type: 'string'}), + value: Schema.normalize(value), + } + }, + serialize: (source, view, offset, concrete) => { + const keys = Object.keys(source); + view.setUint32(offset, keys.length, true); + let size = 4; + for (const key of keys) { + size += Schema.serialize( + key, + view, + offset + size, + concrete.key, + ); + size += Schema.serialize( + source[key], + view, + offset + size, + concrete.value, + ); + } + return size; + }, + sizeOf: (instance, concrete) => { + let size = 4; + for (const key in instance) { + size += Schema.sizeOf(key, concrete.key); + size += Schema.sizeOf(instance[key], concrete.value); + } + return size; + }, + staticSizeOf: () => 0, + }; +} diff --git a/app/ecs/schema-types/object.js b/app/ecs/schema-types/object.js new file mode 100644 index 0000000..8c37b84 --- /dev/null +++ b/app/ecs/schema-types/object.js @@ -0,0 +1,55 @@ +export default function (Schema) { + return { + defaultValue: ({properties}) => { + const object = {}; + for (const key in properties) { + object[key] = Schema.defaultValue(properties[key]); + } + return object; + }, + deserialize: (view, offset, {properties}) => { + const value = {}; + for (const key in properties) { + value[key] = Schema.deserialize(view, offset, properties[key]); + } + return value; + }, + normalize: ({properties}) => { + const normalized = {properties: {}}; + for (const key in properties) { + normalized.properties[key] = Schema.normalize(properties[key]) + } + return normalized; + }, + serialize: (source, view, offset, {properties}) => { + let size = 0; + for (const key in properties) { + size += Schema.serialize( + source[key], + view, + offset + size, + properties[key], + ); + } + return size; + }, + sizeOf: (instance, {properties}) => { + let size = 0; + for (const key in properties) { + size += Schema.sizeOf(instance[key], properties[key]); + } + return size; + }, + staticSizeOf: ({properties}) => { + let size = 0; + for (const key in properties) { + const propertySize = Schema.size(properties[key]); + if (0 === propertySize) { + return 0; + } + size += propertySize; + } + return size; + }, + }; +} diff --git a/app/ecs/schema-types/string.js b/app/ecs/schema-types/string.js new file mode 100644 index 0000000..e05624c --- /dev/null +++ b/app/ecs/schema-types/string.js @@ -0,0 +1,31 @@ +const decoder = new TextDecoder(); +const encoder = new TextEncoder(); + +export default function () { + return { + defaultValue: () => '', + deserialize: (view, offset) => { + const length = view.getUint32(offset.value, true); + offset.value += 4; + const {buffer, byteOffset} = view; + const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length)); + offset.value += length; + return value; + }, + serialize: (source, view, offset) => { + const bytes = encoder.encode(source); + view.setUint32(offset, bytes.length, true); + offset += 4; + for (let i = 0; i < bytes.length; ++i) { + view.setUint8(offset++, bytes[i]); + } + return 4 + bytes.length; + }, + sizeOf: (instance) => { + let size = 4; + size += (encoder.encode(instance)).length; + return size; + }, + staticSizeOf: () => 0, + }; +} diff --git a/app/ecs/schema-types/uint16.js b/app/ecs/schema-types/uint16.js new file mode 100644 index 0000000..f7c59ba --- /dev/null +++ b/app/ecs/schema-types/uint16.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getUint16(offset.value, true); + offset.value += 2; + return value; + }, + serialize: (source, view, offset) => { + view.setUint16(offset, source, true); + return 2; + }, + sizeOf: () => 2, + staticSizeOf: () => 2, + }; +} diff --git a/app/ecs/schema-types/uint32.js b/app/ecs/schema-types/uint32.js new file mode 100644 index 0000000..bfb563f --- /dev/null +++ b/app/ecs/schema-types/uint32.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getUint32(offset.value, true); + offset.value += 4; + return value; + }, + serialize: (source, view, offset) => { + view.setUint32(offset, source, true); + return 4; + }, + sizeOf: () => 4, + staticSizeOf: () => 4, + }; +} diff --git a/app/ecs/schema-types/uint64.js b/app/ecs/schema-types/uint64.js new file mode 100644 index 0000000..864f463 --- /dev/null +++ b/app/ecs/schema-types/uint64.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0n, + deserialize: (view, offset) => { + const value = view.getBigUint64(offset.value, true); + offset.value += 8; + return value; + }, + serialize: (source, view, offset) => { + view.setBigUint64(offset, source, true); + return 8; + }, + sizeOf: () => 8, + staticSizeOf: () => 8, + }; +} diff --git a/app/ecs/schema-types/uint8.js b/app/ecs/schema-types/uint8.js new file mode 100644 index 0000000..a6c84ee --- /dev/null +++ b/app/ecs/schema-types/uint8.js @@ -0,0 +1,16 @@ +export default function () { + return { + defaultValue: () => 0, + deserialize: (view, offset) => { + const value = view.getUint8(offset.value, true); + offset.value += 1; + return value; + }, + serialize: (source, view, offset) => { + view.setUint8(offset, source, true); + return 1; + }, + sizeOf: () => 1, + staticSizeOf: () => 1, + }; +} diff --git a/app/ecs/schema.js b/app/ecs/schema.js index a379f65..16d1749 100644 --- a/app/ecs/schema.js +++ b/app/ecs/schema.js @@ -1,303 +1,66 @@ -const encoder = new TextEncoder(); - export default class Schema { - $$size = 0; + static $$types = {}; specification; - static viewGetMethods = { - uint8: 'getUint8', - int8: 'getInt8', - uint16: 'getUint16', - int16: 'getInt16', - uint32: 'getUint32', - int32: 'getInt32', - float32: 'getFloat32', - float64: 'getFloat64', - int64: 'getBigInt64', - uint64: 'getBigUint64', - }; - - static viewSetMethods = { - uint8: 'setUint8', - int8: 'setInt8', - uint16: 'setUint16', - int16: 'setInt16', - uint32: 'setUint32', - int32: 'setInt32', - float32: 'setFloat32', - float64: 'setFloat64', - int64: 'setBigInt64', - uint64: 'setBigUint64', - }; - constructor(specification) { this.specification = this.constructor.normalize(specification); } - static deserialize(view, offset, specification) { - const viewGetMethod = this.viewGetMethods[specification.type]; - if (viewGetMethod) { - const value = view[viewGetMethod](offset.value, true); - offset.value += this.size(specification); - return value; - } - switch (specification.type) { - case 'array': { - const length = view.getUint32(offset.value, true); - offset.value += 4; - const value = []; - for (let i = 0; i < length; ++i) { - value.push(this.deserialize(view, offset, specification.subtype)); - } - return value; - } - case 'map': { - const length = view.getUint32(offset.value, true); - offset.value += 4; - const value = {}; - for (let i = 0; i < length; ++i) { - const key = this.deserialize(view, offset, {type: 'string'}); - value[key] = this.deserialize(view, offset, specification.value); - } - return value; - } - case 'object': { - const value = {}; - for (const key in specification.properties) { - value[key] = this.deserialize(view, offset, specification.properties[key]); - } - return value; - } - case 'string': { - const length = view.getUint32(offset.value, true); - offset.value += 4; - const {buffer, byteOffset} = view; - const decoder = new TextDecoder(); - const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length)); - offset.value += length; - return value; - } - } - } - - deserialize(view, offset = 0) { - const wrapped = {value: offset}; - return this.constructor.deserialize(view, wrapped, this.specification); - } - - static defaultValue(specification) { - if (specification.defaultValue) { - return specification.defaultValue; - } - switch (specification.type) { - case 'uint8': case 'int8': - case 'uint16': case 'int16': - case 'uint32': case 'int32': - case 'uint64': case 'int64': - case 'float32': case 'float64': { - return 0; - } - case 'array': { - return []; - } - case 'map': { - return {}; - } - case 'object': { - const object = {}; - for (const key in specification.properties) { - object[key] = this.defaultValue(specification.properties[key]); - } - return object; - } - case 'string': { - return ''; - } + static defaultValue({$, concrete}) { + if (concrete.defaultValue) { + return concrete.defaultValue; } + return $.defaultValue(concrete); } defaultValue() { return this.constructor.defaultValue(this.specification); } - static normalize(specification) { - let normalized = specification; - switch (specification.type) { - case 'array': { - normalized.subtype = this.normalize(specification.subtype); - break; - } - case 'map': { - normalized.value = this.normalize(specification.value); - break; - } - case 'object': { - for (const key in specification.properties) { - normalized.properties[key] = this.normalize(specification.properties[key]) - } - break; - } - case 'uint8': - case 'int8': - case 'uint16': - case 'int16': - case 'uint32': - case 'int32': - case 'uint64': - case 'int64': - case 'float32': - case 'float64': - case 'string': { - break; - } - /* v8 ignore next 2 */ - default: - throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`); - } - return {...normalized, defaultValue: this.defaultValue(normalized)}; + static deserialize(view, offset, {$, concrete}) { + return $.deserialize(view, offset, concrete); } - static serialize(source, view, offset, specification) { - const viewSetMethod = this.viewSetMethods[specification.type]; - if (viewSetMethod) { - view[viewSetMethod](offset, source, true); - return this.size(specification); - } - switch (specification.type) { - case 'array': { - view.setUint32(offset, source.length, true); - let arraySize = 4; - for (const element of source) { - arraySize += this.serialize( - element, - view, - offset + arraySize, - specification.subtype, - ); - } - return arraySize; - } - case 'map': { - const keys = Object.keys(source); - view.setUint32(offset, keys.length, true); - let mapSize = 4; - for (const key of keys) { - mapSize += this.serialize( - key, - view, - offset + mapSize, - {type: 'string'}, - ); - mapSize += this.serialize( - source[key], - view, - offset + mapSize, - specification.value, - ); - } - return mapSize; - } - case 'object': { - let objectSize = 0; - for (const key in specification.properties) { - objectSize += this.serialize( - source[key], - view, - offset + objectSize, - specification.properties[key], - ); - } - return objectSize; - } - case 'string': { - const encoder = new TextEncoder(); - const bytes = encoder.encode(source); - view.setUint32(offset, bytes.length, true); - offset += 4; - for (let i = 0; i < bytes.length; ++i) { - view.setUint8(offset++, bytes[i]); - } - return 4 + bytes.length; - } + deserialize(view, offset = 0) { + return this.constructor.deserialize(view, {value: offset}, this.specification); + } + + static normalize({type, ...rest}) { + const $$type = this.$$types[type]; + if (!$$type) { + throw new TypeError(`unregistered schema type '${type}'`); } + return { + $: this.$$types[type], + concrete: $$type.normalize ? $$type.normalize(rest) : rest, + }; + } + + static serialize(source, view, offset, {$, concrete}) { + return $.serialize(source, view, offset, concrete); } serialize(source, view, offset = 0) { this.constructor.serialize(source, view, offset, this.specification); } - static sizeOf(concrete, specification) { - const size = this.size(specification); - if (size > 0) { - return size; - } - let fullSize = 0; - const {type} = specification; - switch (type) { - case 'array': { - fullSize += 4; - for (const element of concrete) { - fullSize += this.sizeOf(element, specification.subtype); - } - break; - } - case 'map': { - fullSize += 4; - for (const key in concrete) { - fullSize += this.sizeOf(key, {type: 'string'}); - fullSize += this.sizeOf(concrete[key], specification.value); - } - break; - } - case 'object': { - for (const key in specification.properties) { - fullSize += this.sizeOf(concrete[key], specification.properties[key]); - } - break; - } - case 'string': - fullSize += 4; - fullSize += (encoder.encode(concrete)).length; - break; - } - return fullSize; + static sizeOf(instance, {$, concrete}) { + return $.sizeOf(instance, concrete); } - sizeOf(concrete) { - return this.constructor.sizeOf(concrete, this.specification); + sizeOf(instance) { + return this.constructor.sizeOf(instance, this.specification); } - static size(specification) { - switch (specification.type) { - case 'array': return 0; - case 'map': return 0; // TODO could be fixed-size w/ fixed-size key - case 'object': { - let size = 0; - for (const key in specification.properties) { - const propertySize = this.size(specification.properties[key]); - if (0 === propertySize) { - return 0; - } - size += propertySize; - } - return size; - } - case 'uint8': case 'int8': { - return 1; - } - case 'uint16': case 'int16': { - return 2; - } - case 'uint32': case 'int32': case 'float32': { - return 4; - } - case 'uint64': case 'int64': case 'float64': { - return 8; - } - default: return 0; - } + static size({$, concrete}) { + return $.staticSizeOf(concrete); } } + +const imports = import.meta.glob('./schema-types/*.js', {eager: true, import: 'default'}); +for (const path in imports) { + Schema.$$types[path.replace(/.\/schema-types\/(.*)\.js/, '$1')] = imports[path](Schema); +} diff --git a/app/ecs/schema.test.js b/app/ecs/schema.test.js index 50a1d5d..366028e 100644 --- a/app/ecs/schema.test.js +++ b/app/ecs/schema.test.js @@ -4,10 +4,6 @@ import Schema from './schema.js'; test('defaults values', () => { const compare = (specification, value) => { - expect(Schema.defaultValue(specification)) - .to.deep.equal(value); - expect(new Schema(specification).specification.defaultValue) - .to.deep.equal(value); expect(new Schema(specification).defaultValue()) .to.deep.equal(value); }; @@ -18,13 +14,17 @@ test('defaults values', () => { 'int16', 'uint32', 'int32', - 'uint64', - 'int64', 'float32', 'float64', ].forEach((type) => { compare({type}, 0); }); + [ + 'uint64', + 'int64', + ].forEach((type) => { + compare({type}, 0n); + }); compare({type: 'string'}, ''); compare({type: 'array', subtype: {type: 'string'}}, []); compare( @@ -167,11 +167,11 @@ test('encodes and decodes', () => { [{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}], [{type: 'object', properties: {foo: {type: 'uint8'}}}, {foo: 64}], ]; - entries.forEach(([specification, concrete]) => { + entries.forEach(([specification, instance]) => { const schema = new Schema(specification); - const view = new DataView(new ArrayBuffer(schema.sizeOf(concrete))); - schema.serialize(concrete, view); - expect(concrete) + const view = new DataView(new ArrayBuffer(schema.sizeOf(instance))); + schema.serialize(instance, view); + expect(instance) .to.deep.equal(schema.deserialize(view)); }); });