refactor: liberalize deserializer

This commit is contained in:
cha0s 2024-06-14 01:11:14 -05:00
parent 547cd5a9b9
commit 0685243b7b
4 changed files with 74 additions and 58 deletions

View File

@ -1,5 +1,6 @@
import Component from './component.js'; import Component from './component.js';
import EntityFactory from './entity-factory.js'; import EntityFactory from './entity-factory.js';
import Schema from './schema.js';
import System from './system.js'; import System from './system.js';
export default class Ecs { export default class Ecs {
@ -122,8 +123,15 @@ export default class Ecs {
static deserialize(view) { static deserialize(view) {
const ecs = new this(); const ecs = new this();
let cursor = 0; 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 count = view.getUint32(cursor, true);
const keys = Object.keys(ecs.Types);
cursor += 4; cursor += 4;
const creating = new Map(); const creating = new Map();
const updating = new Map(); const updating = new Map();
@ -142,8 +150,12 @@ export default class Ecs {
const componentId = view.getUint16(cursor, true); const componentId = view.getUint16(cursor, true);
cursor += 2; cursor += 2;
const component = keys[componentId]; const component = keys[componentId];
if (!component) { const componentSize = view.getUint32(cursor, true);
throw new Error(`can't decode component ${componentId}`); cursor += 4;
if (!this.Types[component]) {
console.error(`can't deserialize nonexistent component ${component}`);
cursor += componentSize;
continue;
} }
if (!ecs.$$entities[entityId]) { if (!ecs.$$entities[entityId]) {
creating.get(entityId)[component] = false; creating.get(entityId)[component] = false;
@ -156,7 +168,7 @@ export default class Ecs {
updating.get(component).push([entityId, false]); updating.get(component).push([entityId, false]);
} }
cursors.get(entityId)[component] = cursor; cursors.get(entityId)[component] = cursor;
cursor += ecs.Types[component].constructor.schema.readSize(view, cursor); cursor += componentSize;
} }
if (addedComponents.length > 0 && ecs.$$entities[entityId]) { if (addedComponents.length > 0 && ecs.$$entities[entityId]) {
ecs.rebuild(entityId, (types) => types.concat(addedComponents)); ecs.rebuild(entityId, (types) => types.concat(addedComponents));
@ -333,9 +345,15 @@ export default class Ecs {
view = new DataView(new ArrayBuffer(ecs.size())); view = new DataView(new ArrayBuffer(ecs.size()));
} }
let cursor = 0; 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; let entitiesWritten = 0;
cursor += 4; cursor += 4;
const keys = Object.keys(ecs.Types);
for (const entityId of ecs.entities) { for (const entityId of ecs.entities) {
const entity = ecs.get(entityId); const entity = ecs.get(entityId);
entitiesWritten += 1; entitiesWritten += 1;
@ -343,18 +361,21 @@ export default class Ecs {
cursor += 4; cursor += 4;
const entityComponents = entity.constructor.types; const entityComponents = entity.constructor.types;
view.setUint16(cursor, entityComponents.length, true); view.setUint16(cursor, entityComponents.length, true);
const componentsWrittenIndex = cursor; const componentsWrittenAt = cursor;
cursor += 2; cursor += 2;
for (const component of entityComponents) { for (const component of entityComponents) {
const instance = ecs.Types[component]; const instance = ecs.Types[component];
view.setUint16(cursor, keys.indexOf(component), true); view.setUint16(cursor, keys.indexOf(component), true);
cursor += 2; cursor += 2;
const sizeOf = instance.sizeOf(entityId);
view.setUint32(cursor, sizeOf, true);
cursor += 4;
instance.serialize(entityId, view, cursor); 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; return view;
} }
@ -363,8 +384,14 @@ export default class Ecs {
} }
size() { size() {
let size = 0;
// # of components.
size += 2;
for (const type in this.Types) {
size += Schema.sizeOf(type, {type: 'string'});
}
// # of entities. // # of entities.
let size = 4; size += 4;
for (const entityId of this.entities) { for (const entityId of this.entities) {
size += this.get(entityId).size(); size += this.get(entityId).size();
} }

View File

@ -1,4 +1,4 @@
import {expect, test} from 'vitest'; import {expect, test, vi} from 'vitest';
import Ecs from './ecs.js'; import Ecs from './ecs.js';
import System from './system.js'; import System from './system.js';
@ -403,31 +403,33 @@ test('calculates entity size', () => {
const ecs = new SizingEcs(); const ecs = new SizingEcs();
ecs.createSpecific(1, {Empty: {}, Position: {}}); ecs.createSpecific(1, {Empty: {}, Position: {}});
// ID + # of components + Empty + Position + x + y + z // 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()) expect(ecs.get(1).size())
.to.equal(22); .to.equal(30);
}); });
test('serializes and deserializes', () => { test('serializes and deserializes', () => {
class SerializingEcs extends Ecs { class SerializingEcs extends Ecs {
static Types = {Empty, Name, Position}; static Types = {Empty, Name, Position};
} }
// # of components + strings (Empty, Name, Position)
// 2 + 4 + 5 + 4 + 4 + 4 + 8 = 31
const ecs = new SerializingEcs(); const ecs = new SerializingEcs();
// ID + # of components + Empty + Position + x + y + z // 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}}); ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
// ID + # of components + Name + 'foobar' + Position + x + y + z // ID + # of components + Name + 'foobar' + Position + x + y + z
// 4 + 2 + 2 + 4 + 6 + 2 + 4 + 4 + 4 = 32 // 4 + 2 + 2 + 4 + 4 + 6 + 2 + 4 + 4 + 4 + 4 = 40
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}}); ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
const view = SerializingEcs.serialize(ecs); const view = SerializingEcs.serialize(ecs);
// # of entities + Entity(1) + Entity(16) // # of entities + Header + Entity(1) + Entity(16)
// 4 + 22 + 32 = 58 // 4 + 31 + 30 + 40 = 105
expect(view.byteLength) expect(view.byteLength)
.to.equal(58); .to.equal(105);
// Entity values. // Entity values.
expect(view.getUint32(4 + 22 - 12, true)) expect(view.getUint32(4 + 31 + 30 - 12, true))
.to.equal(64); .to.equal(64);
expect(view.getUint32(4 + 22 + 32 - 12, true)) expect(view.getUint32(4 + 31 + 30 + 40 - 12, true))
.to.equal(128); .to.equal(128);
const deserialized = SerializingEcs.deserialize(view); const deserialized = SerializingEcs.deserialize(view);
// # of entities. // # of entities.
@ -449,11 +451,33 @@ test('serializes and deserializes', () => {
.to.equal('foobar'); .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', () => { test('deserializes empty', () => {
class SerializingEcs extends Ecs { class SerializingEcs extends Ecs {
static Types = {Empty, Name, Position}; 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) expect(ecs)
.to.not.be.undefined; .to.not.be.undefined;
}); });

View File

@ -28,7 +28,7 @@ export default class EntityFactory {
let size = 0; let size = 0;
for (const component of this.constructor.types) { for (const component of this.constructor.types) {
const instance = Types[component]; 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. // ID + # of components.
return size + 4 + 2; return size + 4 + 2;

View File

@ -145,41 +145,6 @@ export default class Schema {
return {...normalized, defaultValue: this.defaultValue(normalized)}; 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) { static serialize(source, view, offset, specification) {
const viewSetMethod = this.viewSetMethods[specification.type]; const viewSetMethod = this.viewSetMethods[specification.type];
if (viewSetMethod) { if (viewSetMethod) {