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 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();
}

View File

@ -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;
});

View File

@ -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;

View File

@ -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) {