diff --git a/app/ecs/ecs.js b/app/ecs/ecs.js index c98574a..33b5cef 100644 --- a/app/ecs/ecs.js +++ b/app/ecs/ecs.js @@ -1,5 +1,6 @@ import Component from './component.js'; import EntityFactory from './entity-factory.js'; +import Schema from './schema.js'; import System from './system.js'; export default class Ecs { @@ -122,8 +123,15 @@ export default class Ecs { static deserialize(view) { const ecs = new this(); let cursor = 0; + const headerComponents = view.getUint16(cursor, true); + cursor += 2; + const keys = []; + for (let i = 0; i < headerComponents; ++i) { + const wrapped = {value: cursor}; + keys[i] = Schema.deserialize(view, wrapped, {type: 'string'}); + cursor = wrapped.value; + } const count = view.getUint32(cursor, true); - const keys = Object.keys(ecs.Types); cursor += 4; const creating = new Map(); const updating = new Map(); @@ -142,8 +150,12 @@ export default class Ecs { const componentId = view.getUint16(cursor, true); cursor += 2; const component = keys[componentId]; - if (!component) { - throw new Error(`can't decode component ${componentId}`); + const componentSize = view.getUint32(cursor, true); + cursor += 4; + if (!this.Types[component]) { + console.error(`can't deserialize nonexistent component ${component}`); + cursor += componentSize; + continue; } if (!ecs.$$entities[entityId]) { creating.get(entityId)[component] = false; @@ -156,7 +168,7 @@ export default class Ecs { updating.get(component).push([entityId, false]); } cursors.get(entityId)[component] = cursor; - cursor += ecs.Types[component].constructor.schema.readSize(view, cursor); + cursor += componentSize; } if (addedComponents.length > 0 && ecs.$$entities[entityId]) { ecs.rebuild(entityId, (types) => types.concat(addedComponents)); @@ -333,9 +345,15 @@ export default class Ecs { view = new DataView(new ArrayBuffer(ecs.size())); } let cursor = 0; + const keys = Object.keys(ecs.Types); + view.setUint16(cursor, keys.length, true); + cursor += 2; + for (const i in keys) { + cursor += Schema.serialize(keys[i], view, cursor, {type: 'string'}); + } + const entitiesWrittenAt = cursor; let entitiesWritten = 0; cursor += 4; - const keys = Object.keys(ecs.Types); for (const entityId of ecs.entities) { const entity = ecs.get(entityId); entitiesWritten += 1; @@ -343,18 +361,21 @@ export default class Ecs { cursor += 4; const entityComponents = entity.constructor.types; view.setUint16(cursor, entityComponents.length, true); - const componentsWrittenIndex = cursor; + const componentsWrittenAt = cursor; cursor += 2; for (const component of entityComponents) { const instance = ecs.Types[component]; view.setUint16(cursor, keys.indexOf(component), true); cursor += 2; + const sizeOf = instance.sizeOf(entityId); + view.setUint32(cursor, sizeOf, true); + cursor += 4; instance.serialize(entityId, view, cursor); - cursor += instance.sizeOf(entityId); + cursor += sizeOf; } - view.setUint16(componentsWrittenIndex, entityComponents.length, true); + view.setUint16(componentsWrittenAt, entityComponents.length, true); } - view.setUint32(0, entitiesWritten, true); + view.setUint32(entitiesWrittenAt, entitiesWritten, true); return view; } @@ -363,8 +384,14 @@ export default class Ecs { } size() { + let size = 0; + // # of components. + size += 2; + for (const type in this.Types) { + size += Schema.sizeOf(type, {type: 'string'}); + } // # of entities. - let size = 4; + size += 4; for (const entityId of this.entities) { size += this.get(entityId).size(); } diff --git a/app/ecs/ecs.test.js b/app/ecs/ecs.test.js index c90f1a6..231cc7e 100644 --- a/app/ecs/ecs.test.js +++ b/app/ecs/ecs.test.js @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test, vi} from 'vitest'; import Ecs from './ecs.js'; import System from './system.js'; @@ -403,31 +403,33 @@ test('calculates entity size', () => { const ecs = new SizingEcs(); ecs.createSpecific(1, {Empty: {}, Position: {}}); // ID + # of components + Empty + Position + x + y + z - // 4 + 2 + 2 + 2 + 4 + 4 + 4 = 22 + // 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30 expect(ecs.get(1).size()) - .to.equal(22); + .to.equal(30); }); test('serializes and deserializes', () => { class SerializingEcs extends Ecs { static Types = {Empty, Name, Position}; } + // # of components + strings (Empty, Name, Position) + // 2 + 4 + 5 + 4 + 4 + 4 + 8 = 31 const ecs = new SerializingEcs(); // ID + # of components + Empty + Position + x + y + z - // 4 + 2 + 2 + 2 + 4 + 4 + 4 = 22 + // 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30 ecs.createSpecific(1, {Empty: {}, Position: {x: 64}}); - // ID + # of components + Name + 'foobar' + Position + x + y + z - // 4 + 2 + 2 + 4 + 6 + 2 + 4 + 4 + 4 = 32 + // ID + # of components + Name + 'foobar' + Position + x + y + z + // 4 + 2 + 2 + 4 + 4 + 6 + 2 + 4 + 4 + 4 + 4 = 40 ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}}); const view = SerializingEcs.serialize(ecs); - // # of entities + Entity(1) + Entity(16) - // 4 + 22 + 32 = 58 + // # of entities + Header + Entity(1) + Entity(16) + // 4 + 31 + 30 + 40 = 105 expect(view.byteLength) - .to.equal(58); + .to.equal(105); // Entity values. - expect(view.getUint32(4 + 22 - 12, true)) + expect(view.getUint32(4 + 31 + 30 - 12, true)) .to.equal(64); - expect(view.getUint32(4 + 22 + 32 - 12, true)) + expect(view.getUint32(4 + 31 + 30 + 40 - 12, true)) .to.equal(128); const deserialized = SerializingEcs.deserialize(view); // # of entities. @@ -449,11 +451,33 @@ test('serializes and deserializes', () => { .to.equal('foobar'); }); +test('deserializes from compatible ECS', () => { + class DeserializingEcs extends Ecs { + static Types = {Empty, Name}; + } + class SerializingEcs extends Ecs { + static Types = {Empty, Name, Position}; + } + const ecs = new SerializingEcs(); + ecs.createSpecific(1, {Empty: {}, Position: {x: 64}}); + ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}}); + const view = SerializingEcs.serialize(ecs); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const deserialized = DeserializingEcs.deserialize(view); + expect(consoleErrorSpy.mock.calls.length) + .to.equal(2); + consoleErrorSpy.mockRestore(); + expect(deserialized.get(1).toJSON()) + .to.deep.equal({Empty: {}}); + expect(deserialized.get(16).toJSON()) + .to.deep.equal({Name: {name: 'foobar'}}); +}); + test('deserializes empty', () => { class SerializingEcs extends Ecs { static Types = {Empty, Name, Position}; } - const ecs = SerializingEcs.deserialize(new DataView(new Uint32Array([0]).buffer)); + const ecs = SerializingEcs.deserialize(new DataView(new Uint16Array([0, 0, 0]).buffer)); expect(ecs) .to.not.be.undefined; }); diff --git a/app/ecs/entity-factory.js b/app/ecs/entity-factory.js index e7634b7..dbf2a6b 100644 --- a/app/ecs/entity-factory.js +++ b/app/ecs/entity-factory.js @@ -28,7 +28,7 @@ export default class EntityFactory { let size = 0; for (const component of this.constructor.types) { const instance = Types[component]; - size += 2 + instance.constructor.schema.sizeOf(instance.get(this.id)); + size += 2 + 4 + instance.constructor.schema.sizeOf(instance.get(this.id)); } // ID + # of components. return size + 4 + 2; diff --git a/app/ecs/schema.js b/app/ecs/schema.js index beb6fbf..dafb4fb 100644 --- a/app/ecs/schema.js +++ b/app/ecs/schema.js @@ -145,41 +145,6 @@ export default class Schema { return {...normalized, defaultValue: this.defaultValue(normalized)}; } - static readSize(view, offset, specification) { - const size = this.size(specification); - if (size > 0) { - return size; - } - switch (specification.type) { - case 'array': { - const length = view.getUint32(offset.value, true); - offset.value += 4; - let arraySize = 0; - for (let i = 0; i < length; ++i) { - arraySize += this.readSize(view, offset, specification.subtype); - } - return 4 + arraySize; - } - case 'object': { - let objectSize = 0; - for (const key in specification.properties) { - objectSize += this.readSize(view, offset, specification.properties[key]); - } - return objectSize; - } - case 'string': { - const length = view.getUint32(offset.value, true); - offset.value += 4 + length; - return 4 + length; - } - } - } - - readSize(view, offset) { - const wrapped = {value: offset}; - return this.constructor.readSize(view, wrapped, this.specification); - } - static serialize(source, view, offset, specification) { const viewSetMethod = this.viewSetMethods[specification.type]; if (viewSetMethod) {